maf 0.1.0-alpha.6

MAF is an authoritative realtime framework for writing simple, secure, and scalable apps.
Documentation
//! RPC (Remote Procedure Call) functionality for the application.
//!
//! It allows a MAF client to call functions on the server and receive responses.
//!
//! ## Defining RPC Functions
//! RPC functions are defined as regular Rust functions (async or sync) that take in parameters that
//! are *extractors* and return a value. Extractors are types that retrieve data from the RPC
//! request context, such as parameters, the app instance, the user making the request, or stores
//! (see below for more details). The return value can be any type that implements
//! [`serde::Serialize`]. The function must be registered with the MAF app using
//! [`crate::AppBuilder::rpc`] and given a unique method name.
//!
//! ```rust
//! use maf::prelude::*; // maf::prelude imports commonly used MAF types
//!
//! // An RPC function that adds a given amount to a counter store.
//! async fn add_counter(
//!     Params(amount): Params<i32>, // `Params<T>` extractor for input parameters
//!     counter: Store<Counter> // `Store<T>` extractor for accessing a store
//! ) -> i32 {
//!     let mut store = counter.write().await;
//!     store.count += amount;
//!     store.count
//! }
//!
//! async fn reset_counter(
//!     user: User, // `User` extractor for accessing the user making the request
//!     counter: Store<Counter>
//! ) {
//!     println!("User {} reset the counter", user.meta().id());
//!     let mut store = counter.write().await;
//!     store.count = 0;
//! }
//!
//! fn build() -> App {
//!     App::builder()
//!         .store::<Counter>()
//!         .rpc("add_counter", add_counter) // Register the add_counter RPC function
//!         .rpc("reset_counter", reset_counter) // Register the reset_counter RPC function
//! }
//!
//! maf::register!(build); // Register the app with MAF
//! ```
//!
//! ## Inputs to RPC Functions
//! An RPC function can accept parameters from callers in the form of [`Params<T>`], where `T` is
//! the type of the parameter. If multiple inputs are needed, they can be passed as a tuple, e.g.
//! `Params<(i32, String)>`. The parameters will be deserialized from the JSON request body, leaving
//! it up to the client to ensure the correct types are sent and are in the correct order.
//!
//! ## Return Values
//! The return value of an RPC function can be any type that implements [`serde::Serialize`]. The
//! response will be serialized to JSON and sent back to the client.
//!
//! ## Extractors
//! An RPC function declaration can take in additional parameters which can be used to access
//! additional MAF APIs:
//!
//! - [`crate::App`]: The app instance that the RPC function is running in.
//! - [`crate::User`]: The user that made the request.
//! - [`crate::Users`]: The user that made the request.
//! - [`crate::Store<T>`]: A store instance that can be used to access shared data.
//! - [`crate::Channel<T>`]: A channel instance that can be used to send messages to clients.

mod params;

#[cfg(feature = "typed")]
use std::sync::Arc;
use std::{any::TypeId, collections::HashMap};

use maf_schemas::packet::{TypedRpcRequestPacket, TypedRpcResponsePacket};
pub use params::Params;

use params::ParamsError;

use crate::{
    callable::{BoxedCallable, CallableFetch},
    platform::SendError,
    App, LocalStateError, User,
};

/// A type-erased RPC function handler.
pub struct RpcFunction {
    pub(crate) method: String,
    pub(crate) type_id: TypeId,
    pub(crate) handler: BoxedCallable<RpcRequestContext, TypedRpcResponsePacket, RpcError>,

    #[cfg(feature = "typed")]
    pub(crate) desc: Arc<
        dyn Fn(&mut schemars::SchemaGenerator) -> crate::typed::RpcDesc + Send + Sync + 'static,
    >,
}

impl std::fmt::Debug for RpcFunction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RpcFunction")
            .field("method", &self.method)
            .field("type_id", &self.type_id)
            .finish()
    }
}

/// A raw RPC request received from a client.
#[derive(Debug)]
pub struct RawRpcRequest {
    pub id: u32,
    pub user: User,
    pub data: Option<RpcRequestData>,
}

#[derive(Debug)]
pub enum RpcRequestData {
    /// JSON data that can have arbitrary structure.
    Typed(serde_json::Value),
}

pub struct RpcRequestContext {
    pub app: App,
    pub request: RawRpcRequest,
}

pub struct RpcRequestInit {
    pub method: String,
}

#[derive(Debug, Default)]
pub struct RpcStore {
    pub(crate) inner: HashMap<String, RpcFunction>,
}

#[derive(Debug, thiserror::Error)]
pub enum RpcError {
    #[error("rpc method `{0}` not found")]
    MethodNotFound(String),
    #[error("rpc function in `{0}` error: {1}")]
    FunctionError(String, anyhow::Error),

    #[error("rpc response serialization error: {0}")]
    ResponseSerializationError(#[from] serde_json::Error),
    #[error("rpc response error: {0}")]
    ResponseError(#[from] SendError),

    #[error("rpc params in `{0}` error: {1}")]
    ParamsError(String, ParamsError),

    #[error("other error: {0}")]
    Other(#[from] anyhow::Error),

    #[error("state error: {0}")]
    State(#[from] LocalStateError),

    #[error("infalliable error: {0}")]
    Infalliable(#[from] std::convert::Infallible),
}

impl RpcStore {
    pub fn add_rpc_function(&mut self, rpc_function: RpcFunction) {
        self.inner.insert(rpc_function.method.clone(), rpc_function);
    }

    pub async fn handle_typed_rpc_request(
        &self,
        app: App,
        user: &User,
        packet: TypedRpcRequestPacket,
    ) -> Result<TypedRpcResponsePacket, RpcError> {
        let method = packet.method;
        let rpc_function = self
            .inner
            .get(&method)
            .ok_or_else(|| RpcError::MethodNotFound(method))?;

        let request = RawRpcRequest {
            id: packet.id,
            user: user.clone(),
            data: Some(RpcRequestData::Typed(packet.params)),
        };

        let res = (rpc_function.handler)(RpcRequestContext { app, request });
        tokio::pin!(res);

        res.await
    }
}

impl CallableFetch<App> for RpcRequestContext {
    #[inline]
    fn fetch(&self) -> App {
        self.app.clone()
    }
}

impl CallableFetch<User> for RpcRequestContext {
    #[inline]
    fn fetch(&self) -> User {
        self.request.user.clone()
    }
}

#[cfg(test)]
mod tests {
    use crate::{callable::CallableParam, Store, StoreData};

    use super::*;

    #[test]
    fn type_checks() {
        const fn check_rpc_parameter<T: CallableParam<RpcRequestContext, RpcRequestInit>>() {}

        check_rpc_parameter::<Params<i32>>();
        check_rpc_parameter::<Params<(i32, String)>>();

        struct T {}

        impl StoreData for T {
            type Select<'this> = ();

            fn init() -> Self {
                T {}
            }

            fn select(&self, _user: &User) -> Self::Select<'_> {
                ()
            }
        }

        check_rpc_parameter::<Store<T>>();
    }
}