1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
//! Ties a Rust struct to the *Trust Task specification* it represents.
//!
//! [`Payload`] is the integration seam between the framework crate and per-
//! spec types (whether generated by `trust-tasks-codegen` or hand-written).
//! Once a type implements [`Payload`], callers can build documents without
//! restating the Type URI:
//!
//! ```rust,ignore
//! use trust_tasks_rs::{Payload, TrustTask};
//!
//! let req = TrustTask::for_payload("req-1", AclGrant { ... });
//! assert_eq!(req.type_uri, AclGrant::type_uri());
//! ```
//!
//! The generated code emits one impl per request payload and, where the
//! specification defines a success response, a second impl on the response
//! type with the `#response` fragment in [`Payload::TYPE_URI`].
use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::error::TrustTaskCode;
use crate::type_uri::TypeUri;
/// A Rust type that corresponds to one variant (request or response) of a
/// versioned *Trust Task specification*.
///
/// The generated code emits one impl per (slug, version, variant). Hand-
/// written impls are equally valid; the only requirement is that
/// [`TYPE_URI`](Self::TYPE_URI) parses as a [`TypeUri`].
pub trait Payload: Serialize + DeserializeOwned {
/// The canonical Type URI this payload targets, including the `#response`
/// fragment for success-response payloads (SPEC.md §4.4.1).
const TYPE_URI: &'static str;
/// Whether the originating *Trust Task specification* is a *bearer
/// specification* per SPEC.md §4.8.3 — that is, opts out of the §4.8.2
/// audience-binding rule.
///
/// Defaults to `false` (non-bearer). The codegen emits an explicit
/// `const IS_BEARER: bool = true;` override only when the spec's front
/// matter declares `bearer: true`.
///
/// Consumers consult this constant via
/// [`crate::TrustTask::enforce_audience_binding`] to apply SPEC.md §7.2
/// item 8 without consulting the registry at runtime.
///
/// The codegen emits this constant on both the request `Payload`
/// impl and the response `Response` impl (when the spec defines
/// one). The audience-binding check fires on request-side documents
/// only, so the constant on the response impl is informational —
/// downstream tooling that walks generated modules generically can
/// read it without special-casing variants.
const IS_BEARER: bool = false;
/// Whether the originating *Trust Task specification* obliges a *consumer*
/// to reject a document that arrives without a `proof`, per SPEC.md §7.3
/// item 8 (`proofRequirement.requirement == "REQUIRED"`).
///
/// Defaults to `false` (i.e. `OPTIONAL` or `RECOMMENDED` — the consumer
/// is free to accept a proofless document). The codegen emits an explicit
/// `const IS_PROOF_REQUIRED: bool = true;` override only when the spec's
/// front matter declares `proofRequirement.requirement: REQUIRED`.
///
/// Consumers consult this constant via [`crate::consume_inbound`] to
/// apply SPEC.md §7.2 item 7 authoritatively per-spec, rather than as a
/// consumer-wide policy toggle.
///
/// Like [`IS_BEARER`](Self::IS_BEARER), this constant is emitted on
/// both the request `Payload` impl and the response `Response` impl.
/// `consume_inbound` consults it on the request side; a producer
/// consuming a response would do the same check against the response
/// impl if its trust posture requires it.
const IS_PROOF_REQUIRED: bool = false;
/// Whether the originating *Trust Task specification* obliges a *consumer*
/// to reject a document that arrives without an in-band `recipient`, per
/// SPEC.md §7.2 item 5 and §7.3 item 5 (the party filling the `recipient`
/// member is declared `REQUIRED`).
///
/// Defaults to `false`. The codegen emits an explicit
/// `const IS_RECIPIENT_REQUIRED: bool = true;` override only when the
/// spec's front matter declares the relevant party (the one carrying
/// `member: recipient`) as `requirement: REQUIRED`. Because a response
/// document swaps the parties, the `Response` impl's value tracks the
/// *issuer* party's requirement instead.
///
/// When `true`, a document whose in-band `recipient` is absent is rejected
/// with `malformedRequest` — the audience must be carried in-band (not
/// merely transport-derived) so the document is self-contained (§4.8).
/// Consumers consult this via [`crate::consume_inbound`].
const IS_RECIPIENT_REQUIRED: bool = false;
/// Parsed form of [`TYPE_URI`](Self::TYPE_URI).
///
/// The default implementation calls [`str::parse`] and panics on a
/// malformed value — which can only happen if a `Payload` impl supplies
/// an invalid `TYPE_URI`, i.e. a static-string bug worth surfacing
/// loudly.
fn type_uri() -> TypeUri {
Self::TYPE_URI
.parse()
.expect("TYPE_URI constant must be a valid Type URI")
}
/// Build an extended [`TrustTaskCode`] under this payload's slug, per
/// SPEC.md §8.5.
///
/// Equivalent to writing:
///
/// ```rust,ignore
/// TrustTaskCode::new_extended("acl/change-role", "last_authority_protected").unwrap()
/// ```
///
/// but sources the slug from [`TYPE_URI`](Self::TYPE_URI) so the slug
/// literal cannot drift away from the type's identity. The §8.5
/// namespace rule ("the slug of the spec being processed") is then
/// enforced by construction.
///
/// `local` is validated against `spec.meta.schema.json`'s
/// `errorCodes[].code` grammar (the part after the colon: a lowercase
/// letter, then letters of either case, digits, or underscores).
/// Both casings are accepted so that framework 0.2 lowerCamelCase
/// locals (`documentRevoked`) and frozen framework 0.1 snake_case
/// locals (`document_revoked`) parse under one rule; SPEC §4.10 item 4
/// **SHOULD**s lowerCamelCase for new specifications. Panics on
/// an invalid `local` — this method is for static call-site usage;
/// callers handling runtime input should use
/// [`TrustTaskCode::new_extended`] and propagate the `Result`.
///
/// Also panics under the same condition as
/// [`type_uri`](Self::type_uri): when [`TYPE_URI`](Self::TYPE_URI)
/// is not a valid Type URI, i.e. a static-string bug.
fn extended_code(local: impl Into<String>) -> TrustTaskCode {
let slug = Self::type_uri().slug().to_string();
let local = local.into();
TrustTaskCode::new_extended(&slug, &local).unwrap_or_else(|e| {
panic!(
"Payload::extended_code({:?}) on slug {:?} failed validation: {e}",
local, slug
)
})
}
/// Build an extended [`TrustTaskCode`] under a *family namespace*, per
/// SPEC.md §8.5 rule 2.
///
/// A family namespace is a proper path prefix of this payload's slug, used
/// for a condition whose meaning is defined once across a family rather
/// than per specification — `did-management:unknownDomain` on
/// `did-management/did/delete`, say, where every member of the family can
/// reject a request naming a domain the *consumer* does not host and the
/// rejection means the same thing in each.
///
/// ```rust,ignore
/// // On a `did-management/did/delete` handler:
/// let code = Payload::family_code("did-management", "unknownDomain");
/// assert_eq!(code.to_string(), "did-management:unknownDomain");
/// ```
///
/// Use [`extended_code`](Self::extended_code) for a code the specification
/// defines for itself; that is the common case. Reach for this only when
/// the code is genuinely shared, because a family namespace claims the
/// condition means the same thing across every sibling.
///
/// `namespace` is checked against the slug derived from
/// [`TYPE_URI`](Self::TYPE_URI) rather than taken on trust, so the §8.5
/// prefix rule holds by construction and a hand-written namespace cannot
/// drift away from the type's identity — the same guarantee
/// [`extended_code`](Self::extended_code) provides for the own-slug case.
///
/// Panics when `namespace` is neither the slug nor a proper path prefix of
/// it, or when `local` fails the `errorCodes[].code` grammar. Like
/// [`extended_code`](Self::extended_code) this method is for static
/// call-site usage; callers handling runtime input should use
/// [`TrustTaskCode::new_extended`] and propagate the `Result`.
fn family_code(namespace: &str, local: impl Into<String>) -> TrustTaskCode {
let slug = Self::type_uri().slug().to_string();
let local = local.into();
// The slug itself plus each proper path prefix of it.
let permitted = slug
.match_indices('/')
.map(|(i, _)| &slug[..i])
.chain(std::iter::once(slug.as_str()));
if !permitted.into_iter().any(|p| p == namespace) {
panic!(
"Payload::family_code({namespace:?}, {local:?}) on slug {slug:?}: \
namespace is neither the slug nor a path prefix of it \
(SPEC §8.5 rule 2)"
);
}
TrustTaskCode::new_extended(namespace, &local).unwrap_or_else(|e| {
panic!(
"Payload::family_code({:?}, {:?}) failed validation: {e}",
namespace, local
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::specs::acl::change_role::v0_1 as change_role;
use crate::specs::acl::grant::v0_1 as grant;
use crate::specs::trust_task_discovery::v0_1 as discovery;
#[test]
fn extended_code_sources_slug_from_type_uri() {
let code = grant::Payload::extended_code("role_not_recognized");
match code {
TrustTaskCode::Extended { slug, local } => {
assert_eq!(slug, "acl/grant");
assert_eq!(local, "role_not_recognized");
}
other => panic!("expected Extended, got {other:?}"),
}
// Hierarchical slug — drift would be especially easy to hit by hand.
let code = change_role::Payload::extended_code("last_authority_protected");
assert_eq!(code.to_string(), "acl/change-role:last_authority_protected");
}
#[test]
fn extended_code_works_for_single_segment_slug() {
// Single-segment slug — no `/` in the namespace.
let code = discovery::Payload::extended_code("filter_unsupported");
assert_eq!(code.to_string(), "trust-task-discovery:filter_unsupported");
}
#[test]
#[should_panic(expected = "failed validation")]
fn extended_code_panics_on_invalid_local() {
// A *leading* capital violates the `errorCodes[].code` grammar —
// the resulting Extended would fail to round-trip through FromStr.
// (Interior capitals are fine: lowerCamelCase locals are the
// SPEC §4.10 preference. It is only the first character that must
// be lowercase.) The trait method panics so a static-string bug
// fails loudly instead of silently producing a code that fails
// parsing later.
let _ = grant::Payload::extended_code("BadLocal");
}
/// SPEC §8.5 rule 2 — a proper path prefix of the emitting slug is a
/// legal namespace. This is the `did-management:unknownDomain` shape:
/// 26 specifications in the registry declare it, and before `family_code`
/// existed the only drift-safe helper derived the namespace from
/// `TYPE_URI` and so could not mint the code the registry advertises.
#[test]
fn family_code_accepts_each_path_prefix_of_the_slug() {
// Two-segment slug — the one available prefix.
let code = change_role::Payload::family_code("acl", "permissionDenied");
assert_eq!(code.to_string(), "acl:permissionDenied");
// The full slug is permitted too, making family_code a superset of
// extended_code rather than a disjoint alternative.
let code = change_role::Payload::family_code("acl/change-role", "lastAuthorityProtected");
assert_eq!(code.to_string(), "acl/change-role:lastAuthorityProtected");
}
/// A sibling's slug shares a prefix but is not itself a prefix, which is
/// exactly the confusion §8.5 forbids ("never that of a related or
/// referenced specification"). Rule 2 must not open a door to it.
#[test]
#[should_panic(expected = "neither the slug nor a path prefix")]
fn family_code_rejects_a_sibling_slug() {
let _ = grant::Payload::family_code("acl/revoke", "borrowedCode");
}
/// An unrelated namespace with no relationship to the slug at all.
#[test]
#[should_panic(expected = "neither the slug nor a path prefix")]
fn family_code_rejects_an_unrelated_namespace() {
let _ = grant::Payload::family_code("vault", "somethingElse");
}
/// A prefix must end on a segment boundary — `ac` is a string prefix of
/// `acl/grant` but names nothing.
#[test]
#[should_panic(expected = "neither the slug nor a path prefix")]
fn family_code_rejects_a_partial_segment() {
let _ = grant::Payload::family_code("ac", "somethingElse");
}
/// Response payloads carry `#response` in TYPE_URI; the prefix check must
/// run against the bare slug, as `extended_code` does.
#[test]
fn family_code_strips_response_fragment_before_checking() {
let code = grant::Response::family_code("acl", "permissionDenied");
assert_eq!(code.to_string(), "acl:permissionDenied");
}
#[test]
fn extended_code_strips_response_fragment_from_slug() {
// Response payloads carry `#response` in their TYPE_URI. The
// helper MUST source the slug via `TypeUri::slug()`, which
// drops the fragment — otherwise an error code minted from a
// Response handler would name the wrong namespace.
let code = grant::Response::extended_code("role_not_recognized");
match code {
TrustTaskCode::Extended { slug, .. } => {
assert_eq!(slug, "acl/grant", "response variant must yield bare slug");
}
other => panic!("expected Extended, got {other:?}"),
}
}
}