trust-tasks-rs 0.2.57

Reference Rust library for the Trust Tasks framework — transport-agnostic, JSON-based descriptions of verifiable work between parties.
//! 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:?}"),
        }
    }
}