forked from meli/meli
1
Fork 0

melib/jmap: respect max_objects_in_get when fetching email

jmap-batch-fetch
Manos Pitsidianakis 2022-10-15 18:10:07 +03:00
parent 88a1f0d4bc
commit e64f2077a8
9 changed files with 285 additions and 172 deletions

View File

@ -72,6 +72,9 @@ macro_rules! _impl {
}
}
pub const JMAP_CORE_CAPABILITY: &str = "urn:ietf:params:jmap:core";
pub const JMAP_MAIL_CAPABILITY: &str = "urn:ietf:params:jmap:mail";
pub mod operations;
use operations::*;
@ -196,6 +199,7 @@ pub struct Store {
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
pub is_subscribed: Arc<IsSubscribedFn>,
pub core_capabilities: Arc<Mutex<rfc8620::CapabilitiesObject>>,
pub event_consumer: BackendEventConsumer,
}
@ -330,15 +334,19 @@ impl MailBackend for JmapType {
Ok(Box::pin(async_stream::try_stream! {
let mut conn = connection.lock().await;
conn.connect().await?;
let res = protocol::fetch(
&conn,
&store,
mailbox_hash,
).await?;
if res.is_empty() {
return;
let batch_size: u64 = conn.store.core_capabilities.lock().unwrap().max_objects_in_get;
let mut fetch_state = protocol::EmailFetchState::Start { batch_size };
loop {
let res = fetch_state.fetch(
&conn,
&store,
mailbox_hash,
).await?;
if res.is_empty() {
return;
}
yield res;
}
yield res;
}))
}
@ -889,7 +897,7 @@ impl JmapType {
event_consumer,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
collection: Collection::default(),
core_capabilities: Arc::new(Mutex::new(rfc8620::CapabilitiesObject::default())),
byte_cache: Default::default(),
id_store: Default::default(),
reverse_id_store: Default::default(),

View File

@ -88,19 +88,15 @@ impl JmapConnection {
}
Ok(s) => s,
};
if !session
.capabilities
.contains_key("urn:ietf:params:jmap:core")
{
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
if !session.capabilities.contains_key(JMAP_CORE_CAPABILITY) {
let err = MeliError::new(format!("Server {} did not return JMAP Core capability ({core_capability}). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", "), core_capability=JMAP_CORE_CAPABILITY));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
if !session
.capabilities
.contains_key("urn:ietf:params:jmap:mail")
{
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.store.core_capabilities.lock().unwrap() =
session.capabilities["urn:ietf:params:jmap:core"].clone();
if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) {
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability ({mail_capability}). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", "), mail_capability=JMAP_MAIL_CAPABILITY));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -111,7 +107,7 @@ impl JmapConnection {
}
pub fn mail_account_id(&self) -> Id<Account> {
self.session.lock().unwrap().primary_accounts["urn:ietf:params:jmap:mail"].clone()
self.session.lock().unwrap().primary_accounts[JMAP_MAIL_CAPABILITY].clone()
}
pub fn session_guard(&'_ self) -> MutexGuard<'_, JmapSession> {
@ -359,4 +355,29 @@ impl JmapConnection {
Ok(())
}
pub async fn send_request(&self, request: String) -> Result<String> {
let api_url = self.session.lock().unwrap().api_url.clone();
let mut res = self.client.post_async(api_url.as_str(), request).await?;
let res_text = res.text().await?;
debug!(&res_text);
let _: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
crate::log(err.to_string(), crate::ERROR);
crate::log(
format!(
"Tried to deserialize this server response as a MethodResponse json:\n{}",
&res_text
),
crate::DEBUG,
);
return Err(err);
}
Ok(s) => s,
};
Ok(res_text)
}
}

View File

@ -26,3 +26,6 @@ pub use email::*;
mod mailbox;
pub use mailbox::*;
mod thread;
pub use thread::*;

View File

@ -33,13 +33,6 @@ use std::hash::Hasher;
mod import;
pub use import::*;
#[derive(Debug)]
pub struct ThreadObject;
impl Object for ThreadObject {
const NAME: &'static str = "Thread";
}
impl Id<EmailObject> {
pub fn into_hash(&self) -> EnvelopeHash {
let mut h = DefaultHasher::new();

View File

@ -0,0 +1,80 @@
/*
* meli - jmap module.
*
* Copyright 2019-2022 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use core::marker::PhantomData;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ThreadObject {
#[serde(default)]
pub id: Id<ThreadObject>,
#[serde(default)]
pub email_ids: Vec<Id<EmailObject>>,
}
impl Object for ThreadObject {
const NAME: &'static str = "Thread";
}
impl ThreadObject {
_impl!(get email_ids, email_ids: Vec<Id<EmailObject>>);
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGet {
#[serde(flatten)]
pub get_call: Get<ThreadObject>,
}
impl Method<ThreadObject> for ThreadGet {
const NAME: &'static str = "Thread/get";
}
impl ThreadGet {
pub const RESULT_FIELD_THREAD_IDS: ResultField<EmailGet, EmailObject> =
ResultField::<EmailGet, EmailObject> {
field: "/list/*/threadId",
_ph: PhantomData,
};
pub fn new(get_call: Get<ThreadObject>) -> Self {
ThreadGet { get_call }
}
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ThreadChanges {
#[serde(flatten)]
pub changes_call: Changes<ThreadObject>,
}
impl Method<ThreadObject> for ThreadChanges {
const NAME: &'static str = "Thread/changes";
}
impl ThreadChanges {
pub fn new(changes_call: Changes<ThreadObject>) -> Self {
ThreadChanges { changes_call }
}
}

View File

@ -27,7 +27,7 @@ use std::convert::{TryFrom, TryInto};
pub type UtcDate = String;
use super::rfc8620::Object;
use super::rfc8620::{Object, State};
macro_rules! get_request_no {
($lock:expr) => {{
@ -86,30 +86,16 @@ pub struct JsonResponse<'a> {
pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash, JmapMailbox>> {
let seq = get_request_no!(conn.request_no);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(
api_url.as_str(),
serde_json::to_string(&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Mailbox/get", {
"accountId": conn.mail_account_id()
},
format!("#m{}",seq).as_str()]],
}))?,
)
let res_text = conn
.send_request(serde_json::to_string(&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Mailbox/get", {
"accountId": conn.mail_account_id()
},
format!("#m{}",seq).as_str()]],
}))?)
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = GetResponse::<MailboxObject>::try_from(v.method_responses.remove(0))?;
let GetResponse::<MailboxObject> {
@ -249,109 +235,131 @@ pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<En
}
*/
pub async fn fetch(
conn: &JmapConnection,
store: &Store,
mailbox_hash: MailboxHash,
) -> Result<Vec<Envelope>> {
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let email_query_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().clone())
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
.into(),
)))
.position(0),
)
.collapse_threads(false);
#[derive(Copy, Clone)]
pub enum EmailFetchState {
Start { batch_size: u64 },
Ongoing { position: u64, batch_size: u64 },
}
let mut req = Request::new(conn.request_no.clone());
let prev_seq = req.add_call(&email_query_call);
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::reference(
prev_seq,
EmailQuery::RESULT_FIELD_IDS,
)))
.account_id(conn.mail_account_id().clone()),
);
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
.client
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
impl EmailFetchState {
pub async fn must_update_state(
&mut self,
conn: &JmapConnection,
mailbox_hash: MailboxHash,
state: State<EmailObject>,
) -> Result<bool> {
{
let (is_empty, is_equal) = {
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
mailboxes_lck
.get(&mailbox_hash)
.map(|mbox| {
let current_state_lck = mbox.email_state.lock().unwrap();
(
current_state_lck.is_none(),
current_state_lck.as_ref() != Some(&state),
)
})
.unwrap_or((true, true))
};
if is_empty {
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
*mbox.email_state.lock().unwrap() = Some(state);
});
} else if !is_equal {
conn.email_changes(mailbox_hash).await?;
}
Ok(is_empty || !is_equal)
}
Ok(s) => s,
};
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let query_response = QueryResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
store
.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|mbox| {
*mbox.email_query_state.lock().unwrap() = Some(query_response.query_state);
});
let GetResponse::<EmailObject> { list, state, .. } = e;
{
let (is_empty, is_equal) = {
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
mailboxes_lck
.get(&mailbox_hash)
.map(|mbox| {
let current_state_lck = mbox.email_state.lock().unwrap();
(
current_state_lck.is_none(),
current_state_lck.as_ref() != Some(&state),
}
pub async fn fetch(
&mut self,
conn: &JmapConnection,
store: &Store,
mailbox_hash: MailboxHash,
) -> Result<Vec<Envelope>> {
loop {
match *self {
EmailFetchState::Start { batch_size } => {
*self = EmailFetchState::Ongoing {
position: 0,
batch_size,
};
continue;
}
EmailFetchState::Ongoing {
mut position,
batch_size,
} => {
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let email_query_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().clone())
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
.into(),
)))
.position(position)
.limit(Some(batch_size)),
)
})
.unwrap_or((true, true))
};
if is_empty {
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
*mbox.email_state.lock().unwrap() = Some(state);
});
} else if !is_equal {
conn.email_changes(mailbox_hash).await?;
.collapse_threads(false);
let mut req = Request::new(conn.request_no.clone());
let prev_seq = req.add_call(&email_query_call);
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::reference(
prev_seq,
EmailQuery::RESULT_FIELD_IDS,
)))
.account_id(conn.mail_account_id().clone()),
);
let _prev_seq = req.add_call(&email_call);
let res_text = conn.send_request(serde_json::to_string(&req)?).await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let e =
GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { list, state, .. } = e;
if self.must_update_state(conn, mailbox_hash, state).await? {
*self = EmailFetchState::Start { batch_size };
continue;
}
let mut total = BTreeSet::default();
let mut unread = BTreeSet::default();
let mut ret = Vec::with_capacity(list.len());
for obj in list {
let env = store.add_envelope(obj);
total.insert(env.hash());
if !env.is_seen() {
unread.insert(env.hash());
}
ret.push(env);
}
let mut mailboxes_lck = store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
mbox.total_emails.lock().unwrap().insert_existing_set(total);
mbox.unread_emails
.lock()
.unwrap()
.insert_existing_set(unread);
});
position += batch_size;
*self = EmailFetchState::Ongoing {
position,
batch_size,
};
return Ok(ret);
}
}
}
}
let mut total = BTreeSet::default();
let mut unread = BTreeSet::default();
let mut ret = Vec::with_capacity(list.len());
for obj in list {
let env = store.add_envelope(obj);
total.insert(env.hash());
if !env.is_seen() {
unread.insert(env.hash());
}
ret.push(env);
}
let mut mailboxes_lck = store.mailboxes.write().unwrap();
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
mbox.total_emails.lock().unwrap().insert_existing_set(total);
mbox.unread_emails
.lock()
.unwrap()
.insert_existing_set(unread);
});
Ok(ret)
}
pub fn keywords_to_flags(keywords: Vec<String>) -> (Flag, Vec<String>) {

View File

@ -202,7 +202,7 @@ impl<OBJ> State<OBJ> {
}
}
#[derive(Deserialize, Serialize, Debug, Default)]
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct JmapSession {
pub capabilities: HashMap<String, CapabilitiesObject>,
@ -223,7 +223,7 @@ impl Object for JmapSession {
const NAME: &'static str = "Session";
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject {
#[serde(default)]
@ -244,7 +244,7 @@ pub struct CapabilitiesObject {
pub collation_algorithms: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub name: String,
@ -259,7 +259,7 @@ impl Object for Account {
const NAME: &'static str = "Account";
}
#[derive(Debug)]
#[derive(Copy, Clone, Debug)]
pub struct BlobObject;
impl Object for BlobObject {
@ -276,7 +276,7 @@ impl Object for BlobObject {
///
/// The id of the account to use.
///
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Get<OBJ: Object>
where
@ -389,7 +389,7 @@ impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Get<OBJ> {
}
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MethodResponse<'a> {
#[serde(borrow)]
@ -400,7 +400,7 @@ pub struct MethodResponse<'a> {
pub session_state: State<JmapSession>,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GetResponse<OBJ: Object> {
pub account_id: Id<Account>,
@ -427,7 +427,7 @@ impl<OBJ: Object> GetResponse<OBJ> {
_impl!(get_mut not_found_mut, not_found: Vec<Id<OBJ>>);
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
enum JmapError {
RequestTooLarge,
@ -435,7 +435,7 @@ enum JmapError {
InvalidResultReference,
}
#[derive(Serialize, Debug)]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
where
@ -499,7 +499,7 @@ pub fn bool_true() -> bool {
true
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryResponse<OBJ: Object> {
pub account_id: Id<Account>,
@ -580,7 +580,7 @@ impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
/// positive integer greater than 0. If a value outside of this range
/// is given, the server MUST re
///
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
/* ch-ch-ch-ch-ch-Changes */
pub struct Changes<OBJ: Object>
@ -636,7 +636,7 @@ where
);
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ChangesResponse<OBJ: Object> {
pub account_id: Id<Account>,
@ -678,7 +678,7 @@ impl<OBJ: Object> ChangesResponse<OBJ> {
///and dependencies that may exist if doing multiple operations at once
///(for example, to ensure there is always a minimum number of a certain
///record type).
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Set<OBJ: Object>
where
@ -788,7 +788,7 @@ where
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SetResponse<OBJ: Object> {
///o accountId: "Id"
@ -859,7 +859,7 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetRes
}
}
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "type", content = "description")]
pub enum SetError {
@ -992,7 +992,7 @@ pub fn upload_request_format(upload_url: &str, account_id: &Id<Account>) -> Stri
ret
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UploadResponse {
///o accountId: "Id"
@ -1024,7 +1024,7 @@ pub struct UploadResponse {
/// The "Foo/queryChanges" method allows a client to efficiently update
/// the state of a cached query to match the new state on the server. It
/// takes the following arguments:
#[derive(Serialize, Debug)]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ: Object>
where
@ -1093,7 +1093,7 @@ where
_impl!(calculate_total: bool);
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryChangesResponse<OBJ: Object> {
/// The id of the account used for the call.
@ -1170,7 +1170,7 @@ pub struct QueryChangesResponse<OBJ: Object> {
pub added: Vec<AddedItem<OBJ>>,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AddedItem<OBJ: Object> {
pub id: Id<OBJ>,

View File

@ -23,9 +23,9 @@ use crate::backends::jmap::protocol::Method;
use crate::backends::jmap::rfc8620::Object;
use crate::backends::jmap::rfc8620::ResultField;
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub enum JmapArgument<T> {
pub enum JmapArgument<T: Clone> {
Value(T),
ResultReference {
result_of: String,
@ -34,7 +34,7 @@ pub enum JmapArgument<T> {
},
}
impl<T> JmapArgument<T> {
impl<T: Clone> JmapArgument<T> {
pub fn value(v: T) -> Self {
JmapArgument::Value(v)
}

View File

@ -21,7 +21,7 @@
use super::*;
#[derive(Serialize, Debug)]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Comparator<OBJ: Object> {
property: String,