From 12249d2c581cdedf2e6b946ce947e4931ca5736c Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 16 Sep 2023 23:26:08 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 504 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 +++++ LICENSE | 13 +++++++++++++ README.md | 17 +++++++++++++++++ rustfmt-unstable.toml | 7 +++++++ jmap-proto/.gitignore | 2 ++ jmap-proto/Cargo.toml | 12 ++++++++++++ jmap-proto/src/common.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/errors.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/lib.rs | 5 +++++ jmap-proto/src/util.rs | 11 +++++++++++ jmap-proto/src/endpoints/mod.rs | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/session.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/events/mod.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/events/state_change.rs | 19 +++++++++++++++++++ jmap-proto/src/endpoints/blob/copy.rs | 40 ++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/blob/download.rs | 27 +++++++++++++++++++++++++++ jmap-proto/src/endpoints/blob/mod.rs | 3 +++ jmap-proto/src/endpoints/blob/upload.rs | 30 ++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/core/echo.rs | 13 +++++++++++++ jmap-proto/src/endpoints/core/mod.rs | 1 + jmap-proto/src/endpoints/object/changes.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/object/copy.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/object/get.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/object/mod.rs | 22 ++++++++++++++++++++++ jmap-proto/src/endpoints/object/query.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/object/query_changes.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/endpoints/object/set.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 29 files changed, 2018 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d 100644 --- /dev/null +++ a/.gitignore @@ -1,0 +1,1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..553d60c 100644 --- /dev/null +++ a/Cargo.lock @@ -1,0 +1,504 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jmap-proto" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_json", + "serde_with", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bf04c28bee9043ed9ea1e41afc0552288d3aba9c6efdd78903b802926f4879" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +dependencies = [ + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7f1b3f 100644 --- /dev/null +++ a/Cargo.toml @@ -1,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "jmap-proto" +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ff9e935 100644 --- /dev/null +++ a/LICENSE @@ -1,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..28047ac 100644 --- /dev/null +++ a/README.md @@ -1,0 +1,17 @@ +# jogre + +JMAP Organization, Great, Real, Exciting (yes ok, the backronym is a WIP, +along with the whole project) + +--- + +An implementation of a JMAP server, aiming to be a modern alternative to +CalDAV/CardDAV servers. + +Aiming to implement: + +- [RFC 8620](https://tools.ietf.org/html/rfc8620) +- [JMAP Calendars](https://jmap.io/spec-calendars.html) +- [JMAP Contacts](https://jmap.io/spec-contacts.html) + +Will probably end up being a forever WIP, but we'll see! diff --git a/rustfmt-unstable.toml b/rustfmt-unstable.toml new file mode 100644 index 0000000..fd4d8a5 100644 --- /dev/null +++ a/rustfmt-unstable.toml @@ -1,0 +1,7 @@ +edition = "2021" +unstable_features = true +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +normalize_comments = true +comment_width = 100 +wrap_comments = true diff --git a/jmap-proto/.gitignore b/jmap-proto/.gitignore new file mode 100644 index 0000000..4fffb2f 100644 --- /dev/null +++ a/jmap-proto/.gitignore @@ -1,0 +1,2 @@ +/target +/Cargo.lock diff --git a/jmap-proto/Cargo.toml b/jmap-proto/Cargo.toml new file mode 100644 index 0000000..cdf8f8d 100644 --- /dev/null +++ a/jmap-proto/Cargo.toml @@ -1,0 +1,12 @@ +[package] +name = "jmap-proto" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_with = { version = "3.3", features = ["macros"] } diff --git a/jmap-proto/src/common.rs b/jmap-proto/src/common.rs new file mode 100644 index 0000000..8f58053 100644 --- /dev/null +++ a/jmap-proto/src/common.rs @@ -1,0 +1,67 @@ +use std::borrow::Cow; + +use chrono::{FixedOffset, Utc}; +use serde::{Deserialize, Serialize}; + +/// Where "Int" is given as a data type, it means an integer in the range +/// -2^53+1 <= value <= 2^53-1, the safe range for integers stored in a +/// floating-point double, represented as a JSON "Number". +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Hash, Default)] +pub struct Int(i64); + +/// Where "UnsignedInt" is given as a data type, it means an "Int" where +/// the value MUST be in the range 0 <= value <= 2^53-1. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct UnsignedInt(u64); + +/// All record ids are assigned by the server and are immutable. +/// +/// Where "Id" is given as a data type, it means a "String" of at least 1 +/// and a maximum of 255 octets in size, and it MUST only contain +/// characters from the "URL and Filename Safe" base64 alphabet, as +/// defined in Section 5 of [RFC4648], excluding the pad character ("="). +/// This means the allowed characters are the ASCII alphanumeric +/// characters ("A-Za-z0-9"), hyphen ("-"), and underscore ("_"). +/// +/// These characters are safe to use in almost any context (e.g., +/// filesystems, URIs, and IMAP atoms). For maximum safety, servers +/// SHOULD also follow defensive allocation strategies to avoid creating +/// risks where glob completion or data type detection may be present +/// (e.g., on filesystems or in spreadsheets). In particular, it is wise +/// to avoid: +/// +/// - Ids starting with a dash +/// - Ids starting with digits +/// - Ids that contain only digits +/// - Ids that differ only by ASCII case (for example, A vs. a) +/// - the specific sequence of three characters "NIL" (because this sequence can be confused with +/// the IMAP protocol expression of the null value) +/// +/// A good solution to these issues is to prefix every id with a single +/// alphabetical character. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct Id<'a>(#[serde(borrow)] Cow<'a, str>); + +/// Where "Date" is given as a type, it means a string in "date-time" +/// format [RFC3339]. To ensure a normalised form, the "time-secfrac" +/// MUST always be omitted if zero, and any letters in the string (e.g., +/// "T" and "Z") MUST be uppercase. For example, +/// "2014-10-30T14:12:00+08:00". +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Date(chrono::DateTime); + +/// Where "UTCDate" is given as a type, it means a "Date" where the +/// "time-offset" component MUST be "Z" (i.e., it must be in UTC time). +/// For example, "2014-10-30T06:12:00Z". +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UtcDate(chrono::DateTime); + +/// A (preferably short) string representing the state of this object +/// on the server. If the value of any other property on the Session +/// object changes, this string will change. The current value is +/// also returned on the API Response object (see Section 3.4), +/// allowing clients to quickly determine if the session information +/// has changed (e.g., an account has been added or removed), so they +/// need to refetch the object. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SessionState<'a>(#[serde(borrow)] Cow<'a, str>); diff --git a/jmap-proto/src/errors.rs b/jmap-proto/src/errors.rs new file mode 100644 index 0000000..97c1e69 100644 --- /dev/null +++ a/jmap-proto/src/errors.rs @@ -1,0 +1,89 @@ +use std::{borrow::Cow, collections::HashMap}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RequestError { + #[serde(rename = "type")] + type_: ProblemType, + status: u16, + detail: Cow<'static, str>, + #[serde(flatten)] + meta: HashMap, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum ProblemType { + /// The client included a capability in the "using" property of the + /// request that the server does not support. + #[serde(rename = "urn:ietf:params:jmap:error:unknownCapability")] + UnknownCapability, + /// The content type of the request was not "application/json" or the + /// request did not parse as I-JSON. + #[serde(rename = "urn:ietf:params:jmap:error:notJSON")] + NotJson, + /// The request parsed as JSON but did not match the type signature of + /// the Request object. + #[serde(rename = "urn:ietf:params:jmap:error:notRequest")] + NotRequest, + /// The request was not processed as it would have exceeded one of the + /// request limits defined on the capability object, such as + /// maxSizeRequest, maxCallsInRequest, or maxConcurrentRequests. A + /// "limit" property MUST also be present on the "problem details" + /// object, containing the name of the limit being applied. + #[serde(rename = "urn:ietf:params:jmap:error:limit")] + OverLimit, +} + +/// If a method encounters an error, the appropriate "error" response +/// MUST be inserted at the current point in the "methodResponses" array +/// and, unless otherwise specified, further processing MUST NOT happen +/// within that method call. +/// +/// Any further method calls in the request MUST then be processed as +/// normal. Errors at the method level MUST NOT generate an HTTP-level +/// error. +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub enum MethodError { + /// Some internal server resource was temporarily unavailable. + /// + /// Attempting the same operation later (perhaps after a backoff with a + /// random factor) may succeed. + ServerUnavailable, + /// An unexpected or unknown error occurred during the processing of the call. + /// + /// The method call made no changes to the server's state. Attempting the + /// same operation again is expected to fail again. Contacting the service + /// administrator is likely necessary to resolve this problem if it is + /// persistent. + ServerFail, + /// Some, but not all, expected changes described by the method occurred. + /// + /// The client MUST resynchronise impacted data to determine server state. + /// + /// Use of this error is strongly discouraged. + ServerPartialFail, + /// The server does not recognise this method name. + UnknownMethod, + /// One of the arguments is of the wrong type or is otherwise invalid, or a + /// required argument is missing. A "description" property MAY be present to + /// help debug with an explanation of what the problem was. This is a + /// non-localised string, and it is not intended to be shown directly to + /// end users. + InvalidArguments, + /// The method used a result reference for one of its arguments (see + /// Section 3.7), but this failed to resolve. + InvalidResultReference, + /// The method and arguments are valid, but executing the method would + /// violate an Access Control List (ACL) or other permissions policy. + Forbidden, + /// The accountId does not correspond to a valid account. + AccountNotFound, + /// The accountId given corresponds to a valid account, but the account + /// does not support this method or data type. + AccountNotSupportedByMethod, + /// This method modifies state, but the account is read-only (as returned on + /// the corresponding Account object in the JMAP Session resource). + AccountReadOnly, +} diff --git a/jmap-proto/src/lib.rs b/jmap-proto/src/lib.rs new file mode 100644 index 0000000..e5c772a 100644 --- /dev/null +++ a/jmap-proto/src/lib.rs @@ -1,0 +1,5 @@ +pub mod common; +pub mod endpoints; +pub mod errors; +pub mod events; +pub(crate) mod util; diff --git a/jmap-proto/src/util.rs b/jmap-proto/src/util.rs new file mode 100644 index 0000000..75a64d7 100644 --- /dev/null +++ a/jmap-proto/src/util.rs @@ -1,0 +1,11 @@ +use std::borrow::Cow; + +pub fn strip_prefix_from_cow<'a>(input: Cow<'a, str>, prefix: &str) -> Option> { + match input { + Cow::Borrowed(v) => v.strip_prefix(prefix).map(Cow::Borrowed), + Cow::Owned(v) => v + .strip_prefix(prefix) + .map(ToString::to_string) + .map(Cow::Owned), + } +} diff --git a/jmap-proto/src/endpoints/mod.rs b/jmap-proto/src/endpoints/mod.rs new file mode 100644 index 0000000..be81b4f 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/mod.rs @@ -1,0 +1,237 @@ +pub mod blob; +pub mod core; +pub mod object; +pub mod session; + +use std::{borrow::Cow, collections::HashMap, fmt::Formatter}; + +use serde::{ + de::{Error, MapAccess, SeqAccess}, + ser::{SerializeMap, SerializeSeq}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use serde_json::Value; +use serde_with::serde_as; + +use crate::{ + common::{Id, SessionState}, + util::strip_prefix_from_cow, +}; + +/// To allow clients to make more efficient use of the network and avoid +/// round trips, an argument to one method can be taken from the result +/// of a previous method call in the same request. +/// +/// To do this, the client prefixes the argument name with "#" (an +/// octothorpe). +const REFERENCE_OCTOTHORPE: &str = "#"; + +#[derive(Debug, Clone, Default)] +pub struct Arguments<'a>(HashMap, Argument<'a>>); + +impl<'a> Serialize for Arguments<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut ser = serializer.serialize_map(Some(self.0.len()))?; + + for (key, value) in &self.0 { + match value { + Argument::Reference(v) => { + ser.serialize_entry(&format!("{REFERENCE_OCTOTHORPE}{key}"), v)? + } + Argument::Absolute(v) => ser.serialize_entry(key, v)?, + } + } + + ser.end() + } +} + +impl<'de> Deserialize<'de> for Arguments<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + pub struct Visitor {} + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Arguments<'de>; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("a list of arguments as defined by RFC8620") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut arguments = Arguments::default(); + + while let Some(key) = map.next_key::>()? { + if let Some(key) = strip_prefix_from_cow(key.clone(), REFERENCE_OCTOTHORPE) { + arguments + .0 + .insert(key, Argument::Reference(map.next_value()?)); + } else { + arguments + .0 + .insert(key, Argument::Absolute(map.next_value()?)); + } + } + + Ok(arguments) + } + } + + deserializer.deserialize_seq(Visitor {}) + } +} + +#[derive(Debug, Clone)] +pub enum Argument<'a> { + Reference(ResultReference<'a>), + Absolute(Value), +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ResultReference<'a> { + /// The method call id (see Section 3.2) of a previous method call in + /// the current request. + #[serde(borrow)] + result_of: Cow<'a, str>, + /// The required name of a response to that method call. + #[serde(borrow)] + name: Cow<'a, str>, + /// A pointer into the arguments of the response selected via the name + /// and resultOf properties. This is a JSON Pointer [RFC6901], except + /// it also allows the use of "*" to map through an array. + #[serde(borrow)] + path: Cow<'a, str>, +} + +/// Method calls and responses are represented by the *Invocation* data +/// type. This is a tuple, represented as a JSON array containing three +/// elements. +#[derive(Clone, Debug)] +pub struct Invocation<'a> { + /// A "String" *name* of the method to call or of the response. + name: Cow<'a, str>, + /// A "String[*]" object containing named *arguments* for that method + /// or response. + arguments: Arguments<'a>, + /// A "String" *method call id*: an arbitrary string from the client + /// to be echoed back with the responses emitted by that method call + /// (a method may return 1 or more responses, as it may make implicit + /// calls to other methods; all responses initiated by this method + /// call get the same method call id in the response). + request_id: Cow<'a, str>, +} + +impl<'a> Serialize for Invocation<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut ser = serializer.serialize_seq(Some(3))?; + ser.serialize_element(&self.name)?; + ser.serialize_element(&self.arguments)?; + ser.serialize_element(&self.request_id)?; + ser.end() + } +} + +impl<'de: 'a, 'a> Deserialize<'de> for Invocation<'a> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + pub struct Visitor {} + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Invocation<'de>; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str( + "an Invocation data type containing 3 elements; name, arguments & id", + ) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let name = seq.next_element()?.ok_or(A::Error::missing_field("name"))?; + let arguments = seq + .next_element()? + .ok_or(A::Error::missing_field("arguments"))?; + let request_id = seq.next_element()?.ok_or(A::Error::missing_field("id"))?; + + if seq.next_element::()?.is_some() { + return Err(A::Error::invalid_length(4, &self)); + } + + Ok(Invocation { + name, + arguments, + request_id, + }) + } + } + + deserializer.deserialize_seq(Visitor {}) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde_as] +#[serde(rename_all = "camelCase")] +pub struct Request<'a> { + /// The set of capabilities the client wishes to use. The client MAY + /// include capability identifiers even if the method calls it makes + /// do not utilise those capabilities. The server advertises the set + /// of specifications it supports in the Session object (see + /// Section 2), as keys on the "capabilities" property. + #[serde_as(as = "Vec")] + using: Vec>, + /// An array of method calls to process on the server. The method + /// calls MUST be processed sequentially, in order. + #[serde(borrow)] + method_calls: Vec>, + /// A map of a (client-specified) creation id to the id the server + /// assigned when a record was successfully created. + /// + /// Records may have a property that contains the id of another record. To + /// allow more efficient network usage, you can set this property to + /// reference a record created earlier in the same API request. Since the + /// real id is unknown when the request is created, the client can instead + /// specify the creation id it assigned, prefixed with a "#" (see + /// Section 5.3 for more details). + #[serde(borrow)] + created_ids: Option, Id<'a>>>, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Response<'a> { + /// An array of responses, in the same format as the "methodCalls" on + /// the Request object. The output of the methods MUST be added to + /// the "methodResponses" array in the same order that the methods are + /// processed. + #[serde(borrow)] + method_responses: Invocation<'a>, + /// A map of a (client-specified) creation id to the id the server + /// assigned when a record was successfully created. This MUST + /// include all creation ids passed in the original createdIds + /// parameter of the Request object, as well as any additional ones + /// added for newly created records. + #[serde(borrow)] + created_ids: Option, Id<'a>>>, + /// The current value of the "state" string on the Session object, as + /// described in Section 2. Clients may use this to detect if this + /// object has changed and needs to be refetched. + session_state: SessionState<'a>, +} diff --git a/jmap-proto/src/endpoints/session.rs b/jmap-proto/src/endpoints/session.rs new file mode 100644 index 0000000..82f346f 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/session.rs @@ -1,0 +1,151 @@ +use std::{ + borrow::Cow, + collections::{BTreeSet, HashMap}, +}; + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, BorrowCow}; + +use crate::common::{Id, SessionState, UnsignedInt}; + +/// Implementors must take care to avoid inappropriate caching of the +/// Session object at the HTTP layer. Since the client should only +/// refetch when it detects there is a change (via the sessionState +/// property of an API response), it is RECOMMENDED to disable HTTP +/// caching altogether, for example, by setting "Cache-Control: no-cache, +/// no-store, must-revalidate" on the response. +/// +/// Exposed from https://${hostname}[:${port}]/.well-known/jmap +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Session<'a> { + /// An object specifying the capabilities of this server. Each key is + /// a URI for a capability supported by the server. The value for + /// each of these keys is an object with further information about the + /// server's capabilities in relation to that capability. + #[serde(borrow)] + capabilities: ServerCapabilities<'a>, + /// A map of an account id to an Account object for each account (see + /// Section 1.6.2) the user has access to. + #[serde(borrow)] + accounts: HashMap, Account<'a>>, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ServerCapabilities<'a> { + /// The capabilities object MUST include a property called + /// "urn:ietf:params:jmap:core". + #[serde(rename = "urn:ietf:params:jmap:core", borrow)] + core: CoreCapability<'a>, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CoreCapability<'a> { + /// The maximum file size, in octets, that the server will accept + /// for a single file upload (for any purpose). Suggested minimum: + /// 50,000,000. + max_size_upload: UnsignedInt, + /// The maximum number of concurrent requests the server will + /// accept to the upload endpoint. Suggested minimum: 4. + max_concurrent_upload: UnsignedInt, + /// The maximum size, in octets, that the server will accept for a + /// single request to the API endpoint. Suggested minimum: + /// 10,000,000. + max_size_request: UnsignedInt, + /// The maximum number of concurrent requests the server will + /// accept to the API endpoint. Suggested minimum: 4. + max_concurrent_requests: UnsignedInt, + /// The maximum number of method calls the server will accept in a + /// single request to the API endpoint. Suggested minimum: 16. + max_calls_in_request: UnsignedInt, + /// The maximum number of objects that the client may request in a + /// single /get type method call. Suggested minimum: 500. + max_objects_in_get: UnsignedInt, + /// The maximum number of objects the client may send to create, + /// update, or destroy in a single /set type method call. This is + /// the combined total, e.g., if the maximum is 10, you could not + /// create 7 objects and destroy 6, as this would be 13 actions, + /// which exceeds the limit. Suggested minimum: 500. + max_objects_in_set: UnsignedInt, + /// A list of identifiers for algorithms registered in the + /// collation registry, as defined in [RFC4790], that the server + /// supports for sorting when querying records. + #[serde_as(as = "BTreeSet")] + collation_algorithms: BTreeSet>, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Account<'a> { + /// A user-friendly string to show when presenting content from + /// this account, e.g., the email address representing the owner of + /// the account. + #[serde(borrow)] + name: Cow<'a, str>, + /// This is true if the account belongs to the authenticated user + /// rather than a group account or a personal account of another + /// user that has been shared with them. + is_personal: bool, + /// This is true if the entire account is read-only. + is_read_only: bool, + /// The set of capability URIs for the methods supported in this + /// account. Each key is a URI for a capability that has methods + /// you can use with this account. The value for each of these + /// keys is an object with further information about the account's + /// permissions and restrictions with respect to this capability, + /// as defined in the capability's specification. + account_capabilities: AccountCapabilities, + /// A map of capability URIs (as found in accountCapabilities) to the + /// account id that is considered to be the user's main or default + /// account for data pertaining to that capability. If no account + /// being returned belongs to the user, or in any other way there is + /// no appropriate way to determine a default account, there MAY be no + /// entry for a particular URI, even though that capability is + /// supported by the server (and in the capabilities object). + /// "urn:ietf:params:jmap:core" SHOULD NOT be present. + #[serde_as(as = "HashMap")] + primary_accounts: HashMap, Id<'a>>, + /// The username associated with the given credentials, or the empty + /// string if none. + #[serde(borrow)] + username: Cow<'a, str>, + /// The URL to use for JMAP API requests. + #[serde(borrow)] + api_url: Cow<'a, str>, + /// The URL endpoint to use when downloading files, in URI Template + /// (level 1) format [RFC6570]. The URL MUST contain variables called + /// "accountId", "blobId", "type", and "name". The use of these + /// variables is described in Section 6.2. Due to potential encoding + /// issues with slashes in content types, it is RECOMMENDED to put the + /// "type" variable in the query section of the URL. + #[serde(borrow)] + download_url: Cow<'a, str>, + /// The URL endpoint to use when uploading files, in URI Template + /// (level 1) format [RFC6570]. The URL MUST contain a variable + /// called "accountId". The use of this variable is described in + /// Section 6.1. + #[serde(borrow)] + upload_url: Cow<'a, str>, + /// The URL to connect to for push events, as described in + /// Section 7.3, in URI Template (level 1) format [RFC6570]. The URL + /// MUST contain variables called "types", "closeafter", and "ping". + /// The use of these variables is described in Section 7.3. + #[serde(borrow)] + event_source_url: Cow<'a, str>, + /// A (preferably short) string representing the state of this object + /// on the server. If the value of any other property on the Session + /// object changes, this string will change. The current value is + /// also returned on the API Response object (see Section 3.4), + /// allowing clients to quickly determine if the session information + /// has changed (e.g., an account has been added or removed), so they + /// need to refetch the object. + #[serde(borrow)] + state: SessionState<'a>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AccountCapabilities {} diff --git a/jmap-proto/src/events/mod.rs b/jmap-proto/src/events/mod.rs new file mode 100644 index 0000000..6f333a0 100644 --- /dev/null +++ a/jmap-proto/src/events/mod.rs @@ -1,0 +1,51 @@ +//! Push notifications allow clients to efficiently update (almost) +//! instantly to stay in sync with data changes on the server. The +//! general model for push is simple and sends minimal data over the push +//! channel: just enough for the client to know whether it needs to +//! resync. The format allows multiple changes to be coalesced into a +//! single push update and the frequency of pushes to be rate limited by +//! the server. It doesn't matter if some push events are dropped before +//! they reach the client; the next time it gets/sets any records of a +//! changed type, it will discover the data has changed and still sync +//! all changes. +//! +//! There are two different mechanisms by which a client can receive push +//! notifications, to allow for the different environments in which a +//! client may exist. An event source resource (see Section 7.3) allows +//! clients that can hold transport connections open to receive push +//! notifications directly from the JMAP server. This is simple and +//! avoids third parties, but it is often not feasible on constrained +//! platforms such as mobile devices. Alternatively, clients can make +//! use of any push service supported by their environment. A URL for +//! the push service is registered with the JMAP server (see +//! Section 7.2); the server then POSTs each notification to that URL. +//! The push service is then responsible for routing these to the client. + +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +pub mod state_change; + +pub trait Event { + const NAME: &'static str; + + fn into_event(self) -> BuiltEvent<'static, Self> + where + Self: Sized, + { + BuiltEvent { + type_: Self::NAME.into(), + inner: self, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct BuiltEvent<'a, T> { + #[serde(borrow, rename = "@type")] + type_: Cow<'a, str>, + #[serde(flatten)] + inner: T, +} diff --git a/jmap-proto/src/events/state_change.rs b/jmap-proto/src/events/state_change.rs new file mode 100644 index 0000000..5b86af1 100644 --- /dev/null +++ a/jmap-proto/src/events/state_change.rs @@ -1,0 +1,19 @@ +//! When something changes on the server, the server pushes a StateChange +//! object to the client. + +use std::{borrow::Cow, collections::HashMap}; + +use serde::{Deserialize, Serialize}; + +use crate::{common::Id, endpoints::object::ObjectState, events::Event}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct StateChange<'a> { + #[serde(borrow)] + changed: HashMap, HashMap, ObjectState<'a>>>, +} + +impl<'a> Event for StateChange<'a> { + const NAME: &'static str = "StateChange"; +} diff --git a/jmap-proto/src/endpoints/blob/copy.rs b/jmap-proto/src/endpoints/blob/copy.rs new file mode 100644 index 0000000..350327a 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/blob/copy.rs @@ -1,0 +1,40 @@ +//! Binary data may be copied *between* two different accounts using the +//! "Blob/copy" method rather than having to download and then reupload +//! on the client. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{common::Id, endpoints::object::set::SetError}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CopyRequest<'a> { + /// The id of the account to copy blobs from. + #[serde(borrow)] + from_account_id: Id<'a>, + /// The id of the account to copy blobs to. + account_id: Id<'a>, + /// A list of ids of blobs to copy to the other account. + blob_ids: Vec>, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CopyResponse<'a> { + /// The id of the account blobs were copied from. + #[serde(borrow)] + from_account_id: Id<'a>, + /// The id of the account blobs were copied to. + account_id: Id<'a>, + /// A map of the blobId in the fromAccount to the id for the blob in + /// the account it was copied to, or null if none were successfully + /// copied. + #[serde(default)] + copied: HashMap, Id<'a>>, + /// A map of blobId to a SetError object for each blob that failed to + /// be copied, or null if none. + #[serde(default)] + not_copied: HashMap, SetError<'a>>, +} diff --git a/jmap-proto/src/endpoints/blob/download.rs b/jmap-proto/src/endpoints/blob/download.rs new file mode 100644 index 0000000..c516f8c 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/blob/download.rs @@ -1,0 +1,27 @@ +//! The Session object (see Section 2) has a "downloadUrl" property +//! which is in URI Template (level 1) format [RFC6570]. + +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use crate::common::Id; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DownloadRequest<'a> { + /// The id of the account to which the record with the + /// blobId belongs. + account_id: Id<'a>, + /// The blobId representing the data of the file to + /// download. + blob_id: Id<'a>, + /// The type for the server to set in the "Content-Type" + /// header of the response; the blobId only represents the binary data + /// and does not have a content-type innately associated with it. + #[serde(rename = "type", borrow)] + type_: Cow<'a, str>, + /// The name for the file; the server MUST return this as the + /// filename if it sets a "Content-Disposition" header. + name: Cow<'a, str>, +} diff --git a/jmap-proto/src/endpoints/blob/mod.rs b/jmap-proto/src/endpoints/blob/mod.rs new file mode 100644 index 0000000..61a3a31 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/blob/mod.rs @@ -1,0 +1,3 @@ +pub mod copy; +pub mod download; +pub mod upload; diff --git a/jmap-proto/src/endpoints/blob/upload.rs b/jmap-proto/src/endpoints/blob/upload.rs new file mode 100644 index 0000000..75eb8c3 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/blob/upload.rs @@ -1,0 +1,30 @@ +//! There is a single endpoint that handles all file uploads for an +//! account, regardless of what they are to be used for. The Session +//! object (see Section 2) has an "uploadUrl" property in URI Template +//! (level 1) format [RFC6570], which MUST contain a variable called +//! "accountId". The client may use this template in combination with an +//! "accountId" to get the URL of the file upload resource. + +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use crate::common::{Id, UnsignedInt}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UploadResponse<'a> { + /// The id of the account used for the call. + account_id: Id<'a>, + /// The id representing the binary data uploaded. The data for this + /// id is immutable. The id *only* refers to the binary data, not any + /// metadata. + blob_id: Id<'a>, + /// The media type of the file (as specified in [RFC6838], + /// Section 4.2) as set in the Content-Type header of the upload HTTP + /// request. + #[serde(rename = "type", borrow)] + type_: Cow<'a, str>, + /// The size of the file in octets. + size: UnsignedInt, +} diff --git a/jmap-proto/src/endpoints/core/echo.rs b/jmap-proto/src/endpoints/core/echo.rs new file mode 100644 index 0000000..fcda1b2 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/core/echo.rs @@ -1,0 +1,13 @@ +use std::{borrow::Cow, collections::HashMap}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_with::{serde_as, BorrowCow}; + +#[serde_as] +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct EchoParams<'a>(#[serde_as(as = "HashMap")] HashMap, Value>); + +#[serde_as] +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct EchoResult<'a>(#[serde_as(as = "HashMap")] HashMap, Value>); diff --git a/jmap-proto/src/endpoints/core/mod.rs b/jmap-proto/src/endpoints/core/mod.rs new file mode 100644 index 0000000..a1b829e 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/core/mod.rs @@ -1,0 +1,1 @@ +pub mod echo; diff --git a/jmap-proto/src/endpoints/object/changes.rs b/jmap-proto/src/endpoints/object/changes.rs new file mode 100644 index 0000000..1a56b37 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/changes.rs @@ -1,0 +1,57 @@ +//! When the state of the set of Foo records in an account changes on the +//! server (whether due to creation, updates, or deletion), the "state" +//! property of the "Foo/get" response will change. The "Foo/changes" +//! method allows a client to efficiently update the state of its Foo +//! cache to match the new state on the server. + +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Id, UnsignedInt}, + endpoints::object::ObjectState, +}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ChangesParams<'a> { + /// The id of the account to use. + #[serde(borrow)] + account_id: Id<'a>, + /// The current state of the client. This is the string that was + /// returned as the "state" argument in the "Foo/get" response. The + /// server will return the changes that have occurred since this + /// state. + since_state: ObjectState<'a>, + /// The maximum number of ids to return in the response. The server + /// MAY choose to return fewer than this value but MUST NOT return + /// more. If not given by the client, the server may choose how many + /// to return. + max_changes: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ChangesResponse<'a> { + /// The id of the account used for the call. + #[serde(borrow)] + account_id: Id<'a>, + /// This is the "sinceState" argument echoed back; it's the state from + /// which the server is returning changes. + old_state: ObjectState<'a>, + /// This is the state the client will be in after applying the set of + /// changes to the old state. + new_state: ObjectState<'a>, + /// If true, the client may call "Foo/changes" again with the + /// "newState" returned to get further updates. If false, "newState" + /// is the current server state. + has_more_changes: bool, + /// An array of ids for records that have been created since the old + /// state. + created: Vec>, + /// An array of ids for records that have been updated since the old + /// state. + updated: Vec>, + /// An array of ids for records that have been destroyed since the old + /// state. + destroyed: Vec>, +} diff --git a/jmap-proto/src/endpoints/object/copy.rs b/jmap-proto/src/endpoints/object/copy.rs new file mode 100644 index 0000000..66d74c2 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/copy.rs @@ -1,0 +1,99 @@ +//! The only way to move Foo records *between* two different accounts is +//! to copy them using the "Foo/copy" method; once the copy has +//! succeeded, delete the original. The "onSuccessDestroyOriginal" +//! argument allows you to try to do this in one method call; however, +//! note that the two different actions are not atomic, so it is possible +//! for the copy to succeed but the original not to be destroyed for some +//! reason. +//! +//! The copy is conceptually in three phases: +//! +//! 1. Reading the current values from the "from" account. +//! 2. Writing the new copies to the other account. +//! 3. Destroying the originals in the "from" account, if requested. +//! +//! Data may change in between phases due to concurrent requests. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + common::Id, + endpoints::object::{set::SetError, ObjectState}, +}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CopyParams<'a, T> { + /// The id of the account to copy records from. + #[serde(borrow)] + from_account_id: Id<'a>, + /// This is a state string as returned by the "Foo/get" method. If + /// supplied, the string must match the current state of the account + /// referenced by the fromAccountId when reading the data to be + /// copied; otherwise, the method will be aborted and a + /// "stateMismatch" error returned. If null, the data will be read + /// from the current state. + if_from_in_state: Option>, + /// The id of the account to copy records to. This MUST be different + /// to the "fromAccountId". + account_id: Id<'a>, + /// This is a state string as returned by the "Foo/get" method. If + /// supplied, the string must match the current state of the account + /// referenced by the accountId; otherwise, the method will be aborted + /// and a "stateMismatch" error returned. If null, any changes will + /// be applied to the current state. + if_in_state: Option>, + /// A map of the *creation id* to a Foo object. The Foo object MUST + /// contain an "id" property, which is the id (in the fromAccount) of + /// the record to be copied. When creating the copy, any other + /// properties included are used instead of the current value for that + /// property on the original. + create: HashMap, T>, + /// If true, an attempt will be made to destroy the original records + /// that were successfully copied: after emitting the "Foo/copy" + /// response, but before processing the next method, the server MUST + /// make a single call to "Foo/set" to destroy the original of each + /// successfully copied record; the output of this is added to the + /// responses as normal, to be returned to the client. + #[serde(default)] + on_success_destroy_original: bool, + /// This argument is passed on as the "ifInState" argument to the + /// implicit "Foo/set" call, if made at the end of this request to + /// destroy the originals that were successfully copied. + destroy_from_if_in_state: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CopyResponse<'a, T> { + /// The id of the account records were copied from. + #[serde(borrow)] + from_account_id: Id<'a>, + /// The id of the account records were copied to. + #[serde(borrow)] + account_id: Id<'a>, + /// The state string that would have been returned by "Foo/get" on the + /// account records that were copied to before making the requested + /// changes, or null if the server doesn't know what the previous + /// state string was. + #[serde(borrow)] + old_state: Option>, + /// The state string that will now be returned by "Foo/get" on the + /// account records were copied to. + #[serde(borrow)] + new_state: ObjectState<'a>, + /// A map of the creation id to an object containing any properties of + /// the copied Foo object that are set by the server (such as the "id" + /// in most object types; note, the id is likely to be different to + /// the id of the object in the account it was copied from). + /// + /// This argument is null if no Foo objects were successfully copied. + #[serde(default, borrow)] + created: HashMap, T>, + /// A map of the creation id to a SetError object for each record that + /// failed to be copied, or null if none. + #[serde(default, borrow)] + not_created: HashMap, SetError<'a>>, +} diff --git a/jmap-proto/src/endpoints/object/get.rs b/jmap-proto/src/endpoints/object/get.rs new file mode 100644 index 0000000..4f60a8b 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/get.rs @@ -1,0 +1,62 @@ +//! Objects of type Foo are fetched via a call to "Foo/get". + +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, BorrowCow}; + +use crate::{common::Id, endpoints::object::ObjectState}; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GetParams<'a> { + /// The id of the account to use. + account_id: Id<'a>, + /// The ids of the Foo objects to return. If null, then *all* records + /// of the data type are returned, if this is supported for that data + /// type and the number of records does not exceed the + /// "maxObjectsInGet" limit. + ids: Option>>, + /// If supplied, only the properties listed in the array are returned + /// for each Foo object. If null, all properties of the object are + /// returned. The id property of the object is *always* returned, + /// even if not explicitly requested. If an invalid property is + /// requested, the call MUST be rejected with an "invalidArguments" + /// error. + #[serde_as(as = "Option>")] + properties: Option>>, +} + +// TODO: requestTooLarge error variant +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GetResponse<'a, T> { + /// The id of the account used for the call. + #[serde(borrow)] + account_id: Id<'a>, + /// A (preferably short) string representing the state on the server + /// for *all* the data of this type in the account (not just the + /// objects returned in this call). If the data changes, this string + /// MUST change. If the Foo data is unchanged, servers SHOULD return + /// the same state string on subsequent requests for this data type. + /// When a client receives a response with a different state string to + /// a previous call, it MUST either throw away all currently cached + /// objects for the type or call "Foo/changes" to get the exact + /// changes. + state: ObjectState<'a>, + /// An array of the Foo objects requested. This is the *empty array* + /// if no objects were found or if the "ids" argument passed in was + /// also an empty array. The results MAY be in a different order to + /// the "ids" in the request arguments. If an identical id is + /// included more than once in the request, the server MUST only + /// include it once in either the "list" or the "notFound" argument of + /// the response. + list: Vec, + /// This array contains the ids passed to the method for records that + /// do not exist. The array is empty if all requested ids were found + /// or if the "ids" argument passed in was either null or an empty + /// array. + id: Vec>, +} diff --git a/jmap-proto/src/endpoints/object/mod.rs b/jmap-proto/src/endpoints/object/mod.rs new file mode 100644 index 0000000..1ca5986 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/mod.rs @@ -1,0 +1,22 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +pub mod changes; +pub mod copy; +pub mod get; +pub mod query; +pub mod query_changes; +pub mod set; + +/// A (preferably short) string representing the state on the server +/// for *all* the data of this type in the account (not just the +/// objects returned in this call). If the data changes, this string +/// MUST change. If the Foo data is unchanged, servers SHOULD return +/// the same state string on subsequent requests for this data type. +/// When a client receives a response with a different state string to +/// a previous call, it MUST either throw away all currently cached +/// objects for the type or call "Foo/changes" to get the exact +/// changes. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ObjectState<'a>(#[serde(borrow)] Cow<'a, str>); diff --git a/jmap-proto/src/endpoints/object/query.rs b/jmap-proto/src/endpoints/object/query.rs new file mode 100644 index 0000000..2d80663 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/query.rs @@ -1,0 +1,200 @@ +//! For data sets where the total amount of data is expected to be very +//! small, clients can just fetch the complete set of data and then do +//! any sorting/filtering locally. However, for large data sets (e.g., +//! multi-gigabyte mailboxes), the client needs to be able to +//! search/sort/window the data type on the server. +//! +//! A query on the set of Foos in an account is made by calling "Foo/ +//! query". This takes a number of arguments to determine which records +//! to include, how they should be sorted, and which part of the result +//! should be returned (the full list may be *very* long). The result is +//! returned as a list of Foo ids. + +use std::{borrow::Cow, collections::HashMap}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::common::{Id, Int, UnsignedInt}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct QueryParams<'a> { + /// The id of the account to use. + #[serde(borrow)] + account_id: Id<'a>, + /// Determines the set of Foos returned in the results. If null, all + /// objects in the account of this type are included in the results. + filter: Filter<'a>, + /// Lists the names of properties to compare between two Foo records, + /// and how to compare them, to determine which comes first in the + /// sort. If two Foo records have an identical value for the first + /// comparator, the next comparator will be considered, and so on. If + /// all comparators are the same (this includes the case where an + /// empty array or null is given as the "sort" argument), the sort + /// order is server dependent, but it MUST be stable between calls to + /// "Foo/query". + #[serde(default)] + sort: Vec>, + /// Offset into the list of results to return. + #[serde(default, flatten)] + offset: Offset<'a>, + /// The maximum number of results to return. If null, no limit + /// presumed. The server MAY choose to enforce a maximum "limit" + /// argument. In this case, if a greater value is given (or if it is + /// null), the limit is clamped to the maximum; the new limit is + /// returned with the response so the client is aware. + limit: Option, + /// Does the client wish to know the total number of results in the + /// query? This may be slow and expensive for servers to calculate, + /// particularly with complex filters, so clients should take care to + /// only request the total when needed. + #[serde(default)] + calculate_total: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct QueryResponse<'a> { + /// The id of the account used for the call. + #[serde(borrow)] + account_id: Id<'a>, + /// A string encoding the current state of the query on the server. + /// This string MUST change if the results of the query (i.e., the + /// matching ids and their sort order) have changed. The queryState + /// string MAY change if something has changed on the server, which + /// means the results may have changed but the server doesn't know for + /// sure. + query_state: QueryState<'a>, + /// This is true if the server supports calling "Foo/queryChanges" + /// with these "filter"/"sort" parameters. Note, this does not + /// guarantee that the "Foo/queryChanges" call will succeed, as it may + /// only be possible for a limited time afterwards due to server + /// internal implementation details. + can_calculate_changes: bool, + /// The zero-based index of the first result in the "ids" array within + /// the complete list of query results. + position: UnsignedInt, + /// The list of ids for each Foo in the query results, starting at the + /// index given by the "position" argument of this response and + /// continuing until it hits the end of the results or reaches the + /// "limit" number of ids. If "position" is >= "total", this MUST be + /// the empty list. + ids: Vec>, + /// The total number of Foos in the results (given the "filter"). + /// This argument MUST be omitted if the "calculateTotal" request + /// argument is not true. + total: Option, + /// The limit enforced by the server on the maximum number of results + /// to return. This is only returned if the server set a limit or + /// used a different limit than that given in the request. + limit: Option, +} + +/// The queryState string only represents the ordered list of ids that +/// match the particular query (including its sort/filter). There is +/// no requirement for it to change if a property on an object +/// matching the query changes but the query results are unaffected +/// (indeed, it is more efficient if the queryState string does not +/// change in this case). The queryState string only has meaning when +/// compared to future responses to a query with the same type/sort/ +/// filter or when used with /queryChanges to fetch changes. +/// +/// Should a client receive back a response with a different +/// queryState string to a previous call, it MUST either throw away +/// the currently cached query and fetch it again (note, this does not +/// require fetching the records again, just the list of ids) or call +/// "Foo/queryChanges" to get the difference. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct QueryState<'a>(#[serde(borrow)] Cow<'a, str>); + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(untagged)] +pub enum Offset<'a> { + Position { + /// The zero-based index of the first id in the full list of results + /// to return. + /// + /// If a negative value is given, it is an offset from the end of the + /// list. Specifically, the negative value MUST be added to the total + /// number of results given the filter, and if still negative, it's + /// clamped to "0". This is now the zero-based index of the first id + /// to return. + /// + /// If the index is greater than or equal to the total number of + /// objects in the results list, then the "ids" array in the response + /// will be empty, but this is not an error. + position: Int, + }, + Anchor { + /// A Foo id. If supplied, the "position" argument is ignored. The + /// index of this id in the results will be used in combination with + /// the "anchorOffset" argument to determine the index of the first + /// result to return (see below for more details). + #[serde(borrow)] + anchor: Id<'a>, + /// The index of the first result to return relative to the index of + /// the anchor, if an anchor is given. This MAY be negative. For + /// example, "-1" means the Foo immediately preceding the anchor is + /// the first result in the list returned (see below for more + /// details). + #[serde(default)] + anchor_offset: Int, + }, + #[default] + Default, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Comparator<'a> { + /// The name of the property on the Foo objects to compare. + property: Cow<'a, str>, + /// If true, sort in ascending order. If false, reverse the + /// comparator's results to sort in descending order. + #[serde(default = "default_is_ascending")] + is_ascending: bool, + /// The identifier, as registered in the collation registry defined + /// in [RFC4790], for the algorithm to use when comparing the order + /// of strings. The algorithms the server supports are advertised + /// in the capabilities object returned with the Session object + /// (see Section 2). + collation: Option>, +} + +const fn default_is_ascending() -> bool { + true +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum Filter<'a> { + Operator(FilterOperator<'a>), + Condition(HashMap, Value>), +} + +/// A *FilterCondition* is an "object" whose allowed properties and +/// semantics depend on the data type and is defined in the /query +/// method specification for that type. It MUST NOT have an +/// "operator" property. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FilterCondition<'a>(HashMap, Value>); + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FilterOperator<'a> { + operator: Operator, + conditions: Vec>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Operator { + /// All of the conditions must match for the filter to match. + And, + /// At least one of the conditions must match for the + /// filter to match. + Or, + /// None of the conditions must match for the filter to + /// match. + Not, +} diff --git a/jmap-proto/src/endpoints/object/query_changes.rs b/jmap-proto/src/endpoints/object/query_changes.rs new file mode 100644 index 0000000..650acbb 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/query_changes.rs @@ -1,0 +1,103 @@ +//! The "Foo/queryChanges" method allows a client to efficiently update +//! the state of a cached query to match the new state on the server. + +use serde::{Deserialize, Serialize}; + +use crate::{ + common::{Id, UnsignedInt}, + endpoints::object::query::{Comparator, Filter, QueryParams, QueryState}, +}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct QueryChangesParams<'a> { + /// The id of the account to use. + #[serde(borrow)] + account_id: Id<'a>, + /// The filter argument that was used with "Foo/query". + filter: Option>, + /// The sort argument that was used with "Foo/query". + #[serde(default)] + sort: Vec>, + /// The current state of the query in the client. This is the string + /// that was returned as the "queryState" argument in the "Foo/query" + /// response with the same sort/filter. The server will return the + /// changes made to the query since this state. + since_query_state: QueryState<'a>, + /// The maximum number of changes to return in the response. See + /// error descriptions below for more details. + max_changes: Option, + /// The last (highest-index) id the client currently has cached from + /// the query results. When there are a large number of results, in a + /// common case, the client may have only downloaded and cached a + /// small subset from the beginning of the results. If the sort and + /// filter are both only on immutable properties, this allows the + /// server to omit changes after this point in the results, which can + /// significantly increase efficiency. If they are not immutable, + /// this argument is ignored. + up_to_id: Option>, + /// Does the client wish to know the total number of results now in + /// the query? This may be slow and expensive for servers to + /// calculate, particularly with complex filters, so clients should + /// take care to only request the total when needed. + #[serde(default)] + calculate_total: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct QueryChangesResponse<'a> { + /// The id of the account used for the call. + #[serde(borrow)] + account_id: Id<'a>, + /// This is the "sinceQueryState" argument echoed back; that is, the + /// state from which the server is returning changes. + old_query_state: QueryState<'a>, + /// This is the state the query will be in after applying the set of + /// changes to the old state. + new_query_state: QueryParams<'a>, + /// The total number of Foos in the results (given the "filter"). + /// This argument MUST be omitted if the "calculateTotal" request + /// argument is not true. + total: Option, + /// The "id" for every Foo that was in the query results in the old + /// state and that is not in the results in the new state. + /// + /// If the server cannot calculate this exactly, the server MAY return + /// the ids of extra Foos in addition that may have been in the old + /// results but are not in the new results. + /// + /// If the sort and filter are both only on immutable properties and + /// an "upToId" is supplied and exists in the results, any ids that + /// were removed but have a higher index than "upToId" SHOULD be + /// omitted. + /// + /// If the "filter" or "sort" includes a mutable property, the server + /// MUST include all Foos in the current results for which this + /// property may have changed. The position of these may have moved + /// in the results, so they must be reinserted by the client to ensure + /// its query cache is correct. + removed: Vec>, + /// The id and index in the query results (in the new state) for every + /// Foo that has been added to the results since the old state AND + /// every Foo in the current results that was included in the + /// "removed" array (due to a filter or sort based upon a mutable + /// property). + /// + /// If the sort and filter are both only on immutable properties and + /// an "upToId" is supplied and exists in the results, any ids that + /// were added but have a higher index than "upToId" SHOULD be + /// omitted. + /// + /// The array MUST be sorted in order of index, with the lowest index + /// first. + added: Vec>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AddedItem<'a> { + #[serde(borrow)] + id: Id<'a>, + index: UnsignedInt, +} diff --git a/jmap-proto/src/endpoints/object/set.rs b/jmap-proto/src/endpoints/object/set.rs new file mode 100644 index 0000000..ef22b7f 100644 --- /dev/null +++ a/jmap-proto/src/endpoints/object/set.rs @@ -1,0 +1,170 @@ +//! Modifying the state of Foo objects on the server is done via the +//! "Foo/set" method. This encompasses creating, updating, and +//! destroying Foo records. This allows the server to sort out ordering +//! 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). + +use std::{borrow::Cow, collections::HashMap}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use serde_with::{serde_as, BorrowCow}; + +use crate::{common::Id, endpoints::object::ObjectState}; + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetParams<'a, T> { + /// The id of the account to use. + account_id: Id<'a>, + /// This is a state string as returned by the "Foo/get" method + /// (representing the state of all objects of this type in the + /// account). If supplied, the string must match the current state; + /// otherwise, the method will be aborted and a "stateMismatch" error + /// returned. If null, any changes will be applied to the current + /// state. + #[serde(borrow)] + if_in_state: Option>, + /// A map of a *creation id* (a temporary id set by the client) to Foo + /// objects, or null if no objects are to be created. + /// + /// The Foo object type definition may define default values for + /// properties. Any such property may be omitted by the client. + /// + /// The client MUST omit any properties that may only be set by the + /// server (for example, the "id" property on most object types). + #[serde(default)] + create: HashMap, T>, + /// A map of an id to a Patch object to apply to the current Foo + /// object with that id, or null if no objects are to be updated. + #[serde(default)] + update: HashMap, PatchObject<'a>>, + /// A list of ids for Foo objects to permanently delete, or null if no + /// objects are to be destroyed. + #[serde(default)] + destroy: Vec>, +} + +/// A *PatchObject* is of type "String[*]" and represents an unordered +/// set of patches. The keys are a path in JSON Pointer format +/// [RFC6901], with an implicit leading "/" (i.e., prefix each key +/// with "/" before applying the JSON Pointer evaluation algorithm). +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PatchObject<'a>(#[serde_as(as = "HashMap")] HashMap, Value>); + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetResult<'a, T> { + /// The id of the account used for the call. + #[serde(borrow)] + account_id: Id<'a>, + /// The state string that would have been returned by "Foo/get" before + /// making the requested changes, or null if the server doesn't know + /// what the previous state string was. + #[serde(borrow)] + old_state: Option>, + /// The state string that will now be returned by "Foo/get". + #[serde(borrow)] + new_state: ObjectState<'a>, + /// A map of the creation id to an object containing any properties of + /// the created Foo object that were not sent by the client. This + /// includes all server-set properties (such as the "id" in most + /// object types) and any properties that were omitted by the client + /// and thus set to a default by the server. + /// + /// This argument is null if no Foo objects were successfully created. + #[serde(default, borrow)] + created: HashMap, T>, + /// The keys in this map are the ids of all Foos that were + /// successfully updated. + /// + /// The value for each id is a Foo object containing any property that + /// changed in a way *not* explicitly requested by the PatchObject + /// sent to the server, or null if none. This lets the client know of + /// any changes to server-set or computed properties. + /// + /// This argument is null if no Foo objects were successfully updated. + #[serde(default, borrow)] + updated: HashMap, Option>, + /// A list of Foo ids for records that were successfully destroyed, or + /// null if none. + #[serde(default, borrow)] + destroyed: Vec>, + /// A map of the creation id to a SetError object for each record that + /// failed to be created, or null if all successful. + #[serde(default, borrow)] + not_created: HashMap, SetError<'a>>, + /// A map of the Foo id to a SetError object for each record that + /// failed to be updated, or null if all successful. + #[serde(default, borrow)] + not_updated: HashMap, SetError<'a>>, + /// A map of the Foo id to a SetError object for each record that + /// failed to be destroyed, or null if all successful. + #[serde(default, borrow)] + not_destroyed: HashMap, SetError<'a>>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SetError<'a> { + /// The type of error. + #[serde(rename = "type")] + type_: SetErrorKind, + /// A description of the error to help with debugging that includes an + /// explanation of what the problem was. This is a non-localised + /// string and is not intended to be shown directly to end users. + #[serde(borrow)] + description: Option>, + /// The SetError object SHOULD also have a property called "properties" of + /// type "String[]" that lists *all* the properties that were invalid. For + /// type of `invalidProperties`. + #[serde(borrow)] + properties: Vec>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub enum SetErrorKind { + /// (create; update; destroy). The create/update/destroy would violate + /// an ACL or other permissions policy. + Forbidden, + /// (create; update). The create would exceed a server-defined limit + /// on the number or total size of objects of this type. + OverQuota, + /// (create; update). The create/update would result in an object that + /// exceeds a server-defined limit for the maximum size of a single object + /// of this type. + TooLarge, + /// (create). Too many objects of this type have been created recently, + /// and a server-defined rate limit has been reached. It may work if tried + /// again later. + RateLimit, + /// (update; destroy). The id given to update/destroy cannot be found. + NotFound, + /// (update). The PatchObject given to update the record was not a valid + /// patch (see the patch description). + InvalidPatch, + /// (update). The client requested that an object be both updated and + /// destroyed in the same /set request, and the server has decided to + /// therefore ignore the update. + WillDestroy, + /// (create; update). The record given is invalid in some way. For + /// example: + /// + /// - It contains properties that are invalid according to the type specification of this + /// record type. + /// - It contains a property that may only be set by the server (e.g., "id") and is different + /// to the current value. Note, to allow clients to pass whole objects back, it is not an + /// error to include a server-set property in an update as long as the value is identical to + /// the current value on the server. + /// - There is a reference to another record (foreign key), and the given id does not + /// correspond to a valid record. + InvalidProperties, + /// (create; destroy). This is a singleton type, so you cannot create + /// another one or destroy the existing one. + Singleton, +} -- rgit 0.1.3