anyclaw-sdk-service 0.2.1

SDK for building anyclaw service extensions
Documentation
//! Minimal JSON-RPC 2.0 types and NDJSON codec for the service harness.
//!
//! Private to this crate — avoids depending on the internal `anyclaw-jsonrpc`
//! crate which is `publish = false`. Only the subset needed by `ServiceHarness`
//! is defined here.

use bytes::{BufMut, BytesMut};
use serde::{Deserialize, Serialize};
use std::io;
use tokio_util::codec::{Decoder, Encoder};

const MAX_LINE_SIZE: usize = 32 * 1024 * 1024;

/// JSON-RPC 2.0 request/response id — String or Number.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub(crate) enum RequestId {
    /// Numeric id (most common in practice).
    Number(i64),
    /// String id (used by some implementations).
    String(String),
}

// D-03 extensible: params schema varies per JSON-RPC method
#[allow(clippy::disallowed_types)]
/// A JSON-RPC 2.0 request (or notification if `id` is `None`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct JsonRpcRequest {
    pub jsonrpc: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<RequestId>,
    pub method: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
}

// D-03 extensible: result schema varies per JSON-RPC method
#[allow(clippy::disallowed_types)]
/// A JSON-RPC 2.0 response (success or error).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct JsonRpcResponse {
    pub jsonrpc: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<RequestId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<JsonRpcError>,
}

// D-03 extensible: error data is implementation-defined
#[allow(clippy::disallowed_types)]
/// A JSON-RPC 2.0 error object.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct JsonRpcError {
    pub code: i64,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<serde_json::Value>,
}

/// Untagged enum that deserialises either a request or a response from a JSON-RPC line.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum JsonRpcMessage {
    /// A JSON-RPC request or notification (has `method` field).
    Request(JsonRpcRequest),
    /// A JSON-RPC response (has `result` or `error` field).
    Response(JsonRpcResponse),
}

impl JsonRpcResponse {
    #[allow(clippy::disallowed_types)]
    pub fn success(id: Option<RequestId>, result: serde_json::Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            result: Some(result),
            error: None,
        }
    }

    pub fn error(id: Option<RequestId>, error: JsonRpcError) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            id,
            result: None,
            error: Some(error),
        }
    }
}

/// NDJSON (newline-delimited JSON) codec for JSON-RPC 2.0 over stdio.
///
/// Framing protocol: one JSON object per line terminated by `\n`. The codec
/// skips empty lines, handles CRLF endings, silently drops non-JSON-RPC lines,
/// and enforces a 32 MB maximum line size to prevent unbounded memory growth.
pub(crate) struct NdJsonCodec;

impl NdJsonCodec {
    pub fn new() -> Self {
        Self
    }
}

impl Decoder for NdJsonCodec {
    type Item = JsonRpcMessage;
    type Error = io::Error;

    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
        loop {
            let newline_pos = src.iter().position(|b| *b == b'\n');
            match newline_pos {
                Some(pos) => {
                    let line = src.split_to(pos + 1);
                    let end = if pos > 0 && line[pos - 1] == b'\r' {
                        pos - 1
                    } else {
                        pos
                    };
                    let trimmed = &line[..end];
                    if trimmed.is_empty() {
                        continue;
                    }
                    if trimmed.len() > MAX_LINE_SIZE {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            format!(
                                "line of {} bytes exceeds {} byte limit",
                                trimmed.len(),
                                MAX_LINE_SIZE
                            ),
                        ));
                    }
                    match serde_json::from_slice::<JsonRpcMessage>(trimmed) {
                        Ok(msg) => return Ok(Some(msg)),
                        Err(_) => continue,
                    }
                }
                None => {
                    if src.len() > MAX_LINE_SIZE {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            format!(
                                "accumulated {} bytes without newline, exceeds {} byte limit",
                                src.len(),
                                MAX_LINE_SIZE
                            ),
                        ));
                    }
                    return Ok(None);
                }
            }
        }
    }
}

impl Encoder<JsonRpcResponse> for NdJsonCodec {
    type Error = io::Error;

    fn encode(&mut self, item: JsonRpcResponse, dst: &mut BytesMut) -> Result<(), Self::Error> {
        serde_json::to_writer((&mut *dst).writer(), &item)
            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
        dst.extend_from_slice(b"\n");
        Ok(())
    }
}