use crate::body::{DataPeriod, Signature};
use crate::data_ref::DataRef;
use crate::serde_helpers::de_present;
use acdp_primitives::primitives::*;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PublishRequest {
pub version: u32,
pub supersedes: Option<CtxId>,
pub agent_id: AgentDid,
pub contributors: Vec<AgentDid>,
pub title: String,
#[serde(rename = "type")]
pub context_type: ContextType,
pub data_refs: Vec<DataRef>,
pub derived_from: Vec<CtxId>,
pub visibility: Visibility,
pub content_hash: ContentHash,
pub signature: Signature,
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<AgentDid>>,
#[serde(
default,
deserialize_with = "de_present",
skip_serializing_if = "Option::is_none"
)]
pub acdp_version: Option<String>,
#[serde(
default,
deserialize_with = "de_present",
skip_serializing_if = "Option::is_none"
)]
pub description: Option<String>,
#[serde(
default,
deserialize_with = "de_present",
skip_serializing_if = "Option::is_none"
)]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lineage_id: Option<LineageId>,
#[serde(
default,
deserialize_with = "de_present",
skip_serializing_if = "Option::is_none"
)]
pub tags: Option<Vec<String>>,
#[serde(
default,
deserialize_with = "de_present",
skip_serializing_if = "Option::is_none"
)]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_period: Option<DataPeriod>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(
default,
deserialize_with = "de_present",
skip_serializing_if = "Option::is_none"
)]
pub schema_uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PublishResponse {
pub ctx_id: CtxId,
pub lineage_id: LineageId,
pub version: u32,
pub created_at: DateTime<Utc>,
pub status: Status,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry_receipt: Option<serde_json::Value>,
}
pub use acdp_primitives::wire_error::{WireError, WireErrorBody};
#[cfg(test)]
mod tests {
use super::*;
fn minimal_request_with_extra(extra: &str) -> String {
format!(
r#"{{
"version": 1,
"agent_id": "did:web:agents.example.com:test",
"contributors": [],
"title": "t",
"type": "data_snapshot",
"data_refs": [],
"derived_from": [],
"visibility": "public",
"content_hash": "sha256:0",
"signature": {{
"algorithm": "ed25519",
"key_id": "did:web:agents.example.com:test#key-1",
"value": "{sig}"
}}{extra}
}}"#,
sig = "A".repeat(88),
extra = extra
)
}
#[test]
fn extra_top_level_field_is_rejected() {
let body = minimal_request_with_extra(r#", "ctx_id": "acdp://r/x""#);
let res: Result<PublishRequest, _> = serde_json::from_str(&body);
assert!(res.is_err(), "ctx_id in publish request must be rejected");
}
#[test]
fn extra_origin_registry_field_is_rejected() {
let body = minimal_request_with_extra(r#", "origin_registry": "did:web:r.x""#);
let res: Result<PublishRequest, _> = serde_json::from_str(&body);
assert!(res.is_err());
}
#[test]
fn extra_created_at_field_is_rejected() {
let body = minimal_request_with_extra(r#", "created_at": "2026-01-01T00:00:00.000Z""#);
let res: Result<PublishRequest, _> = serde_json::from_str(&body);
assert!(res.is_err());
}
#[test]
fn arbitrary_unknown_field_is_rejected() {
let body = minimal_request_with_extra(r#", "noodle": 42"#);
let res: Result<PublishRequest, _> = serde_json::from_str(&body);
assert!(res.is_err());
}
#[test]
fn baseline_no_extra_fields_deserializes_ok() {
let body = minimal_request_with_extra("");
serde_json::from_str::<PublishRequest>(&body)
.expect("baseline minimal request must still deserialize");
}
}