melib/jmap: respect max_objects_in_get when fetching email

Fixes #144
pull/260/head
Manos Pitsidianakis 2023-07-18 11:04:00 +03:00
parent c4c245ee19
commit 0219dc8707
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
9 changed files with 296 additions and 204 deletions

View File

@ -63,6 +63,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::*;
@ -174,6 +177,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,
}
@ -299,15 +303,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;
}))
}
@ -922,7 +930,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

@ -166,39 +166,15 @@ impl JmapConnection {
}
Ok(s) => s,
};
if !session
.capabilities
.contains_key("urn:ietf:params:jmap:core")
{
let err = Error::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 = Error::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 = Error::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[JMAP_CORE_CAPABILITY].clone();
if !session.capabilities.contains_key(JMAP_MAIL_CAPABILITY) {
let err = Error::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);
}
@ -209,7 +185,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> {
@ -463,4 +439,22 @@ 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 = Error::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()));
log::error!("{}", &err);
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

@ -35,13 +35,6 @@ use crate::{
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 {
EnvelopeHash::from_bytes(self.inner.as_bytes())

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 {
Self { 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 {
Self { changes_call }
}
}

View File

@ -28,7 +28,7 @@ use super::{mailbox::JmapMailbox, *};
pub type UtcDate = String;
use super::rfc8620::Object;
use super::rfc8620::{Object, State};
macro_rules! get_request_no {
($lock:expr) => {{
@ -47,7 +47,7 @@ pub trait Method<OBJ: Object>: Serialize {
const NAME: &'static str;
}
static USING: &[&str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
static USING: &[&str] = &[JMAP_CORE_CAPABILITY, JMAP_MAIL_CAPABILITY];
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@ -80,36 +80,17 @@ impl Request {
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": [JMAP_CORE_CAPABILITY, JMAP_MAIL_CAPABILITY],
"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 = Error::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> {
@ -256,115 +237,146 @@ 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())
.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()),
);
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 = Error::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 {
Self::Start { batch_size } => {
*self = Self::Ongoing {
position: 0,
batch_size,
};
continue;
}
Self::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 = match serde_json::from_str(&res_text) {
Err(err) => {
let err = Error::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(v) => v,
};
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 = Self::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 = Self::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

@ -209,7 +209,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>,
@ -230,7 +230,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)]
@ -251,7 +251,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,
@ -266,7 +266,7 @@ impl Object for Account {
const NAME: &'static str = "Account";
}
#[derive(Debug)]
#[derive(Copy, Clone, Debug)]
pub struct BlobObject;
impl Object for BlobObject {
@ -282,7 +282,8 @@ impl Object for BlobObject {
/// - `account_id`: `Id`
///
/// The id of the account to use.
#[derive(Deserialize, Debug)]
///
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Get<OBJ>
where
@ -403,7 +404,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)]
@ -414,7 +415,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>,
@ -448,7 +449,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,
@ -456,7 +457,7 @@ enum JmapError {
InvalidResultReference,
}
#[derive(Serialize, Debug)]
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Query<F: FilterTrait<OBJ>, OBJ>
where
@ -529,7 +530,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>,
@ -617,7 +618,8 @@ impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
/// to return. If supplied by the client, the value MUST be a
/// 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>
@ -679,7 +681,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>,
@ -728,7 +730,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>
where
@ -846,7 +848,7 @@ where
}
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SetResponse<OBJ: Object> {
/// o accountId: `Id`
@ -924,7 +926,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 {
@ -1067,7 +1069,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`
@ -1100,7 +1102,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>
where
@ -1169,7 +1171,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.
@ -1250,7 +1252,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

@ -24,9 +24,9 @@ use crate::backends::jmap::{
rfc8620::{Object, 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,
@ -35,7 +35,7 @@ pub enum JmapArgument<T> {
},
}
impl<T> JmapArgument<T> {
impl<T: Clone> JmapArgument<T> {
pub fn value(v: T) -> Self {
Self::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,