1use crate::body::{DataPeriod, Signature};
2use crate::data_ref::DataRef;
3use crate::serde_helpers::de_present;
4use acdp_primitives::primitives::*;
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct PublishRequest {
26 pub version: u32,
28 pub supersedes: Option<CtxId>,
29 pub agent_id: AgentDid,
30 pub contributors: Vec<AgentDid>,
31 pub title: String,
32 #[serde(rename = "type")]
33 pub context_type: ContextType,
34 pub data_refs: Vec<DataRef>,
35 pub derived_from: Vec<CtxId>,
36 pub visibility: Visibility,
37
38 pub content_hash: ContentHash,
40 pub signature: Signature,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
51 pub audience: Option<Vec<AgentDid>>,
52 #[serde(
53 default,
54 deserialize_with = "de_present",
55 skip_serializing_if = "Option::is_none"
56 )]
57 pub acdp_version: Option<String>,
58 #[serde(
59 default,
60 deserialize_with = "de_present",
61 skip_serializing_if = "Option::is_none"
62 )]
63 pub description: Option<String>,
64 #[serde(
66 default,
67 deserialize_with = "de_present",
68 skip_serializing_if = "Option::is_none"
69 )]
70 pub summary: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
77 pub lineage_id: Option<LineageId>,
78 #[serde(
79 default,
80 deserialize_with = "de_present",
81 skip_serializing_if = "Option::is_none"
82 )]
83 pub tags: Option<Vec<String>>,
84 #[serde(
85 default,
86 deserialize_with = "de_present",
87 skip_serializing_if = "Option::is_none"
88 )]
89 pub domain: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub expires_at: Option<DateTime<Utc>>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub data_period: Option<DataPeriod>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub metadata: Option<serde_json::Value>,
96 #[serde(
97 default,
98 deserialize_with = "de_present",
99 skip_serializing_if = "Option::is_none"
100 )]
101 pub schema_uri: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct PublishResponse {
117 pub ctx_id: CtxId,
119 pub lineage_id: LineageId,
121 pub version: u32,
123 pub created_at: DateTime<Utc>,
125 pub status: Status,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub registry_receipt: Option<serde_json::Value>,
134}
135
136pub use acdp_primitives::wire_error::{WireError, WireErrorBody};
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn minimal_request_with_extra(extra: &str) -> String {
146 format!(
147 r#"{{
148 "version": 1,
149 "agent_id": "did:web:agents.example.com:test",
150 "contributors": [],
151 "title": "t",
152 "type": "data_snapshot",
153 "data_refs": [],
154 "derived_from": [],
155 "visibility": "public",
156 "content_hash": "sha256:0",
157 "signature": {{
158 "algorithm": "ed25519",
159 "key_id": "did:web:agents.example.com:test#key-1",
160 "value": "{sig}"
161 }}{extra}
162 }}"#,
163 sig = "A".repeat(88),
164 extra = extra
165 )
166 }
167
168 #[test]
174 fn extra_top_level_field_is_rejected() {
175 let body = minimal_request_with_extra(r#", "ctx_id": "acdp://r/x""#);
176 let res: Result<PublishRequest, _> = serde_json::from_str(&body);
177 assert!(res.is_err(), "ctx_id in publish request must be rejected");
178 }
179
180 #[test]
181 fn extra_origin_registry_field_is_rejected() {
182 let body = minimal_request_with_extra(r#", "origin_registry": "did:web:r.x""#);
183 let res: Result<PublishRequest, _> = serde_json::from_str(&body);
184 assert!(res.is_err());
185 }
186
187 #[test]
188 fn extra_created_at_field_is_rejected() {
189 let body = minimal_request_with_extra(r#", "created_at": "2026-01-01T00:00:00.000Z""#);
190 let res: Result<PublishRequest, _> = serde_json::from_str(&body);
191 assert!(res.is_err());
192 }
193
194 #[test]
195 fn arbitrary_unknown_field_is_rejected() {
196 let body = minimal_request_with_extra(r#", "noodle": 42"#);
197 let res: Result<PublishRequest, _> = serde_json::from_str(&body);
198 assert!(res.is_err());
199 }
200
201 #[test]
202 fn baseline_no_extra_fields_deserializes_ok() {
203 let body = minimal_request_with_extra("");
204 serde_json::from_str::<PublishRequest>(&body)
205 .expect("baseline minimal request must still deserialize");
206 }
207}