🏡 index : ~doyle/jogre.git

author Jordan Doyle <jordan@doyle.la> 2023-09-16 23:26:08.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-09-16 23:27:27.0 +01:00:00
commit
12249d2c581cdedf2e6b946ce947e4931ca5736c [patch]
tree
4a658cadaadf5d5db08282c3f199e6ef4ab67ce2
download
12249d2c581cdedf2e6b946ce947e4931ca5736c.tar.gz

Initial commit



Diff

 .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 <sam@hocevar.net>

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<FixedOffset>);

/// 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<Utc>);

/// 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<String, Value>,
}

#[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<Cow<'a, str>> {
    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<Cow<'a, str>, Argument<'a>>);

impl<'a> Serialize for Arguments<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    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<D>(deserializer: D) -> Result<Self, D::Error>
    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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: MapAccess<'de>,
            {
                let mut arguments = Arguments::default();

                while let Some(key) = map.next_key::<Cow<'de, str>>()? {
                    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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    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<D>(deserializer: D) -> Result<Self, D::Error>
    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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            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::<Value>()?.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<BorrowedCow>")]
    using: Vec<Cow<'a, str>>,
    /// An array of method calls to process on the server.  The method

    /// calls MUST be processed sequentially, in order.

    #[serde(borrow)]
    method_calls: Vec<Invocation<'a>>,
    /// 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<HashMap<Id<'a>, 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<HashMap<Id<'a>, 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<Id<'a>, 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<BorrowCow>")]
    collation_algorithms: BTreeSet<Cow<'a, str>>,
}

#[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<BorrowCow, _>")]
    primary_accounts: HashMap<Cow<'a, str>, 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<Id<'a>, HashMap<Cow<'a, str>, 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<Id<'a>>,
}

#[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>, 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<Id<'a>, 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<BorrowCow, _>")] HashMap<Cow<'a, str>, Value>);

#[serde_as]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct EchoResult<'a>(#[serde_as(as = "HashMap<BorrowCow, _>")] HashMap<Cow<'a, str>, 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<UnsignedInt>,
}

#[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<Id<'a>>,
    /// An array of ids for records that have been updated since the old

    /// state.

    updated: Vec<Id<'a>>,
    /// An array of ids for records that have been destroyed since the old

    /// state.

    destroyed: Vec<Id<'a>>,
}
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<ObjectState<'a>>,
    /// 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<ObjectState<'a>>,
    /// 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<Id<'a>, 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<ObjectState<'a>>,
}

#[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<ObjectState<'a>>,
    /// 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<Id<'a>, 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<Id<'a>, 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<Vec<Id<'a>>>,
    /// 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<Vec<BorrowCow>>")]
    properties: Option<Vec<Cow<'a, str>>>,
}

// 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<T>,
    /// 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<Id<'a>>,
}
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<Comparator<'a>>,
    /// 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<UnsignedInt>,
    /// 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<Id<'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<UnsignedInt>,
    /// 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<UnsignedInt>,
}

/// 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<Cow<'a, str>>,
}

const fn default_is_ascending() -> bool {
    true
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Filter<'a> {
    Operator(FilterOperator<'a>),
    Condition(HashMap<Cow<'a, str>, 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<Cow<'a, str>, Value>);

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FilterOperator<'a> {
    operator: Operator,
    conditions: Vec<Filter<'a>>,
}

#[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<Filter<'a>>,
    /// The sort argument that was used with "Foo/query".

    #[serde(default)]
    sort: Vec<Comparator<'a>>,
    /// 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<UnsignedInt>,
    /// 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<Id<'a>>,
    /// 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<UnsignedInt>,
    /// 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<Id<'a>>,
    /// 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<AddedItem<'a>>,
}

#[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<ObjectState<'a>>,
    /// 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<Id<'a>, 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<Id<'a>, 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<Id<'a>>,
}

/// 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<BorrowCow, _>")] HashMap<Cow<'a, str>, 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<ObjectState<'a>>,
    /// 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<Id<'a>, 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<Id<'a>, Option<T>>,
    /// A list of Foo ids for records that were successfully destroyed, or

    /// null if none.

    #[serde(default, borrow)]
    destroyed: Vec<Id<'a>>,
    /// 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<Id<'a>, 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<Id<'a>, 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<Id<'a>, 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<Cow<'a, str>>,
    /// 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<Cow<'a, str>>,
}

#[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,
}