use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpdateDidWebvhBody {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub document: Option<Value>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "pre_rotation_count"
)]
pub pre_rotation_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub witnesses: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watchers: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "expected_version_id"
)]
pub expected_version_id: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct RotateDidWebvhKeysBody {
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "pre_rotation_count"
)]
pub pre_rotation_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpdateDidWebvhResultBody {
pub did: String,
#[serde(alias = "new_version_id")]
pub new_version_id: String,
#[serde(alias = "new_scid")]
pub new_scid: String,
#[serde(alias = "new_log_entry")]
pub new_log_entry: String,
#[serde(alias = "update_keys_count")]
pub update_keys_count: u32,
#[serde(alias = "pre_rotation_key_count")]
pub pre_rotation_key_count: u32,
#[serde(default)]
pub serverless: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn update_body_round_trips_minimal() {
let body = UpdateDidWebvhBody::default();
let json = serde_json::to_string(&body).unwrap();
let restored: UpdateDidWebvhBody = serde_json::from_str(&json).unwrap();
assert!(restored.document.is_none());
assert!(restored.pre_rotation_count.is_none());
}
#[test]
fn update_body_round_trips_full() {
let body = UpdateDidWebvhBody {
document: Some(serde_json::json!({"id": "did:webvh:abc"})),
pre_rotation_count: Some(2),
witnesses: None,
watchers: Some(vec!["https://watcher.example.com".into()]),
ttl: Some(3600),
label: Some("rotate after audit".into()),
expected_version_id: None,
};
let json = serde_json::to_string(&body).unwrap();
let restored: UpdateDidWebvhBody = serde_json::from_str(&json).unwrap();
assert_eq!(restored.pre_rotation_count, Some(2));
assert_eq!(restored.ttl, Some(3600));
assert_eq!(restored.label.as_deref(), Some("rotate after audit"));
}
#[test]
fn update_body_expected_version_id_round_trips_and_defaults_none() {
let body: UpdateDidWebvhBody =
serde_json::from_str(r#"{"document":{"id":"did:webvh:abc"}}"#).unwrap();
assert!(body.expected_version_id.is_none());
let body = UpdateDidWebvhBody {
expected_version_id: Some("2-QmHash".into()),
..Default::default()
};
let json = serde_json::to_string(&body).unwrap();
let restored: UpdateDidWebvhBody = serde_json::from_str(&json).unwrap();
assert_eq!(restored.expected_version_id.as_deref(), Some("2-QmHash"));
}
#[test]
fn rotate_body_round_trips() {
let body = RotateDidWebvhKeysBody {
pre_rotation_count: Some(3),
label: Some("scheduled".into()),
};
let json = serde_json::to_string(&body).unwrap();
let restored: RotateDidWebvhKeysBody = serde_json::from_str(&json).unwrap();
assert_eq!(restored.pre_rotation_count, Some(3));
}
#[test]
fn result_body_round_trips() {
let r = UpdateDidWebvhResultBody {
did: "did:webvh:abc".into(),
new_version_id: "3-zVer".into(),
new_scid: "abc".into(),
new_log_entry: "{\"versionId\":\"3-...\"}".into(),
update_keys_count: 1,
pre_rotation_key_count: 2,
serverless: true,
};
let json = serde_json::to_string(&r).unwrap();
let restored: UpdateDidWebvhResultBody = serde_json::from_str(&json).unwrap();
assert_eq!(restored.update_keys_count, 1);
assert_eq!(restored.new_version_id, "3-zVer");
assert!(restored.serverless);
}
#[test]
fn result_body_serverless_defaults_to_false_when_absent() {
let legacy = r#"{
"did": "did:webvh:abc",
"new_version_id": "3-zVer",
"new_scid": "abc",
"new_log_entry": "{}",
"update_keys_count": 1,
"pre_rotation_key_count": 2
}"#;
let r: UpdateDidWebvhResultBody = serde_json::from_str(legacy).unwrap();
assert!(!r.serverless);
}
}
#[cfg(test)]
mod casing_tests {
use super::*;
#[test]
fn an_unset_field_is_absent_from_the_wire_not_null() {
let partial = UpdateDidWebvhBody {
label: Some("resync".into()),
..Default::default()
};
let json = serde_json::to_value(&partial).expect("serialises");
assert_eq!(
json,
serde_json::json!({"label": "resync"}),
"a partial update must carry only the fields that were set"
);
let obj = json.as_object().expect("object");
assert!(
obj.values().all(|v| !v.is_null()),
"no member may serialise as null: {json}"
);
assert_eq!(
serde_json::to_value(UpdateDidWebvhBody::default()).expect("serialises"),
serde_json::json!({}),
);
assert_eq!(
serde_json::to_value(RotateDidWebvhKeysBody {
label: Some("resync".into()),
..Default::default()
})
.expect("serialises"),
serde_json::json!({"label": "resync"}),
);
}
#[test]
fn a_set_field_still_reaches_the_wire() {
let body = UpdateDidWebvhBody {
pre_rotation_count: Some(0),
watchers: Some(vec![]),
expected_version_id: Some("1-QmPrior".into()),
..Default::default()
};
let json = serde_json::to_value(&body).expect("serialises");
assert_eq!(
json,
serde_json::json!({
"preRotationCount": 0,
"watchers": [],
"expectedVersionId": "1-QmPrior",
}),
);
}
#[test]
fn the_concurrency_precondition_is_read_from_the_wire() {
let camel: UpdateDidWebvhBody =
serde_json::from_str(r#"{"expectedVersionId":"3-QmPrior"}"#).expect("camelCase parses");
assert_eq!(
camel.expected_version_id.as_deref(),
Some("3-QmPrior"),
"the framework's camelCase is the wire form and MUST reach the field"
);
let snake: UpdateDidWebvhBody =
serde_json::from_str(r#"{"expected_version_id":"3-QmPrior"}"#)
.expect("snake_case still parses");
assert_eq!(snake.expected_version_id.as_deref(), Some("3-QmPrior"));
}
#[test]
fn pre_rotation_count_reads_from_either_casing() {
let camel: UpdateDidWebvhBody =
serde_json::from_str(r#"{"preRotationCount":2}"#).expect("camelCase");
assert_eq!(camel.pre_rotation_count, Some(2));
let snake: UpdateDidWebvhBody =
serde_json::from_str(r#"{"pre_rotation_count":2}"#).expect("snake_case");
assert_eq!(snake.pre_rotation_count, Some(2));
let rotate: RotateDidWebvhKeysBody =
serde_json::from_str(r#"{"preRotationCount":3}"#).expect("camelCase");
assert_eq!(rotate.pre_rotation_count, Some(3));
}
#[test]
fn single_word_members_are_unaffected() {
let b: UpdateDidWebvhBody =
serde_json::from_str(r#"{"document":{"id":"did:webvh:x"},"ttl":600,"label":"l"}"#)
.expect("parses");
assert!(b.document.is_some());
assert_eq!(b.ttl, Some(600));
assert_eq!(b.label.as_deref(), Some("l"));
}
}