Add extension routing
Diff
jogre-server/src/context.rs | 6 +++++-
jogre-server/src/extensions/contacts.rs | 6 +++++-
jogre-server/src/extensions/core.rs | 21 +++++++++++++++++++++
jogre-server/src/extensions/mod.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
jogre-server/src/extensions/router.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
jogre-server/src/extensions/sharing.rs | 8 +++++++-
jogre-server/src/methods/mod.rs | 2 +-
jogre-server/src/methods/api/mod.rs | 31 ++++++++++++++++++++++---------
8 files changed, 173 insertions(+), 43 deletions(-)
@@ -5,7 +5,7 @@
extensions,
extensions::{
sharing::{Principals, PrincipalsOwner},
ExtensionRegistry,
ExtensionRegistry, ExtensionRouterRegistry,
},
store::Store,
};
@@ -18,6 +18,7 @@
pub base_url: url::Url,
pub core_capabilities: CoreCapabilities,
pub extension_registry: ExtensionRegistry,
pub extension_router_registry: ExtensionRouterRegistry,
}
impl Context {
@@ -34,12 +35,15 @@
sharing_principals_owner: PrincipalsOwner {},
};
let extension_router_registry = extension_registry.build_router_registry();
Self {
oauth2: oauth2::OAuth2::new(store.clone(), derived_keys),
store,
base_url: config.base_url,
core_capabilities: config.core_capabilities,
extension_registry,
extension_router_registry,
}
}
}
@@ -1,14 +1,18 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::extensions::{JmapDataExtension, JmapExtension};
use crate::extensions::{router::ExtensionRouter, Get, JmapDataExtension, JmapExtension};
pub struct Contacts {}
impl JmapExtension for Contacts {
const EXTENSION: &'static str = "urn:ietf:params:jmap:contacts";
fn router(&self) -> ExtensionRouter<Self> {
ExtensionRouter::default().register(Get::<AddressBook>::default())
}
}
impl JmapDataExtension<AddressBook> for Contacts {
@@ -5,7 +5,9 @@
use crate::{
config::CoreCapabilities,
extensions::{JmapExtension, JmapSessionCapabilityExtension},
extensions::{
router::ExtensionRouter, JmapEndpoint, JmapExtension, JmapSessionCapabilityExtension,
},
};
#[derive(Clone)]
@@ -15,6 +17,10 @@
impl JmapExtension for Core {
const EXTENSION: &'static str = "urn:ietf:params:jmap:core";
fn router(&self) -> ExtensionRouter<Self> {
ExtensionRouter::default().register(Echo)
}
}
impl JmapSessionCapabilityExtension for Core {
@@ -31,5 +37,18 @@
max_objects_in_set: self.core_capabilities.max_objects_in_set.into(),
collation_algorithms: BTreeSet::default(),
}
}
}
pub struct Echo;
impl JmapEndpoint<Core> for Echo {
type Parameters<'de> = &'de serde_json::value::RawValue;
type Response<'s> = &'s serde_json::value::RawValue;
const ENDPOINT: &'static str = "echo";
fn handle<'de>(&self, _extension: &Core, params: Self::Parameters<'de>) -> Self::Response<'de> {
params
}
}
@@ -1,26 +1,64 @@
use std::{borrow::Cow, collections::HashMap};
use std::{borrow::Cow, collections::HashMap, marker::PhantomData};
use jmap_proto::{extensions::sharing as proto_sharing, Value};
use router::ExtensionRouter;
use serde::{
de::{value::CowStrDeserializer, DeserializeSeed, MapAccess, Visitor},
forward_to_deserialize_any, Deserialize, Deserializer, Serialize,
};
use serde_json::value::RawValue;
use uuid::Uuid;
pub mod contacts;
pub mod core;
pub mod router;
pub mod sharing;
pub trait JmapExtension {
pub trait JmapExtension: Sized {
const EXTENSION: &'static str;
fn router(&self) -> ExtensionRouter<Self> {
ExtensionRouter::default()
}
}
pub trait JmapDataExtension<D>: JmapExtension {
const ENDPOINT: &'static str;
}
pub struct Get<D> {
_phantom: PhantomData<fn(D)>,
}
impl<D> Default for Get<D> {
fn default() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<D, Ext: JmapDataExtension<D>> JmapEndpoint<Ext> for Get<D> {
type Parameters<'de> = ();
type Response<'s> = ();
const ENDPOINT: &'static str = "";
fn handle<'de>(&self, extension: &Ext, params: Self::Parameters<'de>) -> Self::Response<'de> {
todo!()
}
}
pub trait JmapEndpoint<E: JmapExtension> {
type Parameters<'de>: Deserialize<'de>;
type Response<'s>: Serialize + 's;
const ENDPOINT: &'static str;
fn handle<'de>(&self, extension: &E, params: Self::Parameters<'de>) -> Self::Response<'de>;
}
@@ -38,8 +76,30 @@
type Metadata: Serialize;
fn build(&self, user: Uuid, account: Uuid) -> Self::Metadata;
}
pub struct ExtensionRouterRegistry {
pub core: ExtensionRouter<core::Core>,
}
impl ExtensionRouterRegistry {
pub fn handle(
&self,
uri: &str,
registry: &ExtensionRegistry,
params: ResolvedArguments<'_>,
) -> Option<HashMap<String, Value>> {
let Some((namespace, uri)) = uri.split_once('/') else {
return None;
};
match namespace {
"Core" => self.core.handle(®istry.core, uri, params),
_ => None,
}
}
}
pub struct ExtensionRegistry {
pub core: core::Core,
@@ -66,32 +126,10 @@
);
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 fn build_router_registry(&self) -> ExtensionRouterRegistry {
ExtensionRouterRegistry {
core: self.core.router(),
}
}
}
@@ -1,0 +1,50 @@
use std::collections::HashMap;
use serde::Deserialize;
use serde_json::{value::RawValue, Value};
use crate::extensions::{JmapEndpoint, JmapExtension, ResolvedArguments};
pub struct ExtensionRouter<Ext: JmapExtension> {
routes: HashMap<&'static str, Box<dyn ErasedJmapEndpoint<Ext> + Send + Sync>>,
}
impl<Ext: JmapExtension> ExtensionRouter<Ext> {
pub fn register<E: JmapEndpoint<Ext> + Send + Sync + 'static>(mut self, endpoint: E) -> Self {
self.routes.insert(E::ENDPOINT, Box::new(endpoint));
self
}
pub fn handle(
&self,
extension: &Ext,
method: &str,
params: ResolvedArguments<'_>,
) -> Option<HashMap<String, Value>> {
Some(self.routes.get(method)?.handle(extension, params))
}
}
impl<Ext: JmapExtension> Default for ExtensionRouter<Ext> {
fn default() -> Self {
Self {
routes: HashMap::new(),
}
}
}
trait ErasedJmapEndpoint<Ext> {
fn handle(&self, endpoint: &Ext, params: ResolvedArguments<'_>) -> HashMap<String, Value>;
}
impl<Ext: JmapExtension, E: JmapEndpoint<Ext>> ErasedJmapEndpoint<Ext> for E {
fn handle(&self, endpoint: &Ext, params: ResolvedArguments<'_>) -> HashMap<String, Value> {
let res = <Self as JmapEndpoint<Ext>>::handle(
self,
endpoint,
Deserialize::deserialize(params).unwrap(),
);
serde_json::from_value(serde_json::to_value(res).unwrap()).unwrap()
}
}
@@ -8,7 +8,7 @@
use uuid::Uuid;
use crate::extensions::{
JmapAccountCapabilityExtension, JmapDataExtension, JmapExtension,
router::ExtensionRouter, Get, JmapAccountCapabilityExtension, JmapDataExtension, JmapExtension,
JmapSessionCapabilityExtension,
};
@@ -18,6 +18,12 @@
impl JmapExtension for Principals {
const EXTENSION: &'static str = "urn:ietf:params:jmap:principals";
fn router(&self) -> ExtensionRouter<Self> {
ExtensionRouter::default()
.register(Get::<Principal<'static>>::default())
.register(Get::<ShareNotification<'static>>::default())
}
}
impl JmapSessionCapabilityExtension for Principals {
@@ -19,7 +19,7 @@
pub fn router(context: Arc<Context>) -> Router {
Router::new()
.route("/.well-known/jmap", get(session::get))
.route("/api/*", any(api::handle))
.route("/api", any(api::handle))
.layer(axum::middleware::from_fn_with_state(
context.clone(),
@@ -8,11 +8,7 @@
};
use oxide_auth::primitives::grant::Grant;
use crate::{
context::Context,
extensions::{ConcreteData, ResolvedArguments},
store::UserProvider,
};
use crate::{context::Context, extensions::ResolvedArguments, store::UserProvider};
pub async fn handle(
State(context): State<Arc<Context>>,
@@ -55,20 +51,33 @@
continue;
};
let Some(_request) =
ConcreteData::parse(invocation_request.name.as_ref(), resolved_arguments)
else {
let arguments = if let Some(v) = context.extension_router_registry.handle(
invocation_request.name.as_ref(),
&context.extension_registry,
resolved_arguments,
) {
v.into_iter()
.map(|(k, v)| (Cow::Owned(k), Argument::Absolute(v)))
.collect()
} 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()),
arguments: Arguments(arguments),
request_id: invocation_request.request_id,
});
}