Skip to main content

type_bridge_contract/
capability.rs

1//! Open capability identifiers and deterministic capability sets.
2
3use std::collections::BTreeSet;
4use std::fmt;
5
6use serde::de::Error as _;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use crate::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticDetailValue};
10
11/// Maximum ASCII byte length of one namespaced capability identifier.
12pub const MAX_CAPABILITY_ID_BYTES: usize = 255;
13
14/// An open validated namespaced capability identifier.
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct CapabilityId(String);
17
18impl CapabilityId {
19    /// Validate an identifier such as `query.given-multi-row`.
20    pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
21        let value = value.into();
22        let segments = value.split('.').collect::<Vec<_>>();
23        let valid_segment = |segment: &str| {
24            let mut bytes = segment.bytes();
25            bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
26                && bytes.all(|byte| {
27                    byte.is_ascii_lowercase()
28                        || byte.is_ascii_digit()
29                        || matches!(byte, b'-' | b'_')
30                })
31        };
32        if value.len() <= MAX_CAPABILITY_ID_BYTES
33            && segments.len() >= 2
34            && segments.iter().all(|s| valid_segment(s))
35        {
36            Ok(Self(value))
37        } else {
38            Err(Diagnostic::stable(
39                DiagnosticCategory::InvalidContract,
40                "malformed_capability_id",
41                "capability ID must be a bounded lowercase namespaced identifier",
42            ))
43        }
44    }
45    /// Return the canonical identifier spelling.
46    pub fn as_str(&self) -> &str {
47        &self.0
48    }
49}
50
51impl fmt::Display for CapabilityId {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        formatter.write_str(self.as_str())
54    }
55}
56impl Serialize for CapabilityId {
57    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
58    where
59        S: Serializer,
60    {
61        serializer.serialize_str(self.as_str())
62    }
63}
64impl<'de> Deserialize<'de> for CapabilityId {
65    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
66    where
67        D: Deserializer<'de>,
68    {
69        Self::new(String::deserialize(deserializer)?).map_err(D::Error::custom)
70    }
71}
72
73/// A deterministically ordered open capability set.
74#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(transparent)]
76pub struct CapabilitySet(BTreeSet<CapabilityId>);
77
78impl CapabilitySet {
79    /// Construct an empty set.
80    pub const fn new() -> Self {
81        Self(BTreeSet::new())
82    }
83    /// Insert one capability.
84    pub fn insert(&mut self, capability: CapabilityId) -> bool {
85        self.0.insert(capability)
86    }
87    /// Return whether one capability is present.
88    pub fn contains(&self, capability: &CapabilityId) -> bool {
89        self.0.contains(capability)
90    }
91    /// Return the number of capabilities.
92    pub fn len(&self) -> usize {
93        self.0.len()
94    }
95    /// Return whether the set is empty.
96    pub fn is_empty(&self) -> bool {
97        self.0.is_empty()
98    }
99    /// Iterate in deterministic lexical order.
100    pub fn iter(&self) -> impl ExactSizeIterator<Item = &CapabilityId> {
101        self.0.iter()
102    }
103    /// Return capabilities in this required set that are absent from `available`.
104    pub fn missing_from(&self, available: &Self) -> Self {
105        Self(self.0.difference(&available.0).cloned().collect())
106    }
107    /// Reject missing required capabilities before provider I/O.
108    pub fn ensure_supported_by(&self, available: &Self) -> Result<(), Diagnostic> {
109        let missing = self.missing_from(available);
110        if missing.is_empty() {
111            return Ok(());
112        }
113        Err(Diagnostic::stable(
114            DiagnosticCategory::UnsupportedCapability,
115            "unsupported_required_capability",
116            "one or more required capabilities are not advertised",
117        )
118        .with_detail(
119            "missing",
120            DiagnosticDetailValue::TextList(
121                missing.iter().map(|id| id.as_str().to_owned()).collect(),
122            ),
123        ))
124    }
125}
126
127impl FromIterator<CapabilityId> for CapabilitySet {
128    fn from_iter<T: IntoIterator<Item = CapabilityId>>(iter: T) -> Self {
129        Self(iter.into_iter().collect())
130    }
131}
132impl IntoIterator for CapabilitySet {
133    type Item = CapabilityId;
134    type IntoIter = std::collections::btree_set::IntoIter<CapabilityId>;
135    fn into_iter(self) -> Self::IntoIter {
136        self.0.into_iter()
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn id(value: &str) -> CapabilityId {
145        CapabilityId::new(value).unwrap()
146    }
147
148    #[test]
149    fn unknown_open_ids_round_trip_in_deterministic_order() {
150        let set = CapabilitySet::from_iter([id("schema.annotations"), id("query.future-feature")]);
151        let bytes = serde_json::to_vec(&set).unwrap();
152        assert_eq!(bytes, br#"["query.future-feature","schema.annotations"]"#);
153        assert_eq!(
154            serde_json::from_slice::<CapabilitySet>(&bytes).unwrap(),
155            set
156        );
157    }
158
159    #[test]
160    fn malformed_capability_ids_fail_closed() {
161        for value in [
162            "",
163            "query",
164            "Query.feature",
165            "query..feature",
166            "query.feature!",
167        ] {
168            assert_eq!(
169                CapabilityId::new(value).unwrap_err().code().as_str(),
170                "malformed_capability_id",
171            );
172        }
173        assert!(serde_json::from_str::<CapabilityId>(r#""Query.feature""#).is_err());
174    }
175
176    #[test]
177    fn missing_required_capability_has_a_stable_diagnostic() {
178        let required = CapabilitySet::from_iter([id("query.given-multi-row")]);
179        let error = required
180            .ensure_supported_by(&CapabilitySet::new())
181            .unwrap_err();
182        assert_eq!(error.code().as_str(), "unsupported_required_capability");
183    }
184}