Foundational work for generic extension handling
Diff
Cargo.lock | 24 ++++++++++++++++++++++++
jmap-proto/Cargo.toml | 1 +
jogre-server/Cargo.toml | 1 +
jmap-proto/src/errors.rs | 30 +++++++++++++++++++++++++-----
jmap-proto/src/lib.rs | 2 ++
jogre-server/src/context.rs | 16 ++++++++++++++++
jogre-server/src/main.rs | 1 +
jmap-proto/src/endpoints/mod.rs | 49 ++++++++++++++++++++++++++++++++++++-------------
jmap-proto/src/endpoints/session.rs | 14 +++++---------
jmap-proto/src/extensions/js_contact.rs | 640 --------------------------------------------------------------------------------
jmap-proto/src/extensions/mod.rs | 3 ++-
jmap-proto/src/extensions/sharing.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
jogre-server/src/context/oauth2.rs | 1 -
jogre-server/src/extensions/contacts.rs | 42 ++++++++++++++++++++++++++++++++++++++++++
jogre-server/src/extensions/core.rs | 35 +++++++++++++++++++++++++++++++++++
jogre-server/src/extensions/mod.rs | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
jogre-server/src/extensions/sharing.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
jogre-server/src/methods/mod.rs | 7 ++++++-
jogre-server/src/methods/session.rs | 21 +++++----------------
jmap-proto/src/extensions/contacts/js_contact.rs | 640 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
jmap-proto/src/extensions/contacts/mod.rs | 1 +
jogre-server/src/methods/api/mod.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
22 files changed, 1304 insertions(+), 686 deletions(-)
@@ -904,6 +904,7 @@
"serde",
"serde_json",
"serde_with",
"strum",
]
[[package]]
@@ -935,6 +936,7 @@
"rand",
"rocksdb",
"serde",
"serde_json",
"sha3",
"tokio",
"toml",
@@ -1651,6 +1653,28 @@
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "subtle"
@@ -10,3 +10,4 @@
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "3.3", features = ["macros"] }
strum = { version = "0.25", features = ["derive"] }
@@ -31,4 +31,5 @@
url = { version = "2.4", features = ["serde"] }
uuid = { version = "1.4", features = ["v4", "serde"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0"
sha3 = "0.10"
@@ -1,16 +1,19 @@
use std::{borrow::Cow, collections::HashMap};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum::Display;
use crate::endpoints::{Argument, Arguments, Invocation};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RequestError {
#[serde(rename = "type")]
type_: ProblemType,
status: u16,
detail: Cow<'static, str>,
pub type_: ProblemType,
pub status: u16,
pub detail: Cow<'static, str>,
#[serde(flatten)]
meta: HashMap<String, Value>,
pub meta: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -44,7 +47,8 @@
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Display)]
#[serde(tag = "type")]
pub enum MethodError {
@@ -86,4 +90,20 @@
AccountReadOnly,
}
impl MethodError {
pub fn into_invocation(self, request_id: Cow<'_, str>) -> Invocation<'_> {
let mut arguments = Arguments::default();
arguments.0.insert(
Cow::Borrowed("type"),
Argument::Absolute(Value::String(self.to_string())),
);
Invocation {
name: "error".into(),
arguments,
request_id,
}
}
}
@@ -1,6 +1,8 @@
pub mod common;
pub mod endpoints;
pub mod errors;
pub mod events;
pub mod extensions;
pub(crate) mod util;
pub use serde_json::Value;
@@ -1,7 +1,12 @@
use std::sync::Arc;
use crate::{
config::{Config, CoreCapabilities},
extensions,
extensions::{
sharing::{Principals, PrincipalsOwner},
ExtensionRegistry,
},
store::Store,
};
@@ -12,6 +17,7 @@
pub store: Arc<Store>,
pub base_url: url::Url,
pub core_capabilities: CoreCapabilities,
pub extension_registry: ExtensionRegistry,
}
impl Context {
@@ -19,11 +25,21 @@
let derived_keys = Arc::new(DerivedKeys::new(&config.private_key));
let store = Arc::new(Store::from_config(config.store));
let extension_registry = ExtensionRegistry {
core: extensions::core::Core {
core_capabilities: config.core_capabilities,
},
contacts: extensions::contacts::Contacts {},
sharing_principals: Principals {},
sharing_principals_owner: PrincipalsOwner {},
};
Self {
oauth2: oauth2::OAuth2::new(store.clone(), derived_keys),
store,
base_url: config.base_url,
core_capabilities: config.core_capabilities,
extension_registry,
}
}
}
@@ -1,8 +1,9 @@
#![deny(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
mod config;
mod context;
mod extensions;
mod layers;
mod methods;
mod store;
@@ -27,8 +27,31 @@
const REFERENCE_OCTOTHORPE: &str = "#";
#[derive(Debug, Clone, Default)]
pub struct Arguments<'a>(HashMap<Cow<'a, str>, Argument<'a>>);
pub struct Arguments<'a>(pub HashMap<Cow<'a, str>, Argument<'a>>);
impl Arguments<'_> {
pub fn pointer(&self, pointer: &str) -> Option<Cow<Value>> {
if pointer.is_empty() {
return Some(Cow::Owned(serde_json::to_value(self).unwrap()));
}
let pointer = pointer.strip_prefix('/')?;
let mut pointer = pointer.splitn(2, pointer);
if let Argument::Absolute(value) = self.0.get(pointer.next()?)? {
value
.pointer(pointer.next().unwrap_or(""))
.map(Cow::Borrowed)
} else {
None
}
}
}
impl<'a> Serialize for Arguments<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -101,15 +124,15 @@
#[serde(borrow)]
result_of: Cow<'a, str>,
pub result_of: Cow<'a, str>,
#[serde(borrow)]
name: Cow<'a, str>,
pub name: Cow<'a, str>,
#[serde(borrow)]
path: Cow<'a, str>,
pub path: Cow<'a, str>,
}
@@ -118,16 +141,16 @@
#[derive(Clone, Debug)]
pub struct Invocation<'a> {
name: Cow<'a, str>,
pub name: Cow<'a, str>,
arguments: Arguments<'a>,
pub arguments: Arguments<'a>,
request_id: Cow<'a, str>,
pub request_id: Cow<'a, str>,
}
impl<'a> Serialize for Invocation<'a> {
@@ -195,11 +218,11 @@
#[serde_as(as = "Vec<BorrowedCow>")]
using: Vec<Cow<'a, str>>,
pub using: Vec<Cow<'a, str>>,
#[serde(borrow)]
method_calls: Vec<Invocation<'a>>,
pub method_calls: Vec<Invocation<'a>>,
@@ -210,7 +233,7 @@
#[serde(borrow)]
created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
pub created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
}
#[serde_as]
@@ -222,16 +245,16 @@
#[serde(borrow)]
method_responses: Invocation<'a>,
pub method_responses: Vec<Invocation<'a>>,
#[serde(borrow)]
created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
pub created_ids: Option<HashMap<Id<'a>, Id<'a>>>,
session_state: SessionState<'a>,
pub session_state: SessionState<'a>,
}
@@ -1,9 +1,10 @@
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{serde_as, BorrowCow};
use crate::common::{Id, SessionState, UnsignedInt};
@@ -24,8 +25,11 @@
#[serde(borrow)]
pub capabilities: ServerCapabilities<'a>,
pub capabilities: HashMap<Cow<'a, str>, Value>,
#[serde(borrow)]
@@ -76,14 +80,6 @@
#[serde(borrow)]
pub state: SessionState<'a>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ServerCapabilities<'a> {
#[serde(rename = "urn:ietf:params:jmap:core", borrow)]
pub core: CoreCapability<'a>,
}
#[serde_as]
@@ -1,640 +1,0 @@
use std::{borrow::Cow, collections::HashMap};
use chrono::NaiveDate;
use serde::{
ser::SerializeMap, Deserialize, Serialize, Serializer, __private::ser::FlatMapSerializer,
};
use serde_json::Value;
use crate::common::{Id, UnsignedInt, UtcDate};
#[derive(Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct TypeWrapper<T>(T);
impl<T: Serialize + TypedStruct> Serialize for TypeWrapper<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("@type", T::KIND)?;
self.0.serialize(FlatMapSerializer(&mut map))?;
map.end()
}
}
pub trait TypedStruct {
const KIND: &'static str;
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase", tag = "@type")]
pub enum Data<'a> {
Card(#[serde(borrow)] Card<'a>),
CardGroup(CardGroup<'a>),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CardGroup<'a> {
#[serde(borrow)]
uid: Id<'a>,
members: HashMap<Id<'a>, bool>,
#[serde(default)]
name: Cow<'a, str>,
card: Option<Card<'a>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Card<'a> {
#[serde(borrow)]
uid: Id<'a>,
prod_id: Option<Cow<'a, str>>,
created: Option<UtcDate>,
updated: Option<UtcDate>,
kind: Option<CardKind>,
#[serde(default)]
related_to: HashMap<Id<'a>, TypeWrapper<Relation>>,
language: Option<Cow<'a, str>>,
#[serde(default)]
name: Vec<TypeWrapper<NameComponent<'a>>>,
#[serde(default)]
full_name: Cow<'a, str>,
#[serde(default)]
nick_names: Vec<Cow<'a, str>>,
#[serde(default)]
organizations: HashMap<Id<'a>, TypeWrapper<Organization<'a>>>,
#[serde(default)]
titles: HashMap<Id<'a>, TypeWrapper<Title<'a>>>,
#[serde(default)]
emails: HashMap<Id<'a>, TypeWrapper<EmailAddress<'a>>>,
#[serde(default)]
phones: HashMap<Id<'a>, TypeWrapper<Phone<'a>>>,
#[serde(default)]
online: HashMap<Id<'a>, TypeWrapper<Resource<'a>>>,
#[serde(default)]
photos: HashMap<Id<'a>, TypeWrapper<File<'a>>>,
preferred_contact_method: Option<PreferredContactMethod>,
#[serde(default)]
preferred_contact_languages: HashMap<String, TypeWrapper<ContactLanguage>>,
#[serde(default)]
address: HashMap<Id<'a>, TypeWrapper<Address<'a>>>,
#[serde(default)]
localizations: HashMap<Cow<'a, str>, Value>,
#[serde(default)]
anniversaries: HashMap<Id<'a>, TypeWrapper<Anniversary<'a>>>,
#[serde(default)]
personal_info: HashMap<Id<'a>, TypeWrapper<PersonalInfo<'a>>>,
#[serde(default)]
notes: Cow<'a, str>,
#[serde(default)]
categories: HashMap<Cow<'a, str>, bool>,
#[serde(default)]
time_zones: HashMap<Cow<'a, str>, Value>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PersonalInfo<'a> {
#[serde(rename = "type")]
type_: PersonalInfoType,
value: Cow<'a, str>,
level: Option<PersonalInfoLevel>,
}
impl TypedStruct for PersonalInfo<'_> {
const KIND: &'static str = "PersonalInfo";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoType {
Expertise,
Hobby,
Interest,
Other,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoLevel {
High,
Medium,
Low,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Anniversary<'a> {
#[serde(rename = "type")]
type_: AnniversaryType,
#[serde(default)]
label: Cow<'a, str>,
date: NaiveDate,
place: Option<Address<'a>>,
}
impl TypedStruct for Anniversary<'_> {
const KIND: &'static str = "Anniversary";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AnniversaryType {
Birth,
Death,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Address<'a> {
#[serde(default)]
full_address: Cow<'a, str>,
#[serde(default)]
street: Vec<TypeWrapper<StreetComponent<'a>>>,
#[serde(default)]
locality: Cow<'a, str>,
#[serde(default)]
region: Cow<'a, str>,
#[serde(default)]
country: Cow<'a, str>,
#[serde(default)]
postcode: Cow<'a, str>,
#[serde(default)]
country_code: Cow<'a, str>,
#[serde(default)]
coordinates: Cow<'a, str>,
#[serde(default)]
time_zone: Cow<'a, str>,
#[serde(default)]
context: HashMap<AddressContext, bool>,
#[serde(default)]
label: Cow<'a, str>,
pref: Option<Preference>,
}
impl TypedStruct for Address<'_> {
const KIND: &'static str = "Address";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AddressContext {
Billing,
Postal,
Other(Context),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StreetComponent<'a> {
#[serde(rename = "type")]
type_: StreetComponentKind,
value: Cow<'a, str>,
}
impl TypedStruct for StreetComponent<'_> {
const KIND: &'static str = "StreetComponent";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum StreetComponentKind {
Name,
Number,
Apartment,
Room,
Extension,
Direction,
Building,
Floor,
PostOfficeBox,
Separator,
Unknown,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ContactLanguage {
context: Option<Context>,
pref: Option<Preference>,
}
impl TypedStruct for ContactLanguage {
const KIND: &'static str = "ContactLanguage";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PreferredContactMethod {
Emails,
Phones,
Online,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct File<'a> {
href: Cow<'a, str>,
media_type: Cow<'a, str>,
size: Option<UnsignedInt>,
pref: Option<Preference>,
}
impl TypedStruct for File<'_> {
const KIND: &'static str = "File";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Resource<'a> {
resource: Cow<'a, str>,
#[serde(rename = "type")]
type_: ResourceType,
#[serde(default)]
media_type: Cow<'a, str>,
#[serde(default)]
context: HashMap<Context, bool>,
#[serde(default)]
label: Cow<'a, str>,
pref: Option<Preference>,
}
impl TypedStruct for Resource<'_> {
const KIND: &'static str = "Resource";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ResourceType {
Uri,
Username,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Phone<'a> {
phone: Cow<'a, str>,
#[serde(default)]
features: HashMap<PhoneFeature, bool>,
#[serde(default)]
contexts: HashMap<Context, bool>,
#[serde(default)]
label: Cow<'a, str>,
pref: Option<Preference>,
}
impl TypedStruct for Phone<'_> {
const KIND: &'static str = "Phone";
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum PhoneFeature {
Voice,
Fax,
Pager,
Text,
Cell,
Textphone,
Video,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct EmailAddress<'a> {
email: Cow<'a, str>,
#[serde(default)]
contexts: HashMap<Context, bool>,
pref: Option<Preference>,
}
impl TypedStruct for EmailAddress<'_> {
const KIND: &'static str = "EmailAddress";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Preference(u8);
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Title<'a> {
#[serde(borrow)]
name: Cow<'a, str>,
#[serde(default)]
organization: Vec<Id<'a>>,
}
impl TypedStruct for Title<'_> {
const KIND: &'static str = "Title";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Organization<'a> {
name: Cow<'a, str>,
#[serde(default)]
units: Vec<Cow<'a, str>>,
}
impl TypedStruct for Organization<'_> {
const KIND: &'static str = "Organization";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NameComponent<'a> {
value: Cow<'a, str>,
#[serde(rename = "type")]
type_: NameComponentKind,
}
impl TypedStruct for NameComponent<'_> {
const KIND: &'static str = "NameComponent";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum NameComponentKind {
Prefix,
Personal,
Surname,
Additional,
Suffix,
Separator,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Relation {
relation: HashMap<RelationKind, bool>,
}
impl TypedStruct for Relation {
const KIND: &'static str = "Relation";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Context {
Private,
Work,
Other,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum RelationKind {
Contact,
Acquaintance,
Friend,
Met,
CoWorker,
Colleague,
CoResident,
Neighbor,
Child,
Parent,
Sibling,
Spouse,
Kin,
Muse,
Crush,
Date,
Sweetheart,
Me,
Agent,
Emergency,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum CardKind {
Individual,
Org,
Location,
Device,
Application,
}
@@ -1,1 +1,2 @@
pub mod js_contact;
pub mod contacts;
pub mod sharing;
@@ -1,0 +1,137 @@
use std::{borrow::Cow, collections::HashMap};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
common::{Id, UtcDate},
endpoints::session::Account,
};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalsSessionCapabilities {}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalsAccountCapabilities<'a> {
#[serde(borrow)]
pub current_user_principal_id: Option<Id<'a>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PrincipalsOwnerAccountCapabilities<'a> {
#[serde(borrow)]
pub account_id_for_principal: Id<'a>,
#[serde(borrow)]
pub principal_id: Id<'a>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Principal<'a> {
#[serde(borrow)]
pub id: Id<'a>,
pub type_: PrincipalType,
#[serde(borrow)]
pub name: Cow<'a, str>,
#[serde(borrow)]
pub description: Option<Cow<'a, str>>,
#[serde(borrow)]
pub email: Option<Cow<'a, str>>,
#[serde(borrow)]
pub time_zone: Option<Cow<'a, str>>,
#[serde(borrow)]
pub capabilities: HashMap<Cow<'a, str>, Value>,
#[serde(borrow)]
pub accounts: Option<HashMap<Id<'a>, Account<'a>>>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "camelCase")]
pub enum PrincipalType {
Individual,
Group,
Resource,
Location,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ShareNotification<'a> {
pub id: Cow<'a, str>,
pub created: UtcDate,
pub changed_by: Person<'a>,
pub object_id: Cow<'a, str>,
pub object_account_id: Cow<'a, str>,
pub name: Cow<'a, str>,
pub old_rights: Cow<'a, str>,
pub new_rights: Cow<'a, str>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Person<'a> {
pub name: Cow<'a, str>,
pub email: Option<Cow<'a, str>>,
pub principal: Option<Cow<'a, str>>,
}
@@ -325,7 +325,6 @@
return AuthState::Unauthenticated(Some(UnauthenticatedState::InvalidCsrfToken));
}
let Some(user) = store.get_by_username(username).await.unwrap() else {
return AuthState::Unauthenticated(Some(UnauthenticatedState::InvalidUserPass));
};
@@ -1,0 +1,42 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::extensions::{JmapDataExtension, JmapExtension};
pub struct Contacts {}
impl JmapExtension for Contacts {
const EXTENSION: &'static str = "urn:ietf:params:jmap:contacts";
}
impl JmapDataExtension<AddressBook> for Contacts {
const ENDPOINT: &'static str = "AddressBook";
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ContactMetadata {
pub may_create_address_book: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AddressBook {
id: Uuid,
name: String,
is_subscribed: bool,
owner: Uuid,
share_with: HashMap<Uuid, AddressBookRights>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct AddressBookRights {
may_read: bool,
may_write: bool,
may_admin: bool,
may_delete: bool,
}
@@ -1,0 +1,35 @@
use std::collections::BTreeSet;
use jmap_proto::endpoints::session::CoreCapability;
use uuid::Uuid;
use crate::{
config::CoreCapabilities,
extensions::{JmapExtension, JmapSessionCapabilityExtension},
};
#[derive(Clone)]
pub struct Core {
pub(crate) core_capabilities: CoreCapabilities,
}
impl JmapExtension for Core {
const EXTENSION: &'static str = "urn:ietf:params:jmap:core";
}
impl JmapSessionCapabilityExtension for Core {
type Metadata = CoreCapability<'static>;
fn build(&self, _user: Uuid) -> Self::Metadata {
CoreCapability {
max_size_upload: self.core_capabilities.max_size_upload.into(),
max_concurrent_upload: self.core_capabilities.max_concurrent_upload.into(),
max_size_request: self.core_capabilities.max_size_request.into(),
max_concurrent_requests: self.core_capabilities.max_concurrent_requests.into(),
max_calls_in_request: self.core_capabilities.max_calls_in_request.into(),
max_objects_in_get: self.core_capabilities.max_objects_in_get.into(),
max_objects_in_set: self.core_capabilities.max_objects_in_set.into(),
collation_algorithms: BTreeSet::default(),
}
}
}
@@ -1,0 +1,158 @@
use std::{borrow::Cow, collections::HashMap};
use jmap_proto::{extensions::sharing as proto_sharing, Value};
use serde::{
de::{value::CowStrDeserializer, DeserializeSeed, MapAccess, Visitor},
forward_to_deserialize_any, Deserialize, Deserializer, Serialize,
};
use uuid::Uuid;
pub mod contacts;
pub mod core;
pub mod sharing;
pub trait JmapExtension {
const EXTENSION: &'static str;
}
pub trait JmapDataExtension<D>: JmapExtension {
const ENDPOINT: &'static str;
}
pub trait JmapSessionCapabilityExtension: JmapExtension {
type Metadata: Serialize;
fn build(&self, user: Uuid) -> Self::Metadata;
}
pub trait JmapAccountCapabilityExtension: JmapExtension {
type Metadata: Serialize;
fn build(&self, user: Uuid, account: Uuid) -> Self::Metadata;
}
pub struct ExtensionRegistry {
pub core: core::Core,
pub contacts: contacts::Contacts,
pub sharing_principals: sharing::Principals,
pub sharing_principals_owner: sharing::PrincipalsOwner,
}
impl ExtensionRegistry {
pub fn build_session_capabilities(&self, user: Uuid) -> HashMap<Cow<'static, str>, Value> {
let mut out = HashMap::new();
out.insert(
Cow::Borrowed(core::Core::EXTENSION),
serde_json::to_value(JmapSessionCapabilityExtension::build(&self.core, user)).unwrap(),
);
out.insert(
Cow::Borrowed(sharing::Principals::EXTENSION),
serde_json::to_value(JmapSessionCapabilityExtension::build(
&self.sharing_principals,
user,
))
.unwrap(),
);
out
}
}
pub enum ConcreteData<'a> {
AddressBook(contacts::AddressBook),
Principal(proto_sharing::Principal<'a>),
ShareNotification(proto_sharing::ShareNotification<'a>),
}
impl<'a> ConcreteData<'a> {
pub fn parse(endpoint: &str, data: ResolvedArguments<'a>) -> Option<Self> {
match endpoint {
<contacts::Contacts as JmapDataExtension<contacts::AddressBook>>::ENDPOINT => {
Some(Self::AddressBook(Deserialize::deserialize(data).unwrap()))
}
<sharing::Principals as JmapDataExtension<proto_sharing::Principal>>::ENDPOINT => {
Some(Self::Principal(Deserialize::deserialize(data).unwrap()))
},
<sharing::Principals as JmapDataExtension<proto_sharing::ShareNotification>>::ENDPOINT => {
Some(Self::ShareNotification(Deserialize::deserialize(data).unwrap()))
},
_ => None,
}
}
}
pub struct ResolvedArguments<'a>(pub HashMap<Cow<'a, str>, Cow<'a, Value>>);
impl<'de> Deserializer<'de> for ResolvedArguments<'de> {
type Error = serde_json::Error;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_map(ResolvedArgumentsVisitor {
iter: self.0.into_iter(),
value: None,
})
}
forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
}
struct ResolvedArgumentsVisitor<'de> {
iter: <HashMap<Cow<'de, str>, Cow<'de, Value>> as IntoIterator>::IntoIter,
value: Option<Cow<'de, Value>>,
}
impl<'de> MapAccess<'de> for ResolvedArgumentsVisitor<'de> {
type Error = serde_json::Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: DeserializeSeed<'de>,
{
let Some((key, value)) = self.iter.next() else {
return Ok(None);
};
self.value = Some(value);
seed.deserialize(CowStrDeserializer::new(key)).map(Some)
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: DeserializeSeed<'de>,
{
let value = self
.value
.take()
.ok_or(serde::de::Error::custom("value is missing"))?;
match value {
Cow::Owned(v) => seed.deserialize(v),
Cow::Borrowed(v) => seed.deserialize(v),
}
}
}
@@ -1,0 +1,67 @@
use jmap_proto::{
common::Id,
extensions::sharing::{
Principal, PrincipalsAccountCapabilities, PrincipalsOwnerAccountCapabilities,
PrincipalsSessionCapabilities, ShareNotification,
},
};
use uuid::Uuid;
use crate::extensions::{
JmapAccountCapabilityExtension, JmapDataExtension, JmapExtension,
JmapSessionCapabilityExtension,
};
pub struct Principals {}
impl JmapExtension for Principals {
const EXTENSION: &'static str = "urn:ietf:params:jmap:principals";
}
impl JmapSessionCapabilityExtension for Principals {
type Metadata = PrincipalsSessionCapabilities;
fn build(&self, _user: Uuid) -> Self::Metadata {
PrincipalsSessionCapabilities {}
}
}
impl JmapAccountCapabilityExtension for Principals {
type Metadata = PrincipalsAccountCapabilities<'static>;
fn build(&self, _user: Uuid, _account: Uuid) -> Self::Metadata {
PrincipalsAccountCapabilities {
current_user_principal_id: None,
}
}
}
impl JmapDataExtension<Principal<'static>> for Principals {
const ENDPOINT: &'static str = "Principal";
}
impl JmapDataExtension<ShareNotification<'static>> for Principals {
const ENDPOINT: &'static str = "ShareNotification";
}
pub struct PrincipalsOwner {}
impl JmapExtension for PrincipalsOwner {
const EXTENSION: &'static str = "urn:ietf:params:jmap:principals:owner";
}
impl JmapAccountCapabilityExtension for PrincipalsOwner {
type Metadata = PrincipalsOwnerAccountCapabilities<'static>;
fn build(&self, _user: Uuid, _account: Uuid) -> Self::Metadata {
PrincipalsOwnerAccountCapabilities {
account_id_for_principal: Id("test".into()),
principal_id: Id("test".into()),
}
}
}
@@ -1,9 +1,13 @@
mod api;
mod oauth;
mod session;
use std::sync::Arc;
use axum::{routing::get, Router};
use axum::{
routing::{any, get},
Router,
};
use tower::layer::layer_fn;
use tower_cookies::CookieManagerLayer;
@@ -15,6 +19,7 @@
pub fn router(context: Arc<Context>) -> Router {
Router::new()
.route("/.well-known/jmap", get(session::get))
.route("/api/*", any(api::handle))
.layer(axum::middleware::from_fn_with_state(
context.clone(),
@@ -1,14 +1,12 @@
use std::{
collections::{BTreeSet, HashMap},
collections::HashMap,
sync::{Arc, OnceLock},
};
use axum::{extract::State, Extension, Json};
use jmap_proto::{
common::{Id, SessionState},
endpoints::session::{
Account, AccountCapabilities, CoreCapability, ServerCapabilities, Session,
},
endpoints::session::{Account, AccountCapabilities, Session},
};
use oxide_auth::primitives::grant::Grant;
@@ -66,18 +64,9 @@
);
Json(Session {
capabilities: ServerCapabilities {
core: CoreCapability {
max_size_upload: context.core_capabilities.max_size_upload.into(),
max_concurrent_upload: context.core_capabilities.max_concurrent_upload.into(),
max_size_request: context.core_capabilities.max_size_request.into(),
max_concurrent_requests: context.core_capabilities.max_concurrent_requests.into(),
max_calls_in_request: context.core_capabilities.max_calls_in_request.into(),
max_objects_in_get: context.core_capabilities.max_objects_in_get.into(),
max_objects_in_set: context.core_capabilities.max_objects_in_set.into(),
collation_algorithms: BTreeSet::default(),
},
},
capabilities: context
.extension_registry
.build_session_capabilities(user.id),
accounts,
primary_accounts: HashMap::default(),
username: username.into(),
@@ -1,0 +1,640 @@
use std::{borrow::Cow, collections::HashMap};
use chrono::NaiveDate;
use serde::{
ser::SerializeMap, Deserialize, Serialize, Serializer, __private::ser::FlatMapSerializer,
};
use serde_json::Value;
use crate::common::{Id, UnsignedInt, UtcDate};
#[derive(Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct TypeWrapper<T>(T);
impl<T: Serialize + TypedStruct> Serialize for TypeWrapper<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("@type", T::KIND)?;
self.0.serialize(FlatMapSerializer(&mut map))?;
map.end()
}
}
pub trait TypedStruct {
const KIND: &'static str;
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "PascalCase", tag = "@type")]
pub enum Data<'a> {
Card(#[serde(borrow)] Card<'a>),
CardGroup(CardGroup<'a>),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CardGroup<'a> {
#[serde(borrow)]
uid: Id<'a>,
members: HashMap<Id<'a>, bool>,
#[serde(default)]
name: Cow<'a, str>,
card: Option<Card<'a>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Card<'a> {
#[serde(borrow)]
uid: Id<'a>,
prod_id: Option<Cow<'a, str>>,
created: Option<UtcDate>,
updated: Option<UtcDate>,
kind: Option<CardKind>,
#[serde(default)]
related_to: HashMap<Id<'a>, TypeWrapper<Relation>>,
language: Option<Cow<'a, str>>,
#[serde(default)]
name: Vec<TypeWrapper<NameComponent<'a>>>,
#[serde(default)]
full_name: Cow<'a, str>,
#[serde(default)]
nick_names: Vec<Cow<'a, str>>,
#[serde(default)]
organizations: HashMap<Id<'a>, TypeWrapper<Organization<'a>>>,
#[serde(default)]
titles: HashMap<Id<'a>, TypeWrapper<Title<'a>>>,
#[serde(default)]
emails: HashMap<Id<'a>, TypeWrapper<EmailAddress<'a>>>,
#[serde(default)]
phones: HashMap<Id<'a>, TypeWrapper<Phone<'a>>>,
#[serde(default)]
online: HashMap<Id<'a>, TypeWrapper<Resource<'a>>>,
#[serde(default)]
photos: HashMap<Id<'a>, TypeWrapper<File<'a>>>,
preferred_contact_method: Option<PreferredContactMethod>,
#[serde(default)]
preferred_contact_languages: HashMap<String, TypeWrapper<ContactLanguage>>,
#[serde(default)]
address: HashMap<Id<'a>, TypeWrapper<Address<'a>>>,
#[serde(default)]
localizations: HashMap<Cow<'a, str>, Value>,
#[serde(default)]
anniversaries: HashMap<Id<'a>, TypeWrapper<Anniversary<'a>>>,
#[serde(default)]
personal_info: HashMap<Id<'a>, TypeWrapper<PersonalInfo<'a>>>,
#[serde(default)]
notes: Cow<'a, str>,
#[serde(default)]
categories: HashMap<Cow<'a, str>, bool>,
#[serde(default)]
time_zones: HashMap<Cow<'a, str>, Value>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PersonalInfo<'a> {
#[serde(rename = "type")]
type_: PersonalInfoType,
value: Cow<'a, str>,
level: Option<PersonalInfoLevel>,
}
impl TypedStruct for PersonalInfo<'_> {
const KIND: &'static str = "PersonalInfo";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoType {
Expertise,
Hobby,
Interest,
Other,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PersonalInfoLevel {
High,
Medium,
Low,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Anniversary<'a> {
#[serde(rename = "type")]
type_: AnniversaryType,
#[serde(default)]
label: Cow<'a, str>,
date: NaiveDate,
place: Option<Address<'a>>,
}
impl TypedStruct for Anniversary<'_> {
const KIND: &'static str = "Anniversary";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum AnniversaryType {
Birth,
Death,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Address<'a> {
#[serde(default)]
full_address: Cow<'a, str>,
#[serde(default)]
street: Vec<TypeWrapper<StreetComponent<'a>>>,
#[serde(default)]
locality: Cow<'a, str>,
#[serde(default)]
region: Cow<'a, str>,
#[serde(default)]
country: Cow<'a, str>,
#[serde(default)]
postcode: Cow<'a, str>,
#[serde(default)]
country_code: Cow<'a, str>,
#[serde(default)]
coordinates: Cow<'a, str>,
#[serde(default)]
time_zone: Cow<'a, str>,
#[serde(default)]
context: HashMap<AddressContext, bool>,
#[serde(default)]
label: Cow<'a, str>,
pref: Option<Preference>,
}
impl TypedStruct for Address<'_> {
const KIND: &'static str = "Address";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase", untagged)]
pub enum AddressContext {
Billing,
Postal,
Other(Context),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StreetComponent<'a> {
#[serde(rename = "type")]
type_: StreetComponentKind,
value: Cow<'a, str>,
}
impl TypedStruct for StreetComponent<'_> {
const KIND: &'static str = "StreetComponent";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum StreetComponentKind {
Name,
Number,
Apartment,
Room,
Extension,
Direction,
Building,
Floor,
PostOfficeBox,
Separator,
Unknown,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ContactLanguage {
context: Option<Context>,
pref: Option<Preference>,
}
impl TypedStruct for ContactLanguage {
const KIND: &'static str = "ContactLanguage";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PreferredContactMethod {
Emails,
Phones,
Online,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct File<'a> {
href: Cow<'a, str>,
media_type: Cow<'a, str>,
size: Option<UnsignedInt>,
pref: Option<Preference>,
}
impl TypedStruct for File<'_> {
const KIND: &'static str = "File";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Resource<'a> {
resource: Cow<'a, str>,
#[serde(rename = "type")]
type_: ResourceType,
#[serde(default)]
media_type: Cow<'a, str>,
#[serde(default)]
context: HashMap<Context, bool>,
#[serde(default)]
label: Cow<'a, str>,
pref: Option<Preference>,
}
impl TypedStruct for Resource<'_> {
const KIND: &'static str = "Resource";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ResourceType {
Uri,
Username,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Phone<'a> {
phone: Cow<'a, str>,
#[serde(default)]
features: HashMap<PhoneFeature, bool>,
#[serde(default)]
contexts: HashMap<Context, bool>,
#[serde(default)]
label: Cow<'a, str>,
pref: Option<Preference>,
}
impl TypedStruct for Phone<'_> {
const KIND: &'static str = "Phone";
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum PhoneFeature {
Voice,
Fax,
Pager,
Text,
Cell,
Textphone,
Video,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct EmailAddress<'a> {
email: Cow<'a, str>,
#[serde(default)]
contexts: HashMap<Context, bool>,
pref: Option<Preference>,
}
impl TypedStruct for EmailAddress<'_> {
const KIND: &'static str = "EmailAddress";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Preference(u8);
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Title<'a> {
#[serde(borrow)]
name: Cow<'a, str>,
#[serde(default)]
organization: Vec<Id<'a>>,
}
impl TypedStruct for Title<'_> {
const KIND: &'static str = "Title";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Organization<'a> {
name: Cow<'a, str>,
#[serde(default)]
units: Vec<Cow<'a, str>>,
}
impl TypedStruct for Organization<'_> {
const KIND: &'static str = "Organization";
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct NameComponent<'a> {
value: Cow<'a, str>,
#[serde(rename = "type")]
type_: NameComponentKind,
}
impl TypedStruct for NameComponent<'_> {
const KIND: &'static str = "NameComponent";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum NameComponentKind {
Prefix,
Personal,
Surname,
Additional,
Suffix,
Separator,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Relation {
relation: HashMap<RelationKind, bool>,
}
impl TypedStruct for Relation {
const KIND: &'static str = "Relation";
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Context {
Private,
Work,
Other,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum RelationKind {
Contact,
Acquaintance,
Friend,
Met,
CoWorker,
Colleague,
CoResident,
Neighbor,
Child,
Parent,
Sibling,
Spouse,
Kin,
Muse,
Crush,
Date,
Sweetheart,
Me,
Agent,
Emergency,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum CardKind {
Individual,
Org,
Location,
Device,
Application,
}
@@ -1,0 +1,1 @@
pub mod js_contact;
@@ -1,0 +1,100 @@
use std::{borrow::Cow, collections::HashMap, sync::Arc};
use axum::{body::Bytes, extract::State, Extension};
use jmap_proto::{
common::SessionState,
endpoints::{Argument, Arguments, Invocation, Request, Response},
errors::MethodError,
};
use oxide_auth::primitives::grant::Grant;
use crate::{
context::Context,
extensions::{ConcreteData, ResolvedArguments},
store::UserProvider,
};
pub async fn handle(
State(context): State<Arc<Context>>,
Extension(grant): Extension<Grant>,
body: Bytes,
) {
let payload: Request<'_> = serde_json::from_slice(&body).unwrap();
let username = grant.owner_id;
let user = context
.store
.get_by_username(&username)
.await
.unwrap()
.unwrap();
let session_state = context
.store
.fetch_seq_number_for_user(user.id)
.await
.unwrap();
let mut response = Response {
method_responses: Vec::with_capacity(payload.method_calls.len()),
created_ids: None,
session_state: SessionState(session_state.to_string().into()),
};
for invocation_request in payload.method_calls {
let Some(resolved_arguments) = resolve_arguments(&response, invocation_request.arguments)
else {
response.method_responses.push(
MethodError::InvalidResultReference.into_invocation(invocation_request.request_id),
);
continue;
};
let Some(_request) =
ConcreteData::parse(invocation_request.name.as_ref(), resolved_arguments)
else {
response
.method_responses
.push(MethodError::UnknownMethod.into_invocation(invocation_request.request_id));
continue;
};
response.method_responses.push(Invocation {
name: invocation_request.name,
arguments: Arguments(HashMap::new()),
request_id: invocation_request.request_id,
});
}
}
fn resolve_arguments<'a>(
response: &'a Response,
args: Arguments<'a>,
) -> Option<ResolvedArguments<'a>> {
let mut res = HashMap::with_capacity(args.0.len());
for (key, value) in args.0 {
let value = match value {
Argument::Reference(refer) => {
let referenced_response = response
.method_responses
.iter()
.find(|inv| inv.request_id == refer.result_of && inv.name == refer.name)?;
referenced_response.arguments.pointer(&refer.path)?
}
Argument::Absolute(value) => Cow::Owned(value),
};
res.insert(key, value);
}
Some(ResolvedArguments(res))
}