use std::collections::HashMap;
use chrono::Utc;
use serde_json::Value;
use crate::document::{trust_task_error_type_uri, ErrorResponse, TrustTask};
use crate::error::{ErrorPayload, RejectReason};
use crate::payload::Payload;
type BoxedHandler<R> = Box<dyn Fn(TrustTask<Value>) -> Result<R, RejectReason> + Send + Sync>;
pub struct Dispatcher<R> {
handlers: HashMap<String, BoxedHandler<R>>,
}
impl<R> Default for Dispatcher<R> {
fn default() -> Self {
Self::new()
}
}
impl<R> Dispatcher<R> {
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
pub fn on<P, F>(mut self, handler: F) -> Self
where
P: Payload + 'static,
F: Fn(TrustTask<P>) -> R + Send + Sync + 'static,
{
let wrapped = move |doc: TrustTask<Value>| -> Result<R, RejectReason> {
let typed = downcast_payload::<P>(doc)?;
Ok(handler(typed))
};
self.handlers
.insert(canonical_key(&P::type_uri()), Box::new(wrapped));
self
}
pub fn dispatch(&self, doc: TrustTask<Value>) -> Result<R, RejectReason> {
let key = canonical_key(&doc.type_uri);
match self.handlers.get(&key) {
Some(handler) => handler(doc),
None => Err(RejectReason::UnsupportedType { type_uri: key }),
}
}
pub fn registered_uris(&self) -> Vec<&str> {
let mut v: Vec<&str> = self.handlers.keys().map(String::as_str).collect();
v.sort_unstable();
v
}
#[allow(clippy::result_large_err)]
pub fn dispatch_or_reject(
&self,
doc: TrustTask<Value>,
error_id: impl Into<String>,
) -> Result<R, ErrorResponse> {
let id = doc.id.clone();
let thread_id = doc.thread_id.clone();
let issuer = doc.issuer.clone();
let recipient = doc.recipient.clone();
match self.dispatch(doc) {
Ok(value) => Ok(value),
Err(reason) => Err(build_error_response(
error_id.into(),
id,
thread_id,
issuer,
recipient,
ErrorPayload::from(reason),
)),
}
}
}
fn build_error_response(
error_id: String,
request_id: String,
request_thread_id: Option<String>,
request_issuer: Option<String>,
request_recipient: Option<String>,
payload: ErrorPayload,
) -> ErrorResponse {
let thread_id = request_thread_id.or(Some(request_id));
ErrorResponse {
id: error_id,
thread_id,
type_uri: trust_task_error_type_uri(),
issuer: request_recipient,
recipient: request_issuer,
issued_at: Some(Utc::now()),
expires_at: None,
payload,
context: None,
proof: None,
extra: Default::default(),
}
}
fn canonical_key(uri: &crate::type_uri::TypeUri) -> String {
uri.for_routing().to_string()
}
fn downcast_payload<P>(doc: TrustTask<Value>) -> Result<TrustTask<P>, RejectReason>
where
P: Payload,
{
let TrustTask {
id,
thread_id,
type_uri,
issuer,
recipient,
issued_at,
expires_at,
payload,
context,
proof,
extra,
} = doc;
let payload: P =
serde_json::from_value(payload).map_err(|e| RejectReason::MalformedRequest {
reason: format!("payload does not match {}: {e}", P::TYPE_URI),
})?;
Ok(TrustTask {
id,
thread_id,
type_uri,
issuer,
recipient,
issued_at,
expires_at,
payload,
context,
proof,
extra,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::specs::acl::grant::v0_1 as grant;
use crate::StandardCode;
fn payload() -> grant::Payload {
grant::Payload {
entry: grant::AclEntry {
subject: "did:web:alice.example".into(),
role: "admin".into(),
scopes: vec![],
label: None,
created_at: None,
created_by: None,
updated_at: None,
updated_by: None,
expires_at: None,
ext: None,
},
reason: None,
ext: None,
}
}
#[test]
fn dispatch_or_reject_unsupported_type_routes_back_to_original_issuer() {
let dispatcher: Dispatcher<()> = Dispatcher::new();
let mut doc = TrustTask::for_payload("req-9", payload());
doc.issuer = Some("did:web:org.example".into());
doc.recipient = Some("did:web:maintainer.example".into());
let doc_as_value = serde_json::to_value(&doc).unwrap();
let doc_as_value: TrustTask<Value> = serde_json::from_value(doc_as_value).unwrap();
let err = dispatcher
.dispatch_or_reject(doc_as_value, "err-9")
.unwrap_err();
assert_eq!(err.id, "err-9");
assert_eq!(err.thread_id.as_deref(), Some("req-9"));
assert_eq!(err.issuer.as_deref(), Some("did:web:maintainer.example"));
assert_eq!(err.recipient.as_deref(), Some("did:web:org.example"));
assert_eq!(err.payload.code, StandardCode::UnsupportedType.into());
}
#[test]
fn dispatch_or_reject_passes_handler_value_through() {
let dispatcher =
Dispatcher::<String>::new().on::<grant::Payload, _>(|_doc| "handled".to_string());
let mut doc = TrustTask::for_payload("req-10", payload());
doc.issuer = Some("did:web:org.example".into());
doc.recipient = Some("did:web:maintainer.example".into());
let doc_as_value: TrustTask<Value> =
serde_json::from_value(serde_json::to_value(&doc).unwrap()).unwrap();
let outcome = dispatcher
.dispatch_or_reject(doc_as_value, "err-10")
.unwrap();
assert_eq!(outcome, "handled");
}
}