From 6bb4c8a0724bd81d226475003c9b62ad2660e0ac Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sun, 31 Dec 2023 03:10:11 +0000 Subject: [PATCH] Add extension routing --- 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(-) diff --git a/jogre-server/src/context.rs b/jogre-server/src/context.rs index 0cde0da..8831749 100644 --- a/jogre-server/src/context.rs +++ a/jogre-server/src/context.rs @@ -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, } } } diff --git a/jogre-server/src/extensions/contacts.rs b/jogre-server/src/extensions/contacts.rs index 8b5ecca..09573ba 100644 --- a/jogre-server/src/extensions/contacts.rs +++ a/jogre-server/src/extensions/contacts.rs @@ -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 { + ExtensionRouter::default().register(Get::::default()) + } } impl JmapDataExtension for Contacts { diff --git a/jogre-server/src/extensions/core.rs b/jogre-server/src/extensions/core.rs index c0a709c..451b8ea 100644 --- a/jogre-server/src/extensions/core.rs +++ a/jogre-server/src/extensions/core.rs @@ -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 { + 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 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 } } diff --git a/jogre-server/src/extensions/mod.rs b/jogre-server/src/extensions/mod.rs index 28a4b17..3007c75 100644 --- a/jogre-server/src/extensions/mod.rs +++ a/jogre-server/src/extensions/mod.rs @@ -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; /// Defines a base extension to the JMAP specification. -pub trait JmapExtension { +pub trait JmapExtension: Sized { /// A URI that describes this extension (eg. `urn:ietf:params:jmap:contacts`). const EXTENSION: &'static str; + + fn router(&self) -> ExtensionRouter { + ExtensionRouter::default() + } } /// Defines an extension that can handle reads/writes. pub trait JmapDataExtension: JmapExtension { /// Endpoint from which this data type is exposed from (ie. `ContactBook`). + const ENDPOINT: &'static str; +} + +pub struct Get { + _phantom: PhantomData, +} + +impl Default for Get { + fn default() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +impl> JmapEndpoint for Get { + 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 { + 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>; } /// Defines an extension which should be exposed via session capabilities. @@ -38,8 +76,30 @@ type Metadata: Serialize; fn build(&self, user: Uuid, account: Uuid) -> Self::Metadata; +} + +pub struct ExtensionRouterRegistry { + pub core: ExtensionRouter, } +impl ExtensionRouterRegistry { + pub fn handle( + &self, + uri: &str, + registry: &ExtensionRegistry, + params: ResolvedArguments<'_>, + ) -> Option> { + let Some((namespace, uri)) = uri.split_once('/') else { + return None; + }; + + match namespace { + "Core" => self.core.handle(®istry.core, uri, params), + _ => None, + } + } +} + /// Registry containing all extensions that can be handled by Jogre. pub struct ExtensionRegistry { pub core: core::Core, @@ -66,32 +126,10 @@ ); out } -} -/// Defines all the data types that can be handled by our [`JmapDataExtension`] -/// extensions. -pub enum ConcreteData<'a> { - AddressBook(contacts::AddressBook), - Principal(proto_sharing::Principal<'a>), - ShareNotification(proto_sharing::ShareNotification<'a>), -} - -impl<'a> ConcreteData<'a> { - /// Determines which extension should handle an incoming request by - /// the defined endpoint, and deserializes the request into the - /// relevant data type. - pub fn parse(endpoint: &str, data: ResolvedArguments<'a>) -> Option { - match endpoint { - >::ENDPOINT => { - Some(Self::AddressBook(Deserialize::deserialize(data).unwrap())) - } - >::ENDPOINT => { - Some(Self::Principal(Deserialize::deserialize(data).unwrap())) - }, - >::ENDPOINT => { - Some(Self::ShareNotification(Deserialize::deserialize(data).unwrap())) - }, - _ => None, + pub fn build_router_registry(&self) -> ExtensionRouterRegistry { + ExtensionRouterRegistry { + core: self.core.router(), } } } diff --git a/jogre-server/src/extensions/router.rs b/jogre-server/src/extensions/router.rs new file mode 100644 index 0000000..6363ca0 100644 --- /dev/null +++ a/jogre-server/src/extensions/router.rs @@ -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 { + routes: HashMap<&'static str, Box + Send + Sync>>, +} + +impl ExtensionRouter { + pub fn register + 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> { + Some(self.routes.get(method)?.handle(extension, params)) + } +} + +impl Default for ExtensionRouter { + fn default() -> Self { + Self { + routes: HashMap::new(), + } + } +} + +trait ErasedJmapEndpoint { + fn handle(&self, endpoint: &Ext, params: ResolvedArguments<'_>) -> HashMap; +} + +impl> ErasedJmapEndpoint for E { + fn handle(&self, endpoint: &Ext, params: ResolvedArguments<'_>) -> HashMap { + let res = >::handle( + self, + endpoint, + Deserialize::deserialize(params).unwrap(), + ); + + serde_json::from_value(serde_json::to_value(res).unwrap()).unwrap() + } +} diff --git a/jogre-server/src/extensions/sharing.rs b/jogre-server/src/extensions/sharing.rs index b206864..aac0cb9 100644 --- a/jogre-server/src/extensions/sharing.rs +++ a/jogre-server/src/extensions/sharing.rs @@ -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 { + ExtensionRouter::default() + .register(Get::>::default()) + .register(Get::>::default()) + } } impl JmapSessionCapabilityExtension for Principals { diff --git a/jogre-server/src/methods/mod.rs b/jogre-server/src/methods/mod.rs index 8910624..5b17cb5 100644 --- a/jogre-server/src/methods/mod.rs +++ a/jogre-server/src/methods/mod.rs @@ -19,7 +19,7 @@ pub fn router(context: Arc) -> Router { Router::new() .route("/.well-known/jmap", get(session::get)) - .route("/api/*", any(api::handle)) + .route("/api", any(api::handle)) // only apply auth requirement on endpoints above .layer(axum::middleware::from_fn_with_state( context.clone(), diff --git a/jogre-server/src/methods/api/mod.rs b/jogre-server/src/methods/api/mod.rs index ba2c847..d3bbd7c 100644 --- a/jogre-server/src/methods/api/mod.rs +++ a/jogre-server/src/methods/api/mod.rs @@ -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>, @@ -55,20 +51,33 @@ continue; }; - let Some(_request) = - ConcreteData::parse(invocation_request.name.as_ref(), resolved_arguments) - else { + // 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; + // }; + + 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; }; - - // TODO: call handler response.method_responses.push(Invocation { name: invocation_request.name, - arguments: Arguments(HashMap::new()), + arguments: Arguments(arguments), request_id: invocation_request.request_id, }); } -- rgit 0.1.3