Skip to main content

acdp_types/
profile.rs

1//! ACDP conformance profiles (RFC-ACDP-0001 §9.1).
2//!
3//! Implementations declare their profile(s) in the capabilities document
4//! `profiles` field. Each profile is a strict superset of its prerequisite.
5//!
6//! This crate's *consumer-side* claim is [`Profile::Consumer`]: it
7//! verifies producer signatures end-to-end, resolves cross-registry
8//! references, applies visibility rules client-side, and tolerates
9//! unknown fields. The validation and SSRF building blocks
10//! (`acdp::registry::PublishValidator`, `acdp::safe_http::SsrfPolicy`)
11//! are designed for consumption by `acdp-registry-core` /
12//! `acdp-registry-federated` registry implementations built on top.
13
14use crate::capabilities::CapabilitiesDocument;
15
16/// One of the four conformance profiles defined by RFC-ACDP-0001 §9.1.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Profile {
19    /// `acdp-registry-core` — minimum profile for any registry.
20    RegistryCore,
21    /// `acdp-registry-discovery` — adds keyword search.
22    RegistryDiscovery,
23    /// `acdp-registry-federated` — adds cross-registry resolution.
24    RegistryFederated,
25    /// `acdp-registry-receipts` — mints registry-signed publication
26    /// receipts (ACDP 0.2, RFC-ACDP-0010).
27    RegistryReceipts,
28    /// `acdp-consumer` — a consumer of contexts (not a registry).
29    Consumer,
30}
31
32impl Profile {
33    /// Wire-form identifier as it appears in
34    /// `capabilities.profiles` and prose references.
35    pub fn as_str(self) -> &'static str {
36        match self {
37            Profile::RegistryCore => "acdp-registry-core",
38            Profile::RegistryDiscovery => "acdp-registry-discovery",
39            Profile::RegistryFederated => "acdp-registry-federated",
40            Profile::RegistryReceipts => "acdp-registry-receipts",
41            Profile::Consumer => "acdp-consumer",
42        }
43    }
44}
45
46/// Profiles that this `acdp` crate is designed to satisfy on the
47/// consumer side. A registry implementer building on top of the
48/// crate's primitives (`PublishValidator`, `SsrfPolicy`,
49/// `CrossRegistryResolver`) MAY claim additional profiles in their
50/// own capabilities document.
51pub const CLAIMED: &[Profile] = &[Profile::Consumer];
52
53impl CapabilitiesDocument {
54    /// Returns `true` if the registry advertises the given profile.
55    pub fn claims_profile(&self, profile: Profile) -> bool {
56        self.profiles.iter().any(|p| p == profile.as_str())
57    }
58
59    /// Returns `Ok(())` if the registry advertises every profile in
60    /// `required`. Returns the first missing profile in
61    /// [`acdp_primitives::error::AcdpError::SchemaViolation`] otherwise.
62    pub fn supports_required(
63        &self,
64        required: &[Profile],
65    ) -> Result<(), acdp_primitives::error::AcdpError> {
66        for p in required {
67            if !self.claims_profile(*p) {
68                return Err(acdp_primitives::error::AcdpError::SchemaViolation(format!(
69                    "registry does not advertise required profile '{}'",
70                    p.as_str()
71                )));
72            }
73        }
74        Ok(())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::capabilities::Limits;
82
83    fn caps_with(profiles: Vec<&str>) -> CapabilitiesDocument {
84        CapabilitiesDocument {
85            acdp_version: "0.1.0".into(),
86            registry_did: "did:web:r.example.com".into(),
87            supported_signature_algorithms: vec!["ed25519".into()],
88            supported_did_methods: vec!["did:web".into()],
89            profiles: profiles.into_iter().map(String::from).collect(),
90            limits: Limits {
91                max_payload_bytes: 1_048_576,
92                max_embedded_bytes: 65_536,
93                idempotency_key_ttl_seconds: None,
94            },
95            read_authentication_methods: vec![],
96            anonymous_public_reads: true,
97            supports_idempotency_key: false,
98            extensions: Default::default(),
99        }
100    }
101
102    #[test]
103    fn claimed_profile_matches() {
104        let caps = caps_with(vec!["acdp-registry-core", "acdp-registry-discovery"]);
105        assert!(caps.claims_profile(Profile::RegistryCore));
106        assert!(caps.claims_profile(Profile::RegistryDiscovery));
107        assert!(!caps.claims_profile(Profile::RegistryFederated));
108    }
109
110    #[test]
111    fn supports_required_returns_first_missing() {
112        let caps = caps_with(vec!["acdp-registry-core"]);
113        caps.supports_required(&[Profile::RegistryCore]).unwrap();
114        let err = caps
115            .supports_required(&[Profile::RegistryCore, Profile::RegistryFederated])
116            .unwrap_err();
117        match err {
118            acdp_primitives::error::AcdpError::SchemaViolation(msg) => {
119                assert!(msg.contains("acdp-registry-federated"));
120            }
121            other => panic!("got {other:?}"),
122        }
123    }
124
125    #[test]
126    fn claimed_includes_consumer() {
127        assert!(CLAIMED.contains(&Profile::Consumer));
128    }
129}