chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
//! Transport-neutral routing between request shapes and handler outputs.
|
||||
|
||||
use crate::{
|
||||
handlers::{RpcHandlerContext, RpcHandlerRegistry},
|
||||
jsonrpc::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse},
|
||||
model::{RpcError, RpcMeta, RpcResultEnvelope, RpcValue},
|
||||
xmlrpc::{
|
||||
XmlRpcFault, XmlRpcMethodCall, XmlRpcMethodResponse, XmlRpcParam, rpc_value_to_xmlrpc,
|
||||
xmlrpc_value_to_rpc,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Transport-neutral inbound request variants accepted by the router.
|
||||
pub enum RpcDispatchRequest {
|
||||
/// `JSON-RPC` request expecting a response.
|
||||
Json(JsonRpcRequest),
|
||||
/// `JSON-RPC` notification with no response body.
|
||||
JsonNotification(JsonRpcNotification),
|
||||
/// `XML-RPC` method call expecting a response.
|
||||
Xml(XmlRpcMethodCall),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// Transport-neutral outbound response variants produced by the router.
|
||||
pub enum RpcDispatchResult {
|
||||
/// `JSON-RPC` response payload.
|
||||
Json(JsonRpcResponse),
|
||||
/// `XML-RPC` response payload.
|
||||
Xml(XmlRpcMethodResponse),
|
||||
/// No response should be emitted.
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
/// Shared router that normalizes transport-specific requests into handler calls.
|
||||
pub struct RpcRouter {
|
||||
/// Shared handler registry reused across `JSON-RPC` and `XML-RPC` dispatch paths.
|
||||
handlers: RpcHandlerRegistry,
|
||||
}
|
||||
|
||||
impl RpcRouter {
|
||||
#[must_use]
|
||||
/// Creates a router with the default handler registry.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
handlers: RpcHandlerRegistry,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns the shared handler registry.
|
||||
pub const fn handlers(&self) -> &RpcHandlerRegistry {
|
||||
&self.handlers
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns a mutable handler registry reference.
|
||||
pub const fn handlers_mut(&mut self) -> &mut RpcHandlerRegistry {
|
||||
&mut self.handlers
|
||||
}
|
||||
|
||||
/// Dispatches a transport-neutral request through the handler registry.
|
||||
pub fn dispatch(
|
||||
&mut self,
|
||||
request: RpcDispatchRequest,
|
||||
ctx: RpcHandlerContext,
|
||||
) -> RpcDispatchResult {
|
||||
match request {
|
||||
RpcDispatchRequest::Json(request) => {
|
||||
let request_id = request.id.clone();
|
||||
let envelope = self.handlers.handle_json(request, ctx);
|
||||
RpcDispatchResult::Json(envelope.into_response(request_id))
|
||||
}
|
||||
RpcDispatchRequest::JsonNotification(notification) => {
|
||||
self.handlers.handle_notification(notification, ctx);
|
||||
RpcDispatchResult::Empty
|
||||
}
|
||||
RpcDispatchRequest::Xml(request) => RpcDispatchResult::Xml(
|
||||
self.handlers
|
||||
.handle_json(json_request_from_xmlrpc(request), ctx)
|
||||
.into_xml_response(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcResultEnvelope {
|
||||
#[must_use]
|
||||
/// Converts an envelope into a `JSON-RPC` response with the provided id.
|
||||
pub fn into_response(self, id: Option<crate::jsonrpc::JsonRpcId>) -> JsonRpcResponse {
|
||||
let (result, error) = self.normalize_for_response();
|
||||
JsonRpcResponse {
|
||||
jsonrpc: Some("2.0".to_owned()),
|
||||
id,
|
||||
result,
|
||||
error,
|
||||
meta: RpcMeta::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Converts an envelope into an `XML-RPC` method response.
|
||||
pub fn into_xml_response(self) -> XmlRpcMethodResponse {
|
||||
let (result, error) = self.normalize_for_response();
|
||||
match error {
|
||||
Some(error) => {
|
||||
let message = error.message.clone();
|
||||
XmlRpcMethodResponse {
|
||||
value: None,
|
||||
fault: Some(XmlRpcFault {
|
||||
code: error.xml_fault_code(),
|
||||
message,
|
||||
error: Some(error),
|
||||
}),
|
||||
meta: RpcMeta::default(),
|
||||
}
|
||||
}
|
||||
None => XmlRpcMethodResponse {
|
||||
value: Some(rpc_value_to_xmlrpc(
|
||||
result.map_or(RpcValue::Null, |result| result),
|
||||
)),
|
||||
fault: None,
|
||||
meta: RpcMeta::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizes the envelope so transport encoders always see either a result or an error.
|
||||
fn normalize_for_response(self) -> (Option<RpcValue>, Option<RpcError>) {
|
||||
match (self.result, self.error) {
|
||||
(_, Some(error)) => (None, Some(error)),
|
||||
(Some(result), None) => (Some(result), None),
|
||||
(None, None) => (Some(RpcValue::Null), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a normalized error into an envelope with no successful result payload.
|
||||
impl From<RpcError> for RpcResultEnvelope {
|
||||
fn from(error: RpcError) -> Self {
|
||||
Self {
|
||||
result: None,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-expresses an `XML-RPC` method call as the equivalent `JSON-RPC` request shape.
|
||||
fn json_request_from_xmlrpc(request: XmlRpcMethodCall) -> JsonRpcRequest {
|
||||
JsonRpcRequest {
|
||||
jsonrpc: Some("2.0".to_owned()),
|
||||
id: None,
|
||||
method: request.method_name,
|
||||
params: request
|
||||
.params
|
||||
.into_iter()
|
||||
.map(|XmlRpcParam { value }| xmlrpc_value_to_rpc(value))
|
||||
.collect(),
|
||||
meta: request.meta,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Unit tests for the transport-neutral RPC router.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
jsonrpc::JsonRpcId,
|
||||
model::{RpcAuthContext, RpcErrorCode, RpcErrorKind},
|
||||
xmlrpc::XmlRpcValue,
|
||||
};
|
||||
|
||||
/// Builds a default handler context for router tests.
|
||||
fn ctx() -> RpcHandlerContext {
|
||||
RpcHandlerContext {
|
||||
auth: RpcAuthContext::default(),
|
||||
meta: RpcMeta::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that an empty envelope becomes a `null` `JSON-RPC` result.
|
||||
fn json_response_normalizes_empty_envelope_to_null_result() {
|
||||
let response = RpcResultEnvelope {
|
||||
result: None,
|
||||
error: None,
|
||||
}
|
||||
.into_response(Some(JsonRpcId::Number(7)));
|
||||
|
||||
assert_eq!(response.id, Some(JsonRpcId::Number(7)));
|
||||
assert_eq!(response.result, Some(RpcValue::Null));
|
||||
assert!(response.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that error payloads take precedence over successful results.
|
||||
fn json_response_prefers_error_over_result() {
|
||||
let response = RpcResultEnvelope {
|
||||
result: Some(RpcValue::String("ignored".to_owned())),
|
||||
error: Some(RpcError::invalid_params("bad params")),
|
||||
}
|
||||
.into_response(Some(JsonRpcId::String("req-1".to_owned())));
|
||||
|
||||
assert_eq!(response.id, Some(JsonRpcId::String("req-1".to_owned())));
|
||||
assert!(response.result.is_none());
|
||||
let error = response.error.expect("error should win");
|
||||
assert_eq!(error.code, RpcErrorCode::InvalidParams);
|
||||
assert_eq!(error.kind, RpcErrorKind::InvalidParams);
|
||||
assert_eq!(error.message, "bad params");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that an empty envelope becomes an `XML-RPC` nil value.
|
||||
fn xml_response_normalizes_empty_envelope_to_nil_value() {
|
||||
let response = RpcResultEnvelope {
|
||||
result: None,
|
||||
error: None,
|
||||
}
|
||||
.into_xml_response();
|
||||
|
||||
assert!(response.fault.is_none());
|
||||
assert_eq!(response.value, Some(XmlRpcValue::Nil));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that unknown `XML-RPC` methods reuse the shared fault shape.
|
||||
fn xml_dispatch_uses_shared_fault_shape_for_unknown_method() {
|
||||
let mut router = RpcRouter::new();
|
||||
let response = match router.dispatch(
|
||||
RpcDispatchRequest::Xml(XmlRpcMethodCall {
|
||||
method_name: "aria2.notFound".to_owned(),
|
||||
params: Vec::new(),
|
||||
meta: RpcMeta::default(),
|
||||
}),
|
||||
ctx(),
|
||||
) {
|
||||
RpcDispatchResult::Xml(response) => response,
|
||||
other => panic!("unexpected dispatch result: {other:?}"),
|
||||
};
|
||||
|
||||
assert!(response.value.is_none());
|
||||
let fault = response.fault.expect("fault expected");
|
||||
assert_eq!(fault.code, 1);
|
||||
assert_eq!(fault.message, "Method not found: aria2.notFound");
|
||||
assert_eq!(
|
||||
fault.error,
|
||||
Some(RpcError::unknown_method("aria2.notFound"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that `XML-RPC` dispatch reuses the shared JSON-backed handler results.
|
||||
fn xml_dispatch_reuses_json_handler_results_for_list_methods() {
|
||||
let mut router = RpcRouter::new();
|
||||
let response = match router.dispatch(
|
||||
RpcDispatchRequest::Xml(XmlRpcMethodCall {
|
||||
method_name: "system.listMethods".to_owned(),
|
||||
params: Vec::new(),
|
||||
meta: RpcMeta::default(),
|
||||
}),
|
||||
ctx(),
|
||||
) {
|
||||
RpcDispatchResult::Xml(response) => response,
|
||||
other => panic!("unexpected dispatch result: {other:?}"),
|
||||
};
|
||||
|
||||
assert!(response.fault.is_none());
|
||||
match response.value {
|
||||
Some(XmlRpcValue::Array(items)) => {
|
||||
assert!(!items.is_empty(), "listMethods should not be empty");
|
||||
assert!(items.iter().any(|item| matches!(
|
||||
item,
|
||||
XmlRpcValue::String(name) if name == "system.listMethods"
|
||||
)));
|
||||
}
|
||||
other => panic!("unexpected XML value: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user