actrpc-core 0.1.0

Core types and traits for ActRPC.
Documentation
use actrpc_core::{
    action::{ActionKind, RequestedActionRecord},
    error::{ActRpcError, ProtocolError},
    interception::{InterceptionRequest, InterceptionResponse, InterceptorContinuation},
    json_rpc::{
        JsonRpcError, JsonRpcErrorResponse, JsonRpcId, JsonRpcMessage, JsonRpcParams,
        JsonRpcRequest, JsonRpcResponse, JsonRpcSingleMessage, JsonRpcVersion,
    },
    participant::{Participant, ParticipantType},
};
use serde_json::json;

#[test]
fn test_interception_request_into_json_rpc_request() {
    let payload = InterceptionRequest {
        origin: Participant {
            kind: ParticipantType::User,
            id: "cli-123".to_string(),
        },
        message: JsonRpcMessage::Single(JsonRpcSingleMessage::Request(JsonRpcRequest {
            jsonrpc: JsonRpcVersion::V2_0,
            id: JsonRpcId::Number(99.into()),
            method: "subtract".to_string(),
            params: Some(JsonRpcParams::Array(vec![json!(10), json!(3)])),
        })),
        resolved_action_history: Default::default(),
    };

    let req: JsonRpcRequest = (JsonRpcId::Number(1.into()), payload.clone()).into();

    assert_eq!(req.jsonrpc, JsonRpcVersion::V2_0);
    assert_eq!(req.id, JsonRpcId::Number(1.into()));
    assert_eq!(req.method, "intercept");

    let (roundtrip_id, roundtrip_payload): (JsonRpcId, InterceptionRequest) =
        req.try_into().unwrap();

    assert_eq!(roundtrip_id, JsonRpcId::Number(1.into()));
    assert_eq!(roundtrip_payload, payload);
}

#[test]
fn test_json_rpc_request_try_into_interception_request_rejects_wrong_method() {
    let req = JsonRpcRequest {
        jsonrpc: JsonRpcVersion::V2_0,
        id: JsonRpcId::Number(1.into()),
        method: "not_intercept".to_string(),
        params: Some(JsonRpcParams::Object(serde_json::Map::new())),
    };

    let err = <(JsonRpcId, InterceptionRequest)>::try_from(req).unwrap_err();

    match err {
        ActRpcError::Protocol(ProtocolError::UnexpectedMethod { expected, actual }) => {
            assert_eq!(expected, "intercept");
            assert_eq!(actual, "not_intercept");
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn test_json_rpc_request_try_into_interception_request_rejects_missing_params() {
    let req = JsonRpcRequest {
        jsonrpc: JsonRpcVersion::V2_0,
        id: JsonRpcId::Number(1.into()),
        method: "intercept".to_string(),
        params: None,
    };

    let err = <(JsonRpcId, InterceptionRequest)>::try_from(req).unwrap_err();
    assert!(matches!(
        err,
        ActRpcError::Protocol(ProtocolError::InvalidRequestParams)
    ));
}

#[test]
fn test_json_rpc_request_try_into_interception_request_rejects_array_params() {
    let req = JsonRpcRequest {
        jsonrpc: JsonRpcVersion::V2_0,
        id: JsonRpcId::Number(1.into()),
        method: "intercept".to_string(),
        params: Some(JsonRpcParams::Array(vec![json!(1)])),
    };

    let err = <(JsonRpcId, InterceptionRequest)>::try_from(req).unwrap_err();
    assert!(matches!(
        err,
        ActRpcError::Protocol(ProtocolError::InvalidRequestParams)
    ));
}

#[test]
fn test_interception_response_into_json_rpc_response() {
    let payload = InterceptionResponse {
        actions: vec![RequestedActionRecord {
            kind: ActionKind::from("log"),
            params: Some(json!({ "message": "ok" })),
        }],
        continuation: InterceptorContinuation::Reinvoke,
    };

    let resp: JsonRpcResponse = (JsonRpcId::Number(5.into()), payload.clone()).into();

    let (roundtrip_id, roundtrip_payload): (JsonRpcId, InterceptionResponse) =
        resp.try_into().unwrap();

    assert_eq!(roundtrip_id, JsonRpcId::Number(5.into()));
    assert_eq!(roundtrip_payload, payload);
}

#[test]
fn test_json_rpc_response_try_into_interception_response_returns_remote_error() {
    let resp = JsonRpcResponse::Error(JsonRpcErrorResponse {
        jsonrpc: JsonRpcVersion::V2_0,
        id: JsonRpcId::Number(5.into()),
        error: JsonRpcError {
            code: -32000,
            message: "boom".to_string(),
            data: Some(json!({ "detail": "failed" })),
        },
    });

    let err = <(JsonRpcId, InterceptionResponse)>::try_from(resp).unwrap_err();

    match err {
        ActRpcError::RemoteJsonRpc(error) => {
            assert_eq!(error.code, -32000);
            assert_eq!(error.message, "boom");
            assert_eq!(error.data, Some(json!({ "detail": "failed" })));
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn test_json_rpc_response_try_into_interception_response_rejects_invalid_payload() {
    let resp = JsonRpcResponse::Success(actrpc_core::json_rpc::JsonRpcSuccessResponse {
        jsonrpc: JsonRpcVersion::V2_0,
        id: JsonRpcId::Number(5.into()),
        result: json!({ "actions": "not-an-array", "continuation": "stop" }),
    });

    let err = <(JsonRpcId, InterceptionResponse)>::try_from(resp).unwrap_err();

    match err {
        ActRpcError::Codec(_) => {}
        other => panic!("unexpected error: {other:?}"),
    }
}