🏡 index : ~doyle/jogre.git

author Jordan Doyle <jordan@doyle.la> 2023-12-31 3:10:11.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-12-31 3:10:50.0 +00:00:00
commit
6bb4c8a0724bd81d226475003c9b62ad2660e0ac [patch]
tree
4eae88bfe7bc11c6e0eccae03e1224823f52c01d
parent
2d9b8082946fd808932afe89340092c88f907495
download
6bb4c8a0724bd81d226475003c9b62ad2660e0ac.tar.gz

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(-)

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<Self> {
        ExtensionRouter::default().register(Get::<AddressBook>::default())
    }
}

impl JmapDataExtension<AddressBook> 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<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
    }
}
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<Self> {
        ExtensionRouter::default()
    }
}

/// Defines an extension that can handle reads/writes.

pub trait JmapDataExtension<D>: JmapExtension {
    /// Endpoint from which this data type is exposed from (ie. `ContactBook`).

    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>;
}

/// 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<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(&registry.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<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(),
        }
    }
}
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<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()
    }
}
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<Self> {
        ExtensionRouter::default()
            .register(Get::<Principal<'static>>::default())
            .register(Get::<ShareNotification<'static>>::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<Context>) -> 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<Arc<Context>>,
@@ -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,
        });
    }