chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 14:16:57 +08:00
commit 17688c3e34
321 changed files with 76859 additions and 0 deletions
+280
View File
@@ -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:?}"),
}
}
}