#![allow(clippy::result_large_err)]
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::Value;
use trust_tasks_https::status_for_code;
use trust_tasks_rs::{ErrorPayload, ErrorResponse, RejectReason, TrustTask, TypeUri};
use uuid::Uuid;
use crate::error::AppError;
pub(super) const TRANSPORT_TRUST_TASK: &str = "trust-task";
pub(crate) struct TrustTaskOutcome {
pub(crate) status: StatusCode,
pub(crate) body: Vec<u8>,
}
impl IntoResponse for TrustTaskOutcome {
fn into_response(self) -> Response {
(
self.status,
[(axum::http::header::CONTENT_TYPE, "application/json")],
self.body,
)
.into_response()
}
}
pub(super) fn parse_payload<T: serde::de::DeserializeOwned>(
doc: &TrustTask<Value>,
) -> Result<T, TrustTaskOutcome> {
serde_json::from_value::<T>(doc.payload.clone()).map_err(|e| {
reject_with(
doc,
RejectReason::MalformedRequest {
reason: format!("payload parse: {e}"),
},
)
})
}
pub(super) fn app_error_to_reject(doc: &TrustTask<Value>, err: AppError) -> TrustTaskOutcome {
let message = err.to_string();
let reason = match err {
AppError::Authentication(_) | AppError::Unauthorized(_) | AppError::Forbidden(_) => {
RejectReason::PermissionDenied { reason: message }
}
AppError::Validation(_) | AppError::TrustTaskMalformed(_) | AppError::InvalidCursor => {
RejectReason::MalformedRequest { reason: message }
}
AppError::NotFound(_) | AppError::Conflict(_) => RejectReason::TaskFailed {
reason: message,
details: None,
},
AppError::Internal(cause) => RejectReason::InternalError { reason: cause },
_ => RejectReason::InternalError { reason: message },
};
reject_with(doc, reason)
}
pub(super) fn reject_with(doc: &TrustTask<Value>, reason: RejectReason) -> TrustTaskOutcome {
let routed = doc.reject_with(format!("urn:uuid:{}", Uuid::new_v4()), reason);
error_response(routed)
}
pub(super) fn success_response<R: serde::Serialize>(
doc: &TrustTask<Value>,
payload: R,
) -> TrustTaskOutcome {
let response_doc = doc.respond_with(format!("urn:uuid:{}", Uuid::new_v4()), payload);
let body = match serde_json::to_vec(&response_doc) {
Ok(b) => b,
Err(e) => {
tracing::error!(error = %e, "failed to serialise success response doc");
return reject_with(
doc,
RejectReason::InternalError {
reason: format!("response serialisation: {e}"),
},
);
}
};
TrustTaskOutcome {
status: StatusCode::OK,
body,
}
}
#[allow(dead_code)]
pub(super) fn not_implemented_yet(doc: TrustTask<Value>, reason: &str) -> TrustTaskOutcome {
let reject = RejectReason::TaskFailed {
reason: reason.to_string(),
details: None,
};
let routed = doc.reject_with(format!("urn:uuid:{}", Uuid::new_v4()), reject);
error_response(routed)
}
pub(super) fn method_not_found(doc: TrustTask<Value>, type_uri: &str) -> TrustTaskOutcome {
let reject = RejectReason::UnsupportedType {
type_uri: type_uri.to_string(),
};
let routed = doc.reject_with(format!("urn:uuid:{}", Uuid::new_v4()), reject);
error_response(routed)
}
pub(super) fn error_response(err_doc: ErrorResponse) -> TrustTaskOutcome {
let status = StatusCode::from_u16(status_for_code(&err_doc.payload.code))
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body = serde_json::to_vec(&err_doc).unwrap_or_else(|_| Vec::new());
TrustTaskOutcome { status, body }
}
fn framework_error_type_uri() -> TypeUri {
"https://trusttasks.org/spec/trust-task-error/0.3"
.parse()
.expect("framework error Type URI parses")
}
pub(super) fn body_parse_error_response(reason: &str) -> TrustTaskOutcome {
let reject = RejectReason::MalformedRequest {
reason: format!("body did not parse as a Trust Task document: {reason}"),
};
let payload: ErrorPayload = reject.into();
let type_uri: TypeUri = framework_error_type_uri();
let err = ErrorResponse {
id: format!("urn:uuid:{}", Uuid::new_v4()),
thread_id: None,
parent_thread_id: None,
type_uri,
issuer: None,
recipient: None,
issued_at: Some(chrono::Utc::now()),
expires_at: None,
payload,
context: None,
ceremony: None,
proof: None,
extra: Default::default(),
};
error_response(err)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn doc() -> TrustTask<Value> {
let uri: TypeUri = vta_sdk::trust_tasks::TASK_WEBVH_DIDS_UPDATE_1_0
.parse()
.expect("update uri");
TrustTask::new("urn:uuid:test", uri, json!({}))
}
fn message_of(outcome: TrustTaskOutcome) -> String {
let doc: Value = serde_json::from_slice(&outcome.body).expect("error doc parses");
doc["payload"]["message"]
.as_str()
.expect("payload carries a message")
.to_string()
}
#[test]
fn an_internal_error_does_not_say_internal_error_twice() {
let message = message_of(app_error_to_reject(
&doc(),
AppError::Internal("log entry has no update_keys".into()),
));
assert_eq!(message, "internal error: log entry has no update_keys");
assert!(
!message.contains("internal error: internal error"),
"{message}"
);
}
#[test]
fn unrouted_and_routed_errors_agree_on_the_type_uri() {
let routed = doc().reject_with(
"urn:uuid:routed",
RejectReason::InternalError {
reason: "probe".into(),
},
);
assert_eq!(
framework_error_type_uri(),
routed.type_uri,
"the unrouted body-parse error names a different document type than \
the framework stamps on a routed rejection"
);
}
#[test]
fn the_body_parse_error_goes_out_as_a_framework_error_document() {
let outcome = body_parse_error_response("not json");
let doc: Value = serde_json::from_slice(&outcome.body).expect("error doc parses");
assert_eq!(
doc["type"].as_str().expect("type present"),
framework_error_type_uri().to_string()
);
}
#[test]
fn a_not_found_keeps_the_cause_the_operator_needs() {
let message = message_of(app_error_to_reject(
&doc(),
AppError::NotFound("SCID QmNope not found".into()),
));
assert!(message.contains("SCID QmNope not found"), "{message}");
}
}