nanorpc 0.1.12

a subset of JSON-RPC 2.0, with magical autogeneration of servers and clients
Documentation
#[doc = include_str!("../README.md")]
mod utils;
pub use utils::*;

use std::sync::Arc;

use async_trait::async_trait;
pub use nanorpc_derive::nanorpc_derive;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
/// A raw, JSON-RPC request ID. This should usually never be manually constructed.
pub enum JrpcId {
    Number(i64),
    String(String),
}

#[derive(Serialize, Deserialize, Clone, Debug)]
/// A raw, JSON-RPC request. This should usually never be manually constructed.
pub struct JrpcRequest {
    pub jsonrpc: String,
    pub method: String,
    pub params: Vec<serde_json::Value>,
    pub id: JrpcId,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
/// A raw, JSON-RPC response. This should usually never be manually constructed.
pub struct JrpcResponse {
    pub jsonrpc: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub result: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub error: Option<JrpcError>,
    pub id: JrpcId,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
/// A raw, JSON-RPC error. This should usually never be manually constructed.
pub struct JrpcError {
    pub code: i64,
    pub message: String,
    pub data: serde_json::Value,
}

/// A server-returned error message. Contains a string description as well as a structured value.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct ServerError {
    pub code: u32,
    pub message: String,
    pub details: serde_json::Value,
}

/// A struct implementing the [`RpcService`] represents the *server-side* logic of a NanoRPC. The method that needs to be *implemented* is [`RpcService::respond`], but actual servers would typically call [`RpcService::respond_raw`].
///
/// This trait uses the [`::async_trait`] crate, so the autogenerated documentation has somewhat inscrutable function signatures. [`RpcService`] has this "actual" definition:
///
/// ```
/// use nanorpc::{ServerError, JrpcRequest, JrpcResponse};
///
/// #[async_trait::async_trait]
/// pub trait RpcService {
///     async fn respond(
///         &self,
///         method: &str,
///         params: Vec<serde_json::Value>,
///     ) -> Option<Result<serde_json::Value, ServerError>>;
///
///     async fn respond_raw(&self, jrpc_req: JrpcRequest) -> JrpcResponse;
/// }
/// ```
///
///
/// # Examples
///
/// ## Using an RpcService to respond to client requests
///
/// ```
/// use nanorpc::{RpcService, ServerError, JrpcRequest, JrpcResponse};
///
/// /// Object that implements the business logic
/// struct BusinessLogic;
///
/// #[async_trait::async_trait]
/// impl RpcService for BusinessLogic {
///     async fn respond(&self,
///         method: &str,
///         params: Vec<serde_json::Value>
///     ) -> Option<Result<serde_json::Value, ServerError>> {
///         // business logic here
///         todo!()
///     }
/// }
///
/// /// Return the global BusinessLogic struct
/// fn bizlogic_singleton() -> &'static BusinessLogic { todo!() }
///
/// /// Handle a raw JSON-RPC request from, say, HTTP or TCP, returning the raw request
/// async fn handle_request(request: &[u8]) -> anyhow::Result<Vec<u8>> {
///     let request: JrpcRequest = serde_json::from_slice(request)?;
///     let response: JrpcResponse = bizlogic_singleton().respond_raw(request).await;
///     Ok(serde_json::to_vec(&response).unwrap())
/// }
#[async_trait]
pub trait RpcService: Sync + Send + 'static {
    /// Responds to an RPC call with method `str` and dynamically typed arguments `args`. The service should return `None` to indicate that this method does not exist at all.
    async fn respond(
        &self,
        method: &str,
        params: Vec<serde_json::Value>,
    ) -> Option<Result<serde_json::Value, ServerError>>;

    /// Responds to a raw JSON-RPC request, returning a raw JSON-RPC response.
    async fn respond_raw(&self, jrpc_req: JrpcRequest) -> JrpcResponse {
        if jrpc_req.jsonrpc != "2.0" {
            JrpcResponse {
                id: jrpc_req.id,
                jsonrpc: "2.0".into(),
                result: None,
                error: Some(JrpcError {
                    code: -32600,
                    message: "JSON-RPC version wrong".into(),
                    data: serde_json::Value::Null,
                }),
            }
        } else if let Some(response) = self.respond(&jrpc_req.method, jrpc_req.params).await {
            match response {
                Ok(response) => JrpcResponse {
                    id: jrpc_req.id,
                    jsonrpc: "2.0".into(),
                    result: Some(response),
                    error: None,
                },
                Err(err) => JrpcResponse {
                    id: jrpc_req.id,
                    jsonrpc: "2.0".into(),
                    result: None,
                    error: Some(JrpcError {
                        code: -1,
                        message: err.message,
                        data: err.details,
                    }),
                },
            }
        } else {
            JrpcResponse {
                id: jrpc_req.id,
                jsonrpc: "2.0".into(),
                result: None,
                error: Some(JrpcError {
                    code: -32601,
                    message: "Method not found".into(),
                    data: serde_json::Value::Null,
                }),
            }
        }
    }
}

#[async_trait]
impl<T: RpcService + ?Sized> RpcService for Arc<T> {
    async fn respond(
        &self,
        method: &str,
        params: Vec<serde_json::Value>,
    ) -> Option<Result<serde_json::Value, ServerError>> {
        self.as_ref().respond(method, params).await
    }
}

/// A client-side nanorpc transport. The only method that needs to be implemented is [`RpcTransport::call_raw`], but clients typically call [`RpcTransport::call`].
///
/// # Example
///
/// ```ignore
/// use nanorpc::RpcTransport;
///
/// let transport: impl RpcTransport = connect_to_server().await;
/// let three: u32 = serde_json::from_value(transport.call("add", &[1.into(), 2.into()]).await
///         .expect("transport failed")
///         .expect("no such verb")
///         .expect("server error"))
///     .expect("JSON decoding error");
/// assert_eq!(three, 3);
/// ```
#[async_trait]
pub trait RpcTransport: Sync + Send + 'static {
    /// This error type represents *transport-level* errors, like communication errors and such.
    type Error: Sync + Send + 'static;

    /// Sends an RPC call to the remote side, returning the result. `Ok(None)` means that there is no transport-level error, but that the verb does not exist. This generally does not need a manual implementation.
    async fn call(
        &self,
        method: &str,
        params: &[serde_json::Value],
    ) -> Result<Option<Result<serde_json::Value, ServerError>>, Self::Error> {
        let reqid = format!("req-{}", fastrand::u64(..));
        let req = JrpcRequest {
            jsonrpc: "2.0".into(),
            id: JrpcId::String(reqid),
            method: method.into(),
            params: params
                .iter()
                .map(|s| serde_json::to_value(s).unwrap())
                .collect(),
        };
        let result = self.call_raw(req).await?;
        if let Some(res) = result.result {
            Ok(Some(Ok(res)))
        } else if let Some(res) = result.error {
            if res.code == -32600 {
                Ok(None)
            } else {
                Ok(Some(Err(ServerError {
                    code: res.code as u32,
                    message: res.message,
                    details: res.data,
                })))
            }
        } else {
            // if both result and error are null, that means that the result is actually null and there is no error
            Ok(Some(Ok(serde_json::Value::Null)))
        }
    }

    /// Sends an RPC call to the remote side, as a raw JSON-RPC request, receiving a raw JSON-RPC response.
    async fn call_raw(&self, req: JrpcRequest) -> Result<JrpcResponse, Self::Error>;
}

#[async_trait]
impl<T: RpcTransport + ?Sized> RpcTransport for Arc<T> {
    type Error = T::Error;

    async fn call_raw(&self, req: JrpcRequest) -> Result<JrpcResponse, Self::Error> {
        self.as_ref().call_raw(req).await
    }
}

#[async_trait]
impl<T: RpcTransport + ?Sized> RpcTransport for Box<T> {
    type Error = T::Error;

    async fn call_raw(&self, req: JrpcRequest) -> Result<JrpcResponse, Self::Error> {
        self.as_ref().call_raw(req).await
    }
}

// #[async_trait]
// impl<T: RpcService + Sync> RpcTransport for T {
//     type Error = Infallible;

//     async fn call_raw(&self, req: JrpcRequest) -> Result<JrpcResponse, Self::Error> {
//         Ok(self.respond_raw(req).await)
//     }
// }

#[cfg(test)]
mod tests {
    use crate::{self as nanorpc, ServerError};
    use nanorpc::{nanorpc_derive, RpcService};

    #[nanorpc_derive]
    #[async_trait::async_trait]
    pub trait MathProtocol {
        /// Adds two numbers
        async fn add(&self, x: f64, y: f64) -> f64;
        /// Multiplies two numbers
        async fn mult(&self, x: f64, y: f64) -> f64;
        /// Maybe fails
        async fn maybe_fail(&self) -> Result<f64, f64>;
    }

    struct Mather;

    #[async_trait::async_trait]
    impl MathProtocol for Mather {
        async fn add(&self, x: f64, y: f64) -> f64 {
            x + y
        }

        async fn mult(&self, x: f64, y: f64) -> f64 {
            x * y
        }

        async fn maybe_fail(&self) -> Result<f64, f64> {
            Err(12345.0)
        }
    }

    #[test]
    fn test_notfound_macro() {
        smol::future::block_on(async move {
            let service = MathService(Mather);
            assert_eq!(
                service
                    .respond("!nonexistent!", serde_json::from_str("[]").unwrap())
                    .await,
                None
            );
        });
    }

    #[test]
    fn test_simple_macro() {
        smol::future::block_on(async move {
            let service = MathService(Mather);
            assert_eq!(
                service
                    .respond("maybe_fail", serde_json::from_str("[]").unwrap())
                    .await
                    .unwrap()
                    .unwrap_err(),
                ServerError {
                    code: 1,
                    message: "12345".into(),
                    details: 12345.0f64.into()
                }
            );
            assert_eq!(
                service
                    .respond("add", serde_json::from_str("[1, 2]").unwrap())
                    .await
                    .unwrap()
                    .unwrap(),
                serde_json::Value::from(3.0f64)
            );
        });
    }
}