From 3250cbc443fd2666d958226f831d91850f46eace Mon Sep 17 00:00:00 2001 From: Mauro D Date: Tue, 11 Apr 2023 14:45:07 +0000 Subject: [PATCH] result references --- Cargo.toml | 28 +- crates/core/src/api/mod.rs | 1 + crates/core/src/api/request.rs | 70 +++ crates/core/src/email/get.rs | 30 +- crates/core/src/email/import.rs | 2 + crates/core/src/email/query.rs | 5 +- crates/core/src/lib.rs | 3 + crates/protocol/src/method/get.rs | 21 +- crates/protocol/src/method/query.rs | 26 +- crates/protocol/src/method/set.rs | 18 +- crates/protocol/src/parser/impls.rs | 1 - crates/protocol/src/parser/json.rs | 1 + crates/protocol/src/request/mod.rs | 9 +- crates/protocol/src/request/reference.rs | 9 + crates/protocol/src/response/mod.rs | 18 +- crates/protocol/src/response/references.rs | 535 ++++++++++++++++++++- crates/protocol/src/types/value.rs | 6 +- tests/Cargo.toml | 2 +- tests/src/store/blobs.rs | 14 +- tests/src/store/query.rs | 52 +- 20 files changed, 744 insertions(+), 107 deletions(-) create mode 100644 crates/core/src/api/mod.rs create mode 100644 crates/core/src/api/request.rs diff --git a/Cargo.toml b/Cargo.toml index b860902f..6438e4ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ -[package] -name = "stalwart-jmap" -description = "Stalwart JMAP Server" -authors = [ "Stalwart Labs Ltd. "] -repository = "https://github.com/stalwartlabs/jmap-server" -homepage = "https://stalw.art/jmap" -keywords = ["jmap", "email", "mail", "server"] -categories = ["email"] -license = "AGPL-3.0-only" -version = "0.3.0" -edition = "2021" -resolver = "2" +#[package] +#name = "stalwart-jmap" +#description = "Stalwart JMAP Server" +#authors = [ "Stalwart Labs Ltd. "] +#repository = "https://github.com/stalwartlabs/jmap-server" +#homepage = "https://stalw.art/jmap" +#keywords = ["jmap", "email", "mail", "server"] +#categories = ["email"] +#license = "AGPL-3.0-only" +#version = "0.3.0" +#edition = "2021" +#resolver = "2" -[lib] -path = "crates/core/src/lib.rs" +#[lib] +#path = "crates/core/src/lib.rs" [workspace] members = [ diff --git a/crates/core/src/api/mod.rs b/crates/core/src/api/mod.rs new file mode 100644 index 00000000..be9378d9 --- /dev/null +++ b/crates/core/src/api/mod.rs @@ -0,0 +1 @@ +pub mod request; diff --git a/crates/core/src/api/request.rs b/crates/core/src/api/request.rs new file mode 100644 index 00000000..dbe420b4 --- /dev/null +++ b/crates/core/src/api/request.rs @@ -0,0 +1,70 @@ +use protocol::{ + error::request::RequestError, + method::{get, query}, + request::{Request, RequestMethod}, + response::{Response, ResponseMethod}, +}; + +use crate::JMAP; + +impl JMAP { + pub async fn handle_request(&self, bytes: &[u8]) -> Result { + let request = Request::parse( + bytes, + self.config.request_max_calls, + self.config.request_max_size, + )?; + let mut response = Response::new( + 0, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + for mut call in request.method_calls { + // Resolve result and id references + if let Err(method_error) = response.resolve_references(&mut call.method) { + response.push_response(call.id, method_error); + continue; + } + + let method_response: ResponseMethod = match call.method { + RequestMethod::Get(mut call) => match call.take_arguments() { + get::RequestArguments::Email(arguments) => { + self.email_get(call.with_arguments(arguments)).await.into() + } + get::RequestArguments::Mailbox => todo!(), + get::RequestArguments::Thread => todo!(), + get::RequestArguments::Identity => todo!(), + get::RequestArguments::EmailSubmission => todo!(), + get::RequestArguments::PushSubscription => todo!(), + get::RequestArguments::SieveScript => todo!(), + get::RequestArguments::VacationResponse => todo!(), + get::RequestArguments::Principal => todo!(), + }, + RequestMethod::Query(mut call) => match call.take_arguments() { + query::RequestArguments::Email(arguments) => self + .email_query(call.with_arguments(arguments)) + .await + .into(), + query::RequestArguments::Mailbox(_) => todo!(), + query::RequestArguments::EmailSubmission => todo!(), + query::RequestArguments::SieveScript => todo!(), + query::RequestArguments::Principal => todo!(), + }, + RequestMethod::Set(_) => todo!(), + RequestMethod::Changes(_) => todo!(), + RequestMethod::Copy(_) => todo!(), + RequestMethod::CopyBlob(_) => todo!(), + RequestMethod::ImportEmail(call) => self.email_import(call).await.into(), + RequestMethod::ParseEmail(_) => todo!(), + RequestMethod::QueryChanges(_) => todo!(), + RequestMethod::SearchSnippet(_) => todo!(), + RequestMethod::ValidateScript(_) => todo!(), + RequestMethod::Echo(call) => call.into(), + RequestMethod::Error(error) => error.into(), + }; + response.push_response(call.id, method_response); + } + + Ok(response) + } +} diff --git a/crates/core/src/email/get.rs b/crates/core/src/email/get.rs index d4439e15..4e50eddf 100644 --- a/crates/core/src/email/get.rs +++ b/crates/core/src/email/get.rs @@ -1,9 +1,9 @@ use mail_parser::Message; use protocol::{ error::method::MethodError, - method::get::GetResponse, + method::get::{GetRequest, GetResponse}, object::{email::GetArguments, Object}, - types::{blob::BlobId, collection::Collection, id::Id, property::Property, value::Value}, + types::{blob::BlobId, collection::Collection, property::Property, value::Value}, }; use store::ValueKey; @@ -14,12 +14,9 @@ use super::body::{ToBodyPart, TruncateBody}; impl JMAP { pub async fn email_get( &self, - account_id: u32, - ids: Vec, - properties: Option>, - arguments: GetArguments, + request: GetRequest, ) -> Result { - let properties = properties.unwrap_or_else(|| { + let properties = request.properties.map(|v| v.unwrap()).unwrap_or_else(|| { vec![ Property::Id, Property::BlobId, @@ -47,7 +44,7 @@ impl JMAP { Property::Attachments, ] }); - let body_properties = arguments.body_properties.unwrap_or_else(|| { + let body_properties = request.arguments.body_properties.unwrap_or_else(|| { vec![ Property::PartId, Property::BlobId, @@ -61,13 +58,20 @@ impl JMAP { Property::Location, ] }); - let fetch_text_body_values = arguments.fetch_text_body_values.unwrap_or(false); - let fetch_html_body_values = arguments.fetch_html_body_values.unwrap_or(false); - let fetch_all_body_values = arguments.fetch_all_body_values.unwrap_or(false); - let max_body_value_bytes = arguments.max_body_value_bytes.unwrap_or(0); + let fetch_text_body_values = request.arguments.fetch_text_body_values.unwrap_or(false); + let fetch_html_body_values = request.arguments.fetch_html_body_values.unwrap_or(false); + let fetch_all_body_values = request.arguments.fetch_all_body_values.unwrap_or(false); + let max_body_value_bytes = request.arguments.max_body_value_bytes.unwrap_or(0); + let ids = if let Some(ids) = request.ids.map(|v| v.unwrap()) { + ids + } else { + let implement = ""; + todo!() + }; + let account_id = request.account_id.document_id(); let mut response = GetResponse { - account_id: Some(account_id.into()), + account_id: Some(request.account_id), state: self .store .get_last_change_id(account_id, Collection::Email) diff --git a/crates/core/src/email/import.rs b/crates/core/src/email/import.rs index 86154942..e1a51904 100644 --- a/crates/core/src/email/import.rs +++ b/crates/core/src/email/import.rs @@ -10,6 +10,8 @@ impl JMAP { &self, request: ImportEmailRequest, ) -> Result { + for (id, email) in request.emails {} + todo!() } } diff --git a/crates/core/src/email/query.rs b/crates/core/src/email/query.rs index 72f2304a..9524006a 100644 --- a/crates/core/src/email/query.rs +++ b/crates/core/src/email/query.rs @@ -16,8 +16,7 @@ use crate::JMAP; impl JMAP { pub async fn email_query( &self, - request: QueryRequest, - arguments: QueryArguments, + request: QueryRequest, ) -> Result { let account_id = request.account_id.document_id(); let mut filters = Vec::with_capacity(request.filter.len()); @@ -264,7 +263,7 @@ impl JMAP { request.anchor.map(|a| a.document_id()), request.anchor_offset.unwrap_or(0), ValueKey::new(account_id, Collection::Email, 0, Property::ThreadId).into(), - arguments.collapse_threads.unwrap_or(false), + request.arguments.collapse_threads.unwrap_or(false), ), ) .await?; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 571ad123..dcc27c53 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,5 +1,6 @@ use store::{fts::Language, Store}; +pub mod api; pub mod email; pub struct JMAP { @@ -10,6 +11,8 @@ pub struct JMAP { pub struct Config { pub default_language: Language, pub query_max_results: usize, + pub request_max_size: usize, + pub request_max_calls: usize, } pub enum MaybeError { diff --git a/crates/protocol/src/method/get.rs b/crates/protocol/src/method/get.rs index dc1e5def..ed3c4b41 100644 --- a/crates/protocol/src/method/get.rs +++ b/crates/protocol/src/method/get.rs @@ -11,11 +11,11 @@ use crate::{ }; #[derive(Debug, Clone)] -pub struct GetRequest { +pub struct GetRequest { pub account_id: Id, pub ids: Option, ResultReference>>, pub properties: Option, ResultReference>>, - pub arguments: RequestArguments, + pub arguments: T, } #[derive(Debug, Clone)] @@ -45,7 +45,7 @@ pub struct GetResponse { pub not_found: Vec, } -impl JsonObjectParser for GetRequest { +impl JsonObjectParser for GetRequest { fn parse(parser: &mut Parser<'_>) -> crate::parser::Result where Self: Sized, @@ -124,3 +124,18 @@ impl RequestPropertyParser for RequestArguments { } } } + +impl GetRequest { + pub fn take_arguments(&mut self) -> RequestArguments { + std::mem::replace(&mut self.arguments, RequestArguments::Principal) + } + + pub fn with_arguments(self, arguments: T) -> GetRequest { + GetRequest { + arguments, + account_id: self.account_id, + ids: self.ids, + properties: self.properties, + } + } +} diff --git a/crates/protocol/src/method/query.rs b/crates/protocol/src/method/query.rs index 9003fb20..8f1c0b59 100644 --- a/crates/protocol/src/method/query.rs +++ b/crates/protocol/src/method/query.rs @@ -9,7 +9,7 @@ use crate::{ }; #[derive(Debug, Clone)] -pub struct QueryRequest { +pub struct QueryRequest { pub account_id: Id, pub filter: Vec, pub sort: Option>, @@ -18,7 +18,7 @@ pub struct QueryRequest { pub anchor_offset: Option, pub limit: Option, pub calculate_total: Option, - pub arguments: RequestArguments, + pub arguments: T, } #[derive(Debug, Clone, serde::Serialize)] @@ -138,7 +138,7 @@ pub enum RequestArguments { Principal, } -impl JsonObjectParser for QueryRequest { +impl JsonObjectParser for QueryRequest { fn parse(parser: &mut Parser<'_>) -> crate::parser::Result where Self: Sized, @@ -666,3 +666,23 @@ impl Comparator { } } } + +impl QueryRequest { + pub fn take_arguments(&mut self) -> RequestArguments { + std::mem::replace(&mut self.arguments, RequestArguments::Principal) + } + + pub fn with_arguments(self, arguments: T) -> QueryRequest { + QueryRequest { + arguments, + account_id: self.account_id, + filter: self.filter, + sort: self.sort, + position: self.position, + anchor: self.anchor, + anchor_offset: self.anchor_offset, + limit: self.limit, + calculate_total: self.calculate_total, + } + } +} diff --git a/crates/protocol/src/method/set.rs b/crates/protocol/src/method/set.rs index ae82e891..40c25a8a 100644 --- a/crates/protocol/src/method/set.rs +++ b/crates/protocol/src/method/set.rs @@ -246,23 +246,13 @@ impl JsonObjectParser for Object { Property::ParentId | Property::EmailId | Property::IdentityId => parser .next_token::>()? .unwrap_string_or_null("")? - .map(|id| match id { - MaybeReference::Value(id) => SetValue::Value(Value::Id(id)), - MaybeReference::Reference(r) => SetValue::Value(Value::Text(r)), - }) + .map(SetValue::IdReference) .unwrap_or(SetValue::Value(Value::Null)), Property::MailboxIds => { if property.patch.is_empty() { - SetValue::Value(Value::List( - >>::parse(parser)? - .values - .into_iter() - .map(|id| match id { - MaybeReference::Value(id) => Value::Id(id), - MaybeReference::Reference(r) => Value::Text(r), - }) - .collect(), - )) + SetValue::IdReferences( + >>::parse(parser)?.values, + ) } else { property.patch.push(Value::Bool(bool::parse(parser)?)); SetValue::Patch(property.patch) diff --git a/crates/protocol/src/parser/impls.rs b/crates/protocol/src/parser/impls.rs index 8cb113d6..86558669 100644 --- a/crates/protocol/src/parser/impls.rs +++ b/crates/protocol/src/parser/impls.rs @@ -235,7 +235,6 @@ impl JsonObjectParser Token::DictStart => { let mut map = VecMap::new(); - parser.next_token::()?.assert(Token::DictStart)?; while { map.append(parser.next_dict_key()?, V::parse(parser)?); !parser.is_dict_end()? diff --git a/crates/protocol/src/parser/json.rs b/crates/protocol/src/parser/json.rs index cd9ffa67..ead5800a 100644 --- a/crates/protocol/src/parser/json.rs +++ b/crates/protocol/src/parser/json.rs @@ -35,6 +35,7 @@ impl<'x> Parser<'x> { } pub fn error(&self, message: &str) -> Error { + println!("{}", std::str::from_utf8(&self.bytes[self.pos..]).unwrap()); format!("{message} at position {}.", self.pos).into() } diff --git a/crates/protocol/src/request/mod.rs b/crates/protocol/src/request/mod.rs index 8842b6da..3fab6d7e 100644 --- a/crates/protocol/src/request/mod.rs +++ b/crates/protocol/src/request/mod.rs @@ -14,10 +14,10 @@ use crate::{ method::{ changes::ChangesRequest, copy::{CopyBlobRequest, CopyRequest}, - get::GetRequest, + get::{self, GetRequest}, import::ImportEmailRequest, parse::ParseEmailRequest, - query::QueryRequest, + query::{self, QueryRequest}, query_changes::QueryChangesRequest, search_snippet::GetSearchSnippetRequest, set::SetRequest, @@ -50,7 +50,7 @@ pub struct RequestProperty { #[derive(Debug)] pub enum RequestMethod { - Get(GetRequest), + Get(GetRequest), Set(SetRequest), Changes(ChangesRequest), Copy(CopyRequest), @@ -58,7 +58,7 @@ pub enum RequestMethod { ImportEmail(ImportEmailRequest), ParseEmail(ParseEmailRequest), QueryChanges(QueryChangesRequest), - Query(QueryRequest), + Query(QueryRequest), SearchSnippet(GetSearchSnippetRequest), ValidateScript(ValidateSieveScriptRequest), Echo(Echo), @@ -88,6 +88,7 @@ impl JsonObjectParser for RequestProperty { continue 'outer; } } + break; } Ok(RequestProperty { hash, is_ref }) diff --git a/crates/protocol/src/request/reference.rs b/crates/protocol/src/request/reference.rs index 1010fcb6..95e16c40 100644 --- a/crates/protocol/src/request/reference.rs +++ b/crates/protocol/src/request/reference.rs @@ -22,6 +22,15 @@ pub enum MaybeReference { Reference(R), } +impl MaybeReference { + pub fn unwrap(self) -> V { + match self { + MaybeReference::Value(v) => v, + MaybeReference::Reference(_) => panic!("unwrap() called on MaybeReference::Reference"), + } + } +} + impl JsonObjectParser for ResultReference { fn parse(parser: &mut Parser) -> crate::parser::Result where diff --git a/crates/protocol/src/response/mod.rs b/crates/protocol/src/response/mod.rs index bd59a65c..3a7ffc93 100644 --- a/crates/protocol/src/response/mod.rs +++ b/crates/protocol/src/response/mod.rs @@ -1,12 +1,12 @@ pub mod references; -use ahash::AHashMap; +use std::collections::HashMap; + use serde::Serialize; use crate::{ error::method::MethodError, method::{ - ahash_is_empty, changes::ChangesResponse, copy::{CopyBlobResponse, CopyResponse}, get::GetResponse, @@ -49,12 +49,11 @@ pub struct Response { pub session_state: u32, #[serde(rename(deserialize = "createdIds"))] - #[serde(skip_serializing_if = "ahash_is_empty")] - pub created_ids: AHashMap, + pub created_ids: HashMap, } impl Response { - pub fn new(session_state: u32, created_ids: AHashMap, capacity: usize) -> Self { + pub fn new(session_state: u32, created_ids: HashMap, capacity: usize) -> Self { Response { session_state, created_ids, @@ -158,3 +157,12 @@ impl From for ResponseMethod { ResponseMethod::ValidateScript(validate_script) } } + +impl> From> for ResponseMethod { + fn from(result: Result) -> Self { + match result { + Ok(value) => value.into(), + Err(error) => error.into(), + } + } +} diff --git a/crates/protocol/src/response/references.rs b/crates/protocol/src/response/references.rs index 21c94a0b..220f5257 100644 --- a/crates/protocol/src/response/references.rs +++ b/crates/protocol/src/response/references.rs @@ -1,11 +1,20 @@ +use std::collections::HashMap; + +use utils::map::vec_map::VecMap; + use crate::{ error::method::MethodError, + object::Object, request::{ method::MethodFunction, reference::{MaybeReference, ResultReference}, - Request, RequestMethod, + RequestMethod, + }, + types::{ + id::Id, + property::Property, + value::{SetValue, Value}, }, - types::{id::Id, pointer::JSONPointer, property::Property, value::Value}, }; use super::{Response, ResponseMethod}; @@ -37,6 +46,69 @@ impl Response { } } RequestMethod::Set(request) => { + // Resolve create references + if let Some(create) = &mut request.create { + let mut graph = HashMap::with_capacity(create.len()); + for (id, obj) in create.iter_mut() { + self.eval_object_references(obj, Some((&*id, &mut graph)))?; + } + + // Perform topological sort + if !graph.is_empty() { + let mut sorted_create = VecMap::with_capacity(create.len()); + let mut it_stack = Vec::new(); + let keys = graph.keys().cloned().collect::>(); + let mut it = keys.iter(); + + 'main: loop { + while let Some(from_id) = it.next() { + if let Some(to_ids) = graph.get(from_id) { + it_stack.push((it, from_id)); + if it_stack.len() > 1000 { + return Err(MethodError::InvalidArguments( + "Cyclical references are not allowed.".to_string(), + )); + } + it = to_ids.iter(); + continue; + } else if let Some((id, value)) = create.remove_entry(from_id) { + sorted_create.append(id, value); + if create.is_empty() { + break 'main; + } + } + } + + if let Some((prev_it, from_id)) = it_stack.pop() { + it = prev_it; + if let Some((id, value)) = create.remove_entry(from_id) { + sorted_create.append(id, value); + if create.is_empty() { + break 'main; + } + } + } else { + break; + } + } + + // Add remaining items + if !create.is_empty() { + for (id, value) in std::mem::take(create) { + sorted_create.append(id, value); + } + } + request.create = sorted_create.into(); + } + } + + // Resolve update references + if let Some(update) = &mut request.update { + for obj in update.values_mut() { + self.eval_object_references(obj, None)?; + } + } + // Resolve destroy references if let Some(MaybeReference::Reference(reference)) = &request.destroy { request.destroy = Some(MaybeReference::Value( @@ -45,13 +117,39 @@ impl Response { )); } } - RequestMethod::Changes(_) => todo!(), - RequestMethod::Copy(_) => todo!(), - RequestMethod::CopyBlob(_) => todo!(), - RequestMethod::ImportEmail(_) => todo!(), - RequestMethod::ParseEmail(_) => todo!(), - RequestMethod::QueryChanges(_) => todo!(), - RequestMethod::Query(_) => todo!(), + RequestMethod::Copy(request) => { + // Resolve create references + for (id, obj) in request.create.iter_mut() { + self.eval_object_references(obj, None)?; + if let MaybeReference::Reference(ir) = id { + *id = MaybeReference::Value(self.eval_id_reference(ir)?); + } + } + } + RequestMethod::ImportEmail(request) => { + // Resolve email mailbox references + for email in request.emails.values_mut() { + match &mut email.mailbox_ids { + Some(MaybeReference::Reference(rr)) => { + email.mailbox_ids = Some(MaybeReference::Value( + self.eval_result_references(rr) + .unwrap_ids(rr)? + .into_iter() + .map(MaybeReference::Value) + .collect(), + )); + } + Some(MaybeReference::Value(values)) => { + for value in values { + if let MaybeReference::Reference(ir) = value { + *value = MaybeReference::Value(self.eval_id_reference(ir)?); + } + } + } + _ => (), + } + } + } RequestMethod::SearchSnippet(request) => { // Resolve emailIds references if let MaybeReference::Reference(reference) = &request.email_ids { @@ -61,9 +159,7 @@ impl Response { ); } } - RequestMethod::ValidateScript(_) => todo!(), - RequestMethod::Echo(_) => todo!(), - RequestMethod::Error(_) => todo!(), + _ => {} } Ok(()) @@ -138,6 +234,66 @@ impl Response { EvalResult::Failed } + + fn eval_id_reference(&self, ir: &str) -> Result { + if let Some(id) = self.created_ids.get(ir) { + Ok(*id) + } else { + Err(MethodError::InvalidResultReference(format!( + "Id reference {ir:?} not found." + ))) + } + } + + fn eval_object_references( + &self, + obj: &mut Object, + mut graph: Option<(&str, &mut HashMap>)>, + ) -> Result<(), MethodError> { + for set_value in obj.properties.values_mut() { + match set_value { + SetValue::IdReference(MaybeReference::Reference(parent_id)) => { + if let Some(id) = self.created_ids.get(parent_id) { + *set_value = SetValue::Value(Value::Id(*id)); + } else if let Some((child_id, graph)) = &mut graph { + graph + .entry(child_id.to_string()) + .or_insert_with(Vec::new) + .push(parent_id.to_string()); + } else { + return Err(MethodError::InvalidResultReference(format!( + "Id reference {parent_id:?} not found." + ))); + } + } + SetValue::IdReferences(id_refs) => { + for id_ref in id_refs { + if let MaybeReference::Reference(parent_id) = id_ref { + if let Some(id) = self.created_ids.get(parent_id) { + *id_ref = MaybeReference::Value(*id); + } else if let Some((child_id, graph)) = &mut graph { + graph + .entry(child_id.to_string()) + .or_insert_with(Vec::new) + .push(parent_id.to_string()); + } else { + return Err(MethodError::InvalidResultReference(format!( + "Id reference {parent_id:?} not found." + ))); + } + } + } + } + SetValue::ResultReference(rr) => { + *set_value = + SetValue::Value(self.eval_result_references(rr).unwrap_ids(rr)?.into()); + } + _ => (), + } + } + + Ok(()) + } } impl EvalResult { @@ -183,3 +339,358 @@ impl EvalResult { } } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::{ + error::method::MethodError, + request::{Request, RequestMethod}, + response::Response, + types::{ + id::Id, + property::Property, + value::{SetValue, Value}, + }, + }; + + #[test] + fn eval_references() { + let request = Request::parse( + br##"{ + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "Folder a", + "parentId": "#b" + }, + "b": { + "name": "Folder b", + "parentId": "#c" + }, + "c": { + "name": "Folder c", + "parentId": "#d" + }, + "d": { + "name": "Folder d", + "parentId": "#e" + }, + "e": { + "name": "Folder e", + "parentId": "#f" + }, + "f": { + "name": "Folder f", + "parentId": "#g" + }, + "g": { + "name": "Folder g", + "parentId": null + } + } + }, + "fulltree" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a1": { + "name": "Folder a1", + "parentId": null + }, + "b2": { + "name": "Folder b2", + "parentId": "#a1" + }, + "c3": { + "name": "Folder c3", + "parentId": "#a1" + }, + "d4": { + "name": "Folder d4", + "parentId": "#b2" + }, + "e5": { + "name": "Folder e5", + "parentId": "#b2" + }, + "f6": { + "name": "Folder f6", + "parentId": "#d4" + }, + "g7": { + "name": "Folder g7", + "parentId": "#e5" + } + } + }, + "fulltree2" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "z": { + "name": "Folder Z", + "parentId": "#x" + }, + "y": { + "name": null + }, + "x": { + "name": "Folder X" + } + } + }, + "xyz" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "Folder a", + "parentId": "#b" + }, + "b": { + "name": "Folder b", + "parentId": "#c" + }, + "c": { + "name": "Folder c", + "parentId": "#d" + }, + "d": { + "name": "Folder d", + "parentId": "#a" + } + } + }, + "circular" + ] + ] + }"##, + 100, + 1024 * 1024, + ) + .unwrap(); + + let response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + for (test_num, mut call) in request.method_calls.into_iter().enumerate() { + match response.resolve_references(&mut call.method) { + Ok(_) => assert!( + (0..3).contains(&test_num), + "Unexpected invocation {}", + test_num + ), + Err(err) => { + assert_eq!(test_num, 3); + assert!(matches!(err, MethodError::InvalidArguments(_))); + continue; + } + } + + if let RequestMethod::Set(request) = call.method { + if test_num == 0 { + assert_eq!( + request + .create + .unwrap() + .into_iter() + .map(|b| b.0) + .collect::>(), + ["g", "f", "e", "d", "c", "b", "a"] + .iter() + .map(|i| i.to_string()) + .collect::>() + ); + } else if test_num == 1 { + let mut pending_ids = vec!["a1", "b2", "d4", "e5", "f6", "c3", "g7"]; + + for (id, _) in request.create.as_ref().unwrap() { + match id.as_str() { + "a1" => (), + "b2" | "c3" => assert!(!pending_ids.contains(&"a1")), + "d4" | "e5" => assert!(!pending_ids.contains(&"b2")), + "f6" => assert!(!pending_ids.contains(&"d4")), + "g7" => assert!(!pending_ids.contains(&"e5")), + _ => panic!("Unexpected ID"), + } + pending_ids.retain(|i| i != id); + } + + if !pending_ids.is_empty() { + panic!( + "Unexpected order: {:?}", + request + .create + .as_ref() + .unwrap() + .iter() + .map(|b| b.0.to_string()) + .collect::>() + ); + } + } else if test_num == 2 { + assert_eq!( + request + .create + .unwrap() + .into_iter() + .map(|b| b.0) + .collect::>(), + ["x", "z", "y"] + .iter() + .map(|i| i.to_string()) + .collect::>() + ); + } + } else { + panic!("Expected Set Mailbox Request"); + } + } + + let request = Request::parse( + br##"{ + "using": [ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls": [ + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a": { + "name": "a", + "parentId": "#x" + }, + "b": { + "name": "b", + "parentId": "#y" + }, + "c": { + "name": "c", + "parentId": "#z" + } + } + }, + "ref1" + ], + [ + "Mailbox/set", + { + "accountId": "b", + "create": { + "a1": { + "name": "a1", + "parentId": "#a" + }, + "b2": { + "name": "b2", + "parentId": "#b" + }, + "c3": { + "name": "c3", + "parentId": "#c" + } + } + }, + "red2" + ] + ], + "createdIds": { + "x": "b", + "y": "c", + "z": "d" + } + }"##, + 1024, + 1024 * 1024, + ) + .unwrap(); + + let mut response = Response::new( + 1234, + request.created_ids.unwrap_or_default(), + request.method_calls.len(), + ); + + let mut invocations = request.method_calls.into_iter(); + let mut call = invocations.next().unwrap(); + response.resolve_references(&mut call.method).unwrap(); + + if let RequestMethod::Set(request) = call.method { + let create = request + .create + .unwrap() + .into_iter() + .map(|(p, mut v)| (p, v.properties.remove(&Property::ParentId).unwrap())) + .collect::>(); + assert_eq!( + create.get("a").unwrap(), + &SetValue::Value(Value::Id(Id::new(1))) + ); + assert_eq!( + create.get("b").unwrap(), + &SetValue::Value(Value::Id(Id::new(2))) + ); + assert_eq!( + create.get("c").unwrap(), + &SetValue::Value(Value::Id(Id::new(3))) + ); + } else { + panic!("Expected Mailbox Set Request"); + } + + response.created_ids.insert("a".to_string(), Id::new(5)); + response.created_ids.insert("b".to_string(), Id::new(6)); + response.created_ids.insert("c".to_string(), Id::new(7)); + + let mut call = invocations.next().unwrap(); + response.resolve_references(&mut call.method).unwrap(); + + if let RequestMethod::Set(request) = call.method { + let create = request + .create + .unwrap() + .into_iter() + .map(|(p, mut v)| (p, v.properties.remove(&Property::ParentId).unwrap())) + .collect::>(); + assert_eq!( + create.get("a1").unwrap(), + &SetValue::Value(Value::Id(Id::new(5))) + ); + assert_eq!( + create.get("b2").unwrap(), + &SetValue::Value(Value::Id(Id::new(6))) + ); + assert_eq!( + create.get("c3").unwrap(), + &SetValue::Value(Value::Id(Id::new(7))) + ); + } else { + panic!("Expected Mailbox Set Request"); + } + } +} diff --git a/crates/protocol/src/types/value.rs b/crates/protocol/src/types/value.rs index e272f64c..4c185f5d 100644 --- a/crates/protocol/src/types/value.rs +++ b/crates/protocol/src/types/value.rs @@ -8,7 +8,7 @@ use crate::{ error::method::MethodError, object::Object, parser::{json::Parser, Ignore, JsonObjectParser, Token}, - request::reference::ResultReference, + request::reference::{MaybeReference, ResultReference}, }; use super::{ @@ -38,10 +38,12 @@ pub enum Value { Null, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SetValue { Value(Value), Patch(Vec), + IdReference(MaybeReference), + IdReferences(Vec>), ResultReference(ResultReference), } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 70f3b3bc..6ccbb868 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -store = { path = "../crates/store" } +store = { path = "../crates/store", features = ["test_mode"] } utils = { path = "/home/vagrant/code/utils" } [dev-dependencies] diff --git a/tests/src/store/blobs.rs b/tests/src/store/blobs.rs index 0f5c216d..1409e4a3 100644 --- a/tests/src/store/blobs.rs +++ b/tests/src/store/blobs.rs @@ -4,7 +4,7 @@ use store::ahash::AHashMap; use store::{ write::{BatchBuilder, F_CLEAR}, - BlobId, BlobKey, Store, BLOB_HASH_LEN, + BlobHash, BlobKey, Store, BLOB_HASH_LEN, }; pub async fn test(db: Arc) { @@ -13,8 +13,8 @@ pub async fn test(db: Arc) { let blob_1 = vec![b'a'; 1024]; let blob_2 = vec![b'b'; 1024]; - let blob_id_1 = BlobId::from(&blob_1[..]); - let blob_id_2 = BlobId::from(&blob_2[..]); + let blob_id_1 = BlobHash::from(&blob_1[..]); + let blob_id_2 = BlobHash::from(&blob_2[..]); // Insert the same blobs concurrently let handles = (1..=100) @@ -91,13 +91,13 @@ pub async fn test(db: Arc) { } struct BlobPurge { - result: AHashMap, + result: AHashMap, link_count: u32, ephemeral_count: u32, id: [u8; BLOB_HASH_LEN], } -async fn get_all_blobs(store: &Store) -> AHashMap { +async fn get_all_blobs(store: &Store) -> AHashMap { let results = BlobPurge { result: AHashMap::new(), id: [0u8; BLOB_HASH_LEN], @@ -122,7 +122,7 @@ async fn get_all_blobs(store: &Store) -> AHashMap { .iterate(results, from_key, to_key, false, true, move |b, k, v| { if !k.starts_with(&b.id) { if b.link_count != u32::MAX { - let id = BlobId { hash: b.id }; + let id = BlobHash { hash: b.id }; b.result.insert(id, (b.link_count, b.ephemeral_count)); } b.link_count = 0; @@ -142,7 +142,7 @@ async fn get_all_blobs(store: &Store) -> AHashMap { .unwrap(); if b.link_count != u32::MAX { - let id = BlobId { hash: b.id }; + let id = BlobHash { hash: b.id }; b.result.insert(id, (b.link_count, b.ephemeral_count)); } diff --git a/tests/src/store/query.rs b/tests/src/store/query.rs index e6840927..c1e864a1 100644 --- a/tests/src/store/query.rs +++ b/tests/src/store/query.rs @@ -26,12 +26,12 @@ use std::{ time::Instant, }; -use store::ahash::AHashMap; +use store::{ahash::AHashMap, query::sort::Pagination}; use store::{ fts::{builder::FtsIndexBuilder, Language}, query::{Comparator, Filter}, - write::{BatchBuilder, F_INDEX, F_TOKENIZE, F_VALUE}, + write::{BatchBuilder, F_BITMAP, F_INDEX, F_VALUE}, Store, ValueKey, }; @@ -127,7 +127,7 @@ pub async fn test(db: Arc, do_insert: bool) { builder.value( field_id, field.to_lowercase(), - F_VALUE | F_TOKENIZE, + F_VALUE | F_BITMAP, ); } } @@ -155,7 +155,7 @@ pub async fn test(db: Arc, do_insert: bool) { builder.value( field_id, field.to_lowercase(), - F_VALUE | F_INDEX | F_TOKENIZE, + F_VALUE | F_INDEX | F_BITMAP, ); } } @@ -221,14 +221,14 @@ pub async fn test_filter(db: Arc) { let tests = [ ( vec![ - Filter::match_english(fields["title"], "water"), + Filter::has_english_text(fields["title"], "water"), Filter::eq(fields["year"], 1979u32), ], vec!["p11293"], ), ( vec![ - Filter::match_english(fields["medium"], "gelatin"), + Filter::has_english_text(fields["medium"], "gelatin"), Filter::gt(fields["year"], 2000u32), Filter::lt(fields["width"], 180u32), Filter::gt(fields["width"], 0u32), @@ -236,19 +236,19 @@ pub async fn test_filter(db: Arc) { vec!["p79426", "p79427", "p79428", "p79429", "p79430"], ), ( - vec![Filter::match_english(fields["title"], "'rustic bridge'")], + vec![Filter::has_english_text(fields["title"], "'rustic bridge'")], vec!["d05503"], ), ( vec![ - Filter::match_english(fields["title"], "'rustic'"), - Filter::match_english(fields["title"], "study"), + Filter::has_english_text(fields["title"], "'rustic'"), + Filter::has_english_text(fields["title"], "study"), ], vec!["d00399", "d05352"], ), ( vec![ - Filter::has_keywords(fields["artist"], "mauro kunst"), + Filter::has_text(fields["artist"], "mauro kunst", Language::None), Filter::has_keyword(fields["artistRole"], "artist"), Filter::Or, Filter::eq(fields["year"], 1969u32), @@ -260,9 +260,9 @@ pub async fn test_filter(db: Arc) { ( vec![ Filter::Not, - Filter::match_english(fields["medium"], "oil"), + Filter::has_english_text(fields["medium"], "oil"), Filter::End, - Filter::match_english(fields["creditLine"], "bequeath"), + Filter::has_english_text(fields["creditLine"], "bequeath"), Filter::Or, Filter::And, Filter::ge(fields["year"], 1900u32), @@ -285,7 +285,7 @@ pub async fn test_filter(db: Arc) { Filter::And, Filter::has_keyword(fields["artist"], "warhol"), Filter::Not, - Filter::match_english(fields["title"], "'campbell'"), + Filter::has_english_text(fields["title"], "'campbell'"), Filter::End, Filter::Not, Filter::Or, @@ -303,12 +303,12 @@ pub async fn test_filter(db: Arc) { ), ( vec![ - Filter::match_english(fields["title"], "study"), - Filter::match_english(fields["medium"], "paper"), - Filter::match_english(fields["creditLine"], "'purchased'"), + Filter::has_english_text(fields["title"], "study"), + Filter::has_english_text(fields["medium"], "paper"), + Filter::has_english_text(fields["creditLine"], "'purchased'"), Filter::Not, - Filter::match_english(fields["title"], "'anatomical'"), - Filter::match_english(fields["title"], "'for'"), + Filter::has_english_text(fields["title"], "'anatomical'"), + Filter::has_english_text(fields["title"], "'for'"), Filter::End, Filter::gt(fields["year"], 1900u32), Filter::gt(fields["acquisitionYear"], 2000u32), @@ -326,10 +326,7 @@ pub async fn test_filter(db: Arc) { .sort( docset, vec![Comparator::ascending(fields["accession_number"])], - 0, - 0, - None, - 0, + Pagination::new(0, 0, None, 0, None, false), ) .await .unwrap(); @@ -342,7 +339,7 @@ pub async fn test_filter(db: Arc) { .map(|document_id| ValueKey { account_id: 0, collection: COLLECTION_ID, - document_id, + document_id: document_id as u32, family: 0, field: fields["accession_number"], }) @@ -420,8 +417,13 @@ pub async fn test_sort(db: Arc) { for (filter, sort, expected_results) in tests { //println!("Running test: {:?}", sort); let docset = db.filter(0, COLLECTION_ID, filter).await.unwrap(); + let sorted_docset = db - .sort(docset, sort, expected_results.len(), 0, None, 0) + .sort( + docset, + sort, + Pagination::new(expected_results.len(), 0, None, 0, None, false), + ) .await .unwrap(); @@ -433,7 +435,7 @@ pub async fn test_sort(db: Arc) { .map(|document_id| ValueKey { account_id: 0, collection: COLLECTION_ID, - document_id, + document_id: document_id as u32, family: 0, field: fields["accession_number"], })