type_bridge_contract/
capability.rs1use 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
11pub const MAX_CAPABILITY_ID_BYTES: usize = 255;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct CapabilityId(String);
17
18impl CapabilityId {
19 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 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(transparent)]
76pub struct CapabilitySet(BTreeSet<CapabilityId>);
77
78impl CapabilitySet {
79 pub const fn new() -> Self {
81 Self(BTreeSet::new())
82 }
83 pub fn insert(&mut self, capability: CapabilityId) -> bool {
85 self.0.insert(capability)
86 }
87 pub fn contains(&self, capability: &CapabilityId) -> bool {
89 self.0.contains(capability)
90 }
91 pub fn len(&self) -> usize {
93 self.0.len()
94 }
95 pub fn is_empty(&self) -> bool {
97 self.0.is_empty()
98 }
99 pub fn iter(&self) -> impl ExactSizeIterator<Item = &CapabilityId> {
101 self.0.iter()
102 }
103 pub fn missing_from(&self, available: &Self) -> Self {
105 Self(self.0.difference(&available.0).cloned().collect())
106 }
107 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}