use acdp_primitives::error::AcdpError;
use acdp_types::lifecycle::LifecycleEvent;
const BODY_FIELD_NAMES: &[&str] = &[
"body",
"ctx_id",
"lineage_id",
"origin_registry",
"created_at",
"content_hash",
"signature",
"version",
"supersedes",
"agent_id",
"contributors",
"title",
"type",
"data_refs",
"derived_from",
"visibility",
"audience",
"acdp_version",
"description",
"summary",
"tags",
"domain",
"expires_at",
"data_period",
"metadata",
"schema_uri",
];
pub fn parse_lifecycle_request(
raw: &serde_json::Value,
) -> Result<(LifecycleEvent, serde_json::Value), AcdpError> {
let map = raw.as_object().ok_or_else(|| {
AcdpError::SchemaViolation("lifecycle request body must be a JSON object".into())
})?;
for key in map.keys() {
if key == "event" {
continue;
}
if BODY_FIELD_NAMES.contains(&key.as_str()) {
return Err(AcdpError::ImmutableField(format!(
"lifecycle request member '{key}' attempts to supply or alter immutable \
body content — bodies are immutable and lifecycle endpoints mutate \
registry state only (RFC-ACDP-0013 §6 step 2)"
)));
}
return Err(AcdpError::SchemaViolation(format!(
"lifecycle request has unknown member '{key}' — the request body is a closed \
object with exactly one member, 'event' (RFC-ACDP-0013 §6)"
)));
}
let raw_event = map.get("event").ok_or_else(|| {
AcdpError::SchemaViolation(
"lifecycle request is missing the required 'event' member (RFC-ACDP-0013 §6)".into(),
)
})?;
let event = LifecycleEvent::from_value(raw_event)?;
Ok((event, raw_event.clone()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn valid_event() -> serde_json::Value {
json!({
"event_id": "018f6d0a-7b2e-4c4d-9e1f-3a5b7c9d1e2f",
"ctx_id": "acdp://registry.example.com/12345678-1234-4321-8123-123456781234",
"event_type": "retracted",
"occurred_at": "2026-07-04T09:15:42.000Z",
"actor": "did:web:agents.example.com:test-producer",
"reason": "underlying data source found to be fabricated",
"signature": {
"algorithm": "ed25519",
"key_id": "did:web:agents.example.com:test-producer#key-1",
"value": "AA=="
}
})
}
#[test]
fn valid_envelope_parses() {
let (event, raw) = parse_lifecycle_request(&json!({ "event": valid_event() })).unwrap();
assert_eq!(event.event_id, "018f6d0a-7b2e-4c4d-9e1f-3a5b7c9d1e2f");
assert_eq!(raw, valid_event());
}
#[test]
fn body_member_is_immutable_field() {
let err = parse_lifecycle_request(&json!({
"event": valid_event(),
"body": { "title": "Corrected title" }
}))
.unwrap_err();
assert!(matches!(err, AcdpError::ImmutableField(_)), "got {err:?}");
}
#[test]
fn body_field_named_member_is_immutable_field() {
let err = parse_lifecycle_request(&json!({
"event": valid_event(),
"summary": "please update the summary too"
}))
.unwrap_err();
assert!(matches!(err, AcdpError::ImmutableField(_)), "got {err:?}");
}
#[test]
fn non_body_unknown_member_is_schema_violation() {
let err = parse_lifecycle_request(&json!({
"event": valid_event(),
"note": "hello"
}))
.unwrap_err();
assert!(matches!(err, AcdpError::SchemaViolation(_)), "got {err:?}");
}
#[test]
fn missing_event_member_rejected() {
let err = parse_lifecycle_request(&json!({})).unwrap_err();
assert!(matches!(err, AcdpError::SchemaViolation(_)));
let err = parse_lifecycle_request(&json!([1, 2])).unwrap_err();
assert!(matches!(err, AcdpError::SchemaViolation(_)));
}
}