result references

This commit is contained in:
Mauro D 2023-04-11 14:45:07 +00:00
parent 633db406f7
commit 3250cbc443
20 changed files with 744 additions and 107 deletions

View file

@ -1,18 +1,18 @@
[package]
name = "stalwart-jmap"
description = "Stalwart JMAP Server"
authors = [ "Stalwart Labs Ltd. <hello@stalw.art>"]
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. <hello@stalw.art>"]
#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 = [

View file

@ -0,0 +1 @@
pub mod request;

View file

@ -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<Response, RequestError> {
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)
}
}

View file

@ -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<Id>,
properties: Option<Vec<Property>>,
arguments: GetArguments,
request: GetRequest<GetArguments>,
) -> Result<GetResponse, MethodError> {
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)

View file

@ -10,6 +10,8 @@ impl JMAP {
&self,
request: ImportEmailRequest,
) -> Result<ImportEmailResponse, MethodError> {
for (id, email) in request.emails {}
todo!()
}
}

View file

@ -16,8 +16,7 @@ use crate::JMAP;
impl JMAP {
pub async fn email_query(
&self,
request: QueryRequest,
arguments: QueryArguments,
request: QueryRequest<QueryArguments>,
) -> Result<QueryResponse, MethodError> {
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?;

View file

@ -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 {

View file

@ -11,11 +11,11 @@ use crate::{
};
#[derive(Debug, Clone)]
pub struct GetRequest {
pub struct GetRequest<T> {
pub account_id: Id,
pub ids: Option<MaybeReference<Vec<Id>, ResultReference>>,
pub properties: Option<MaybeReference<Vec<Property>, ResultReference>>,
pub arguments: RequestArguments,
pub arguments: T,
}
#[derive(Debug, Clone)]
@ -45,7 +45,7 @@ pub struct GetResponse {
pub not_found: Vec<Id>,
}
impl JsonObjectParser for GetRequest {
impl JsonObjectParser for GetRequest<RequestArguments> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
where
Self: Sized,
@ -124,3 +124,18 @@ impl RequestPropertyParser for RequestArguments {
}
}
}
impl GetRequest<RequestArguments> {
pub fn take_arguments(&mut self) -> RequestArguments {
std::mem::replace(&mut self.arguments, RequestArguments::Principal)
}
pub fn with_arguments<T>(self, arguments: T) -> GetRequest<T> {
GetRequest {
arguments,
account_id: self.account_id,
ids: self.ids,
properties: self.properties,
}
}
}

View file

@ -9,7 +9,7 @@ use crate::{
};
#[derive(Debug, Clone)]
pub struct QueryRequest {
pub struct QueryRequest<T> {
pub account_id: Id,
pub filter: Vec<Filter>,
pub sort: Option<Vec<Comparator>>,
@ -18,7 +18,7 @@ pub struct QueryRequest {
pub anchor_offset: Option<i32>,
pub limit: Option<usize>,
pub calculate_total: Option<bool>,
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<RequestArguments> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
where
Self: Sized,
@ -666,3 +666,23 @@ impl Comparator {
}
}
}
impl QueryRequest<RequestArguments> {
pub fn take_arguments(&mut self) -> RequestArguments {
std::mem::replace(&mut self.arguments, RequestArguments::Principal)
}
pub fn with_arguments<T>(self, arguments: T) -> QueryRequest<T> {
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,
}
}
}

View file

@ -246,23 +246,13 @@ impl JsonObjectParser for Object<SetValue> {
Property::ParentId | Property::EmailId | Property::IdentityId => parser
.next_token::<MaybeReference<Id, String>>()?
.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(
<SetValueMap<MaybeReference<Id, String>>>::parse(parser)?
.values
.into_iter()
.map(|id| match id {
MaybeReference::Value(id) => Value::Id(id),
MaybeReference::Reference(r) => Value::Text(r),
})
.collect(),
))
SetValue::IdReferences(
<SetValueMap<MaybeReference<Id, String>>>::parse(parser)?.values,
)
} else {
property.patch.push(Value::Bool(bool::parse(parser)?));
SetValue::Patch(property.patch)

View file

@ -235,7 +235,6 @@ impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser
Token::DictStart => {
let mut map = VecMap::new();
parser.next_token::<Ignore>()?.assert(Token::DictStart)?;
while {
map.append(parser.next_dict_key()?, V::parse(parser)?);
!parser.is_dict_end()?

View file

@ -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()
}

View file

@ -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<get::RequestArguments>),
Set(SetRequest),
Changes(ChangesRequest),
Copy(CopyRequest),
@ -58,7 +58,7 @@ pub enum RequestMethod {
ImportEmail(ImportEmailRequest),
ParseEmail(ParseEmailRequest),
QueryChanges(QueryChangesRequest),
Query(QueryRequest),
Query(QueryRequest<query::RequestArguments>),
SearchSnippet(GetSearchSnippetRequest),
ValidateScript(ValidateSieveScriptRequest),
Echo(Echo),
@ -88,6 +88,7 @@ impl JsonObjectParser for RequestProperty {
continue 'outer;
}
}
break;
}
Ok(RequestProperty { hash, is_ref })

View file

@ -22,6 +22,15 @@ pub enum MaybeReference<V, R> {
Reference(R),
}
impl<V, R> MaybeReference<V, R> {
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<Self>
where

View file

@ -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<String, Id>,
pub created_ids: HashMap<String, Id>,
}
impl Response {
pub fn new(session_state: u32, created_ids: AHashMap<String, Id>, capacity: usize) -> Self {
pub fn new(session_state: u32, created_ids: HashMap<String, Id>, capacity: usize) -> Self {
Response {
session_state,
created_ids,
@ -158,3 +157,12 @@ impl From<ValidateSieveScriptResponse> for ResponseMethod {
ResponseMethod::ValidateScript(validate_script)
}
}
impl<T: Into<ResponseMethod>> From<Result<T, MethodError>> for ResponseMethod {
fn from(result: Result<T, MethodError>) -> Self {
match result {
Ok(value) => value.into(),
Err(error) => error.into(),
}
}
}

View file

@ -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::<Vec<_>>();
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<Id, MethodError> {
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<SetValue>,
mut graph: Option<(&str, &mut HashMap<String, Vec<String>>)>,
) -> 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::<Vec<_>>(),
["g", "f", "e", "d", "c", "b", "a"]
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
);
} 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::<Vec<_>>()
);
}
} else if test_num == 2 {
assert_eq!(
request
.create
.unwrap()
.into_iter()
.map(|b| b.0)
.collect::<Vec<_>>(),
["x", "z", "y"]
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
);
}
} 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::<HashMap<_, _>>();
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::<HashMap<_, _>>();
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");
}
}
}

View file

@ -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<Value>),
IdReference(MaybeReference<Id, String>),
IdReferences(Vec<MaybeReference<Id, String>>),
ResultReference(ResultReference),
}

View file

@ -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]

View file

@ -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<Store>) {
@ -13,8 +13,8 @@ pub async fn test(db: Arc<Store>) {
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<Store>) {
}
struct BlobPurge {
result: AHashMap<BlobId, (u32, u32)>,
result: AHashMap<BlobHash, (u32, u32)>,
link_count: u32,
ephemeral_count: u32,
id: [u8; BLOB_HASH_LEN],
}
async fn get_all_blobs(store: &Store) -> AHashMap<BlobId, (u32, u32)> {
async fn get_all_blobs(store: &Store) -> AHashMap<BlobHash, (u32, u32)> {
let results = BlobPurge {
result: AHashMap::new(),
id: [0u8; BLOB_HASH_LEN],
@ -122,7 +122,7 @@ async fn get_all_blobs(store: &Store) -> AHashMap<BlobId, (u32, u32)> {
.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<BlobId, (u32, u32)> {
.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));
}

View file

@ -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<Store>, 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<Store>, 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<Store>) {
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<Store>) {
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<Store>) {
(
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<Store>) {
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<Store>) {
),
(
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<Store>) {
.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<Store>) {
.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<Store>) {
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<Store>) {
.map(|document_id| ValueKey {
account_id: 0,
collection: COLLECTION_ID,
document_id,
document_id: document_id as u32,
family: 0,
field: fields["accession_number"],
})