acdp_types/body.rs
1use crate::data_ref::DataRef;
2use crate::serde_helpers::de_present;
3use acdp_primitives::primitives::*;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7// ── Body ─────────────────────────────────────────────────────────────────────
8
9/// The immutable stored body of an ACDP context (RFC-ACDP-0002).
10///
11/// Contains producer-controlled fields (covered by the producer signature)
12/// plus registry-assigned identity fields (`ctx_id`, `lineage_id`,
13/// `origin_registry`, `created_at`) which rely on registry honesty in v0.1.0.
14///
15/// The hash/signature preimage is ProducerContent: the Body with
16/// `content_hash`, `signature`, and the registry-assigned identity fields
17/// removed. See RFC-ACDP-0001 §5.7.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Body {
20 // ── Registry-assigned identity fields (NOT in ProducerContent) ──────
21 pub ctx_id: CtxId,
22 pub lineage_id: LineageId,
23 pub origin_registry: String,
24 pub created_at: DateTime<Utc>,
25
26 // ── Integrity fields (NOT in ProducerContent) ────────────────────────
27 pub content_hash: ContentHash,
28 pub signature: Signature,
29
30 // ── Producer-controlled required fields ──────────────────────────────
31 pub version: u32,
32 pub supersedes: Option<CtxId>,
33 pub agent_id: AgentDid,
34 pub contributors: Vec<AgentDid>,
35 pub title: String,
36 #[serde(rename = "type")]
37 pub context_type: ContextType,
38 pub data_refs: Vec<DataRef>,
39 pub derived_from: Vec<CtxId>,
40 pub visibility: Visibility,
41
42 // ── Producer-controlled optional fields ──────────────────────────────
43 //
44 // Optional bare-typed fields use the absent-vs-null convention
45 // (RFC-ACDP-0005 §2.2.1, schema-005/006/007): an absent key is
46 // tolerated, a present-but-`null` key is rejected at deserialize.
47 // [`crate::serde_helpers::de_present`] implements this.
48 // `supersedes` is the one v0.1.0 field whose schema is
49 // `["string","null"]` (RFC-ACDP-0002 §3.1) — it is legitimately
50 // nullable and intentionally NOT routed through `de_present`.
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub audience: Option<Vec<AgentDid>>,
53 #[serde(
54 default,
55 deserialize_with = "de_present",
56 skip_serializing_if = "Option::is_none"
57 )]
58 pub acdp_version: Option<String>,
59 #[serde(
60 default,
61 deserialize_with = "de_present",
62 skip_serializing_if = "Option::is_none"
63 )]
64 pub description: Option<String>,
65 /// Producer-supplied summary for search results (≤ 1000 chars).
66 /// Part of ProducerContent — included in the content_hash preimage.
67 #[serde(
68 default,
69 deserialize_with = "de_present",
70 skip_serializing_if = "Option::is_none"
71 )]
72 pub summary: Option<String>,
73 #[serde(
74 default,
75 deserialize_with = "de_present",
76 skip_serializing_if = "Option::is_none"
77 )]
78 pub tags: Option<Vec<String>>,
79 #[serde(
80 default,
81 deserialize_with = "de_present",
82 skip_serializing_if = "Option::is_none"
83 )]
84 pub domain: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub expires_at: Option<DateTime<Utc>>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub data_period: Option<DataPeriod>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub metadata: Option<serde_json::Value>,
91 #[serde(
92 default,
93 deserialize_with = "de_present",
94 skip_serializing_if = "Option::is_none"
95 )]
96 pub schema_uri: Option<String>,
97
98 /// Forward-compatible carry-through of unknown producer-controlled
99 /// fields (e.g. v0.1's `priority`). Including these in the typed
100 /// model is required for `serde_json::to_value(body)` → JCS → SHA-256
101 /// to reproduce the original `content_hash`. Without `flatten`, a
102 /// v0.1.0 consumer reading a v0.1 body would silently drop the new
103 /// field and compute a different hash, falsely rejecting the body.
104 #[serde(flatten)]
105 pub extensions: serde_json::Map<String, serde_json::Value>,
106}
107
108/// Time window the underlying data covers.
109///
110/// Per `acdp-common.schema.json#/$defs/data_period`, both `start` and `end`
111/// are required (additionalProperties: false). The schema does not compare
112/// timestamps; producers SHOULD ensure `start <= end` and registries
113/// SHOULD reject `start > end` as `schema_violation` at runtime.
114///
115/// `data_period` is a CLOSED two-field wire shape and part of
116/// ProducerContent — an unknown field would silently change the
117/// `content_hash` preimage, so `deny_unknown_fields` rejects it
118/// (RFC-ACDP-0007 §3.3.1, conformance fixture schema-009).
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(deny_unknown_fields)]
121pub struct DataPeriod {
122 /// Inclusive start of the data period.
123 pub start: DateTime<Utc>,
124 /// Inclusive end of the data period.
125 pub end: DateTime<Utc>,
126}
127
128/// Detached Ed25519 signature over the body's `content_hash` field value.
129///
130/// The `value` bytes are a signature over the ASCII bytes of the full
131/// `content_hash` string (e.g. `"sha256:5f8d…"`) — NOT the raw 32-byte
132/// digest. See RFC-ACDP-0001 §5.8.
133///
134/// The `signature` object is a CLOSED wire shape — exactly `algorithm`,
135/// `key_id`, `value` (`additionalProperties: false`). Future signature
136/// variants (proof chains, threshold attestations) require an explicit
137/// schema bump, not field-level extensibility, so `deny_unknown_fields`
138/// rejects an unknown field (RFC-ACDP-0007 §3.3.1, fixture schema-008).
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct Signature {
142 /// Algorithm identifier. Only `"ed25519"` is required in v0.1.0.
143 pub algorithm: String,
144 /// DID URL identifying the signing key (e.g. `did:web:…#key-1`).
145 pub key_id: String,
146 /// Standard base64-encoded signature bytes.
147 pub value: String,
148}
149
150// ── Registry state ────────────────────────────────────────────────────────────
151
152/// Mutable, registry-derived state returned alongside the Body on retrieval.
153///
154/// In v0.1.0 this contains only `status`. Future versions add lifecycle
155/// events, relationships, and attestations here without modifying the
156/// Body. Unknown fields are preserved in [`Self::extensions`] so consumers
157/// can surface them to operators (RFC-ACDP-0004 §3 forward-compat).
158///
159/// # Reserved extension field names (RFC-ACDP-0009 §2.1)
160///
161/// The following keys are reserved for future RFCs. Until the relevant
162/// RFC ships normative text, v0.1.0 consumers will see them in
163/// [`Self::extensions`] (the `#[serde(flatten)]` map below). v0.1.0
164/// producers MUST NOT emit them.
165///
166/// | Name | RFC | Purpose |
167/// |-------------------|-------------------------------|---------------------------------------------------------------|
168/// | `lifecycle_events`| RFC-ACDP-0009 §2.1 (reserved) | Retraction / republication / status-change audit trail. |
169/// | `relationships` | RFC-ACDP-0009 §2.1 (reserved) | Post-publication `builds_on` / `disputes` etc. |
170/// | `attestations` | RFC-ACDP-0009 §2.1 (reserved) | Third-party `reproduced` / `audit` markers. |
171/// | `subscriptions` | RFC-ACDP-0009 §2.1 (reserved) | Push-subscription receipts. |
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct RegistryState {
174 pub status: Status,
175 /// Forward-compatible passthrough for fields added in future versions
176 /// (e.g. v0.1's `lifecycle_events`, `relationships`, `attestations`,
177 /// `subscriptions` — see the type docs for the reserved set).
178 #[serde(flatten)]
179 pub extensions: serde_json::Map<String, serde_json::Value>,
180}
181
182// ── Full retrieval envelope ───────────────────────────────────────────────────
183
184/// The full context object returned by `GET /contexts/{ctx_id}`.
185///
186/// `acdp-context.schema.json` is `additionalProperties: true`: future
187/// ACDP versions may add top-level keys without a schema bump, and
188/// v0.1.0 consumers MUST tolerate unknown top-level keys. `body`,
189/// `registry_state`, and the reserved `registry_receipt` are modelled
190/// explicitly; any other top-level field is preserved verbatim in
191/// [`Self::extensions`]. These top-level fields are NOT part of
192/// `ProducerContent`, so unlike [`Body::extensions`] this carry-through
193/// is a forward-compatibility contract, not a hash-stability one.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct FullContext {
196 /// Producer-signed body.
197 pub body: Body,
198 /// Mutable registry-derived state (status etc).
199 pub registry_state: RegistryState,
200 /// Optional registry receipt — reserved for RFC-ACDP-0009 §2.7. Opaque
201 /// to the library; preserved verbatim if present.
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub registry_receipt: Option<serde_json::Value>,
204
205 /// Unknown top-level context fields, preserved per
206 /// `acdp-context.schema.json` `additionalProperties: true`. Retained
207 /// for forward compatibility with future ACDP versions that add
208 /// top-level registry fields.
209 #[serde(flatten)]
210 pub extensions: serde_json::Map<String, serde_json::Value>,
211}