use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeleteContextResultBody {
pub id: String,
pub deleted: bool,
#[serde(
default,
alias = "daemon_cleanup_errors",
skip_serializing_if = "Vec::is_empty"
)]
pub daemon_cleanup_errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DeleteContextPreviewBody {
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct DeleteContextPreviewResultBody {
pub id: String,
#[serde(default, alias = "sub_contexts", skip_serializing_if = "Vec::is_empty")]
pub sub_contexts: Vec<String>,
pub keys: Vec<String>,
#[serde(alias = "webvh_dids")]
pub webvh_dids: Vec<String>,
#[serde(alias = "acl_entries_removed")]
pub acl_entries_removed: Vec<String>,
#[serde(alias = "acl_entries_updated")]
pub acl_entries_updated: Vec<String>,
#[serde(default, alias = "did_templates")]
pub did_templates: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostCleanupFailure {
pub did: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
pub reason: String,
}
impl std::fmt::Display for HostCleanupFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.did, self.reason)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum HostCleanupRefusal {
Evidenced(Vec<HostCleanupFailure>),
Unreadable { why: String },
}
pub fn host_cleanup_refusal(err: &crate::error::VtaError) -> Option<HostCleanupRefusal> {
use vti_specs::vta::contexts::delete::v1_1::error_codes::HOST_CLEANUP_FAILED;
let crate::error::VtaError::Rejected { code, details, .. } = err else {
return None;
};
if code != HOST_CLEANUP_FAILED.code {
return None;
}
Some(match read_failures(details.as_ref()) {
Ok(failures) => HostCleanupRefusal::Evidenced(failures),
Err(why) => HostCleanupRefusal::Unreadable { why },
})
}
fn read_failures(details: Option<&serde_json::Value>) -> Result<Vec<HostCleanupFailure>, String> {
let raw = details
.ok_or("the refusal carried no details")?
.get("hostCleanupFailures")
.ok_or("the refusal's details carried no `hostCleanupFailures`")?;
let failures: Vec<HostCleanupFailure> = serde_json::from_value(raw.clone())
.map_err(|e| format!("`hostCleanupFailures` is malformed: {e}"))?;
if failures.is_empty() {
return Err(
"`hostCleanupFailures` is empty; the specification requires at least one entry".into(),
);
}
for (i, f) in failures.iter().enumerate() {
if f.did.is_empty() {
return Err(format!("`hostCleanupFailures[{i}].did` is empty"));
}
if f.reason.is_empty() {
return Err(format!("`hostCleanupFailures[{i}].reason` is empty"));
}
if f.server.as_deref() == Some("") {
return Err(format!("`hostCleanupFailures[{i}].server` is empty"));
}
}
Ok(failures)
}
#[deprecated(note = "use `host_cleanup_refusal`, which reports unreadable evidence separately")]
pub fn host_cleanup_failures(err: &crate::error::VtaError) -> Option<Vec<HostCleanupFailure>> {
match host_cleanup_refusal(err)? {
HostCleanupRefusal::Evidenced(failures) => Some(failures),
HostCleanupRefusal::Unreadable { .. } => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::VtaError;
use serde_json::{Value, json};
use vti_specs::vta::contexts::delete::v1_1::error_codes::HOST_CLEANUP_FAILED;
fn refusal(code: &str, details: Option<Value>) -> VtaError {
VtaError::Rejected {
code: code.to_string(),
message: "host cleanup failed".into(),
details,
}
}
fn declared(details: Option<Value>) -> VtaError {
refusal(HOST_CLEANUP_FAILED.code, details)
}
fn unreadable(err: &VtaError) -> bool {
matches!(
host_cleanup_refusal(err),
Some(HostCleanupRefusal::Unreadable { .. })
)
}
#[test]
fn well_formed_details_are_evidence() {
let err = declared(Some(json!({ "hostCleanupFailures": [
{ "did": "did:webvh:a", "server": "host-1", "reason": "unreachable" },
{ "did": "did:webvh:b", "reason": "refused" },
]})));
let Some(HostCleanupRefusal::Evidenced(failures)) = host_cleanup_refusal(&err) else {
panic!("expected evidence");
};
assert_eq!(failures.len(), 2);
assert_eq!(failures[0].server.as_deref(), Some("host-1"));
assert_eq!(failures[1].did, "did:webvh:b");
}
#[test]
fn an_unknown_member_does_not_hide_the_dids() {
let err = declared(Some(json!({ "hostCleanupFailures": [
{ "did": "did:webvh:a", "reason": "unreachable", "retryAt": "later" },
]})));
assert!(matches!(
host_cleanup_refusal(&err),
Some(HostCleanupRefusal::Evidenced(f)) if f.len() == 1
));
}
#[test]
fn absent_details_are_not_evidence() {
assert!(unreadable(&declared(None)));
assert!(unreadable(&declared(Some(json!({})))));
}
#[test]
fn an_empty_list_is_not_evidence() {
assert!(unreadable(&declared(Some(
json!({ "hostCleanupFailures": [] })
))));
}
#[test]
fn malformed_entries_are_not_an_empty_list() {
for bad in [
json!({ "hostCleanupFailures": [{ "reason": "unreachable" }] }),
json!({ "hostCleanupFailures": [{ "did": "did:webvh:a" }] }),
json!({ "hostCleanupFailures": [{ "did": "", "reason": "x" }] }),
json!({ "hostCleanupFailures": [{ "did": "did:webvh:a", "reason": "" }] }),
json!({ "hostCleanupFailures": [{ "did": "did:webvh:a", "reason": "x", "server": "" }] }),
json!({ "hostCleanupFailures": "did:webvh:a" }),
json!({ "hostCleanupFailures": [ "did:webvh:a" ] }),
] {
let err = declared(Some(bad.clone()));
assert!(unreadable(&err), "{bad} read as evidence");
#[allow(deprecated)]
let legacy = host_cleanup_failures(&err);
assert_eq!(
legacy, None,
"{bad} read as a list by the deprecated helper"
);
}
}
#[test]
fn another_refusal_is_not_this_one_whatever_it_says() {
let details = Some(json!({ "hostCleanupFailures": [
{ "did": "did:webvh:a", "reason": "unreachable" },
]}));
assert_eq!(
host_cleanup_refusal(&refusal("taskFailed", details.clone())),
None
);
assert_eq!(
host_cleanup_refusal(&refusal("vta/contexts/delete:notEmpty", details)),
None
);
assert_eq!(
host_cleanup_refusal(&VtaError::Protocol(
"vta/contexts/delete:hostCleanupFailed".into()
)),
None
);
}
}