metactl 0.1.10

metactl v2 reference kernel and JSON-RPC service
Documentation
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::kernel::MetactlKernel;
use crate::types::{CompileParams, ExplainParams, ResolveParams, SearchParams, ValidateParams};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RpcRequestEnvelope {
    pub jsonrpc: String,
    pub id: Value,
    pub method: String,
    pub params: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RpcError {
    pub code: i64,
    pub message: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RpcResponseEnvelope {
    pub jsonrpc: String,
    pub id: Value,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result: Option<Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<RpcError>,
}

pub struct JsonRpcService<K> {
    kernel: K,
}

impl<K: MetactlKernel> JsonRpcService<K> {
    pub fn new(kernel: K) -> Self {
        Self { kernel }
    }

    pub fn dispatch_bytes(&self, raw: &[u8]) -> Result<Vec<u8>> {
        let response = match serde_json::from_slice::<RpcRequestEnvelope>(raw) {
            Ok(request) => self.dispatch(request),
            Err(err) => error_response(
                Value::Null,
                -32700,
                "parse error",
                Some(Value::String(err.to_string())),
            ),
        };
        Ok(serde_json::to_vec(&response)?)
    }

    pub fn dispatch(&self, request: RpcRequestEnvelope) -> RpcResponseEnvelope {
        if request.jsonrpc != "2.0" {
            return error_response(
                request.id,
                -32600,
                "invalid request",
                Some(json!("jsonrpc must be 2.0")),
            );
        }
        let id = request.id.clone();
        let handled: Result<Value> = match request.method.as_str() {
            "metactl.search" => decode_and_call(&request.params, |params: SearchParams| {
                self.kernel.search(params)
            }),
            "metactl.resolve" => decode_and_call(&request.params, |params: ResolveParams| {
                self.kernel.resolve(params)
            }),
            "metactl.explain" => decode_and_call(&request.params, |params: ExplainParams| {
                self.kernel.explain(params)
            }),
            "metactl.compile" => decode_and_call(&request.params, |params: CompileParams| {
                self.kernel.compile(params)
            }),
            "metactl.validate" => decode_and_call(&request.params, |params: ValidateParams| {
                self.kernel.validate(params)
            }),
            other => {
                return error_response(
                    id,
                    -32601,
                    "method not found",
                    Some(Value::String(format!("unknown method {other}"))),
                )
            }
        };
        match handled {
            Ok(result) => RpcResponseEnvelope {
                jsonrpc: "2.0".to_string(),
                id,
                result: Some(result),
                error: None,
            },
            Err(err) => {
                let detail = err.to_string();
                if detail.contains("decode") {
                    error_response(id, -32602, "invalid params", Some(Value::String(detail)))
                } else {
                    error_response(id, -32000, "application error", Some(Value::String(detail)))
                }
            }
        }
    }
}

fn decode_and_call<T, U, F>(params: &Value, f: F) -> Result<Value>
where
    T: for<'de> Deserialize<'de>,
    U: Serialize,
    F: FnOnce(T) -> Result<U>,
{
    let decoded = serde_json::from_value::<T>(params.clone()).context("decode params")?;
    let value = f(decoded)?;
    serde_json::to_value(value).map_err(|err| anyhow!(err))
}

fn error_response(id: Value, code: i64, message: &str, data: Option<Value>) -> RpcResponseEnvelope {
    RpcResponseEnvelope {
        jsonrpc: "2.0".to_string(),
        id,
        result: None,
        error: Some(RpcError {
            code,
            message: message.to_string(),
            data,
        }),
    }
}