1use std::error::Error;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub struct BoxId(String);
7
8impl BoxId {
9 pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
11 let value = value.into();
12 if valid_identifier(&value, b'-') {
13 Ok(Self(value))
14 } else {
15 Err(IdentityError::InvalidBoxId { value })
16 }
17 }
18
19 pub fn as_str(&self) -> &str {
21 &self.0
22 }
23}
24
25impl fmt::Display for BoxId {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 formatter.write_str(self.as_str())
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct CapabilityName(String);
34
35impl CapabilityName {
36 pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
38 let value = value.into();
39 if valid_identifier(&value, b'_') {
40 Ok(Self(value))
41 } else {
42 Err(IdentityError::InvalidCapabilityName { value })
43 }
44 }
45
46 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl fmt::Display for CapabilityName {
53 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54 formatter.write_str(self.as_str())
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub struct CapabilityId {
61 box_id: BoxId,
62 name: CapabilityName,
63}
64
65impl CapabilityId {
66 pub fn new(box_id: BoxId, name: CapabilityName) -> Self {
68 Self { box_id, name }
69 }
70
71 pub fn box_id(&self) -> &BoxId {
73 &self.box_id
74 }
75
76 pub fn name(&self) -> &CapabilityName {
78 &self.name
79 }
80}
81
82impl fmt::Display for CapabilityId {
83 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84 write!(formatter, "{}.{}", self.box_id, self.name)
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
90pub struct ContractRevision(String);
91
92impl ContractRevision {
93 pub fn new(value: impl Into<String>) -> Result<Self, IdentityError> {
95 let value = value.into();
96 if value.is_empty() {
97 Err(IdentityError::EmptyRevision)
98 } else {
99 Ok(Self(value))
100 }
101 }
102
103 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107}
108
109impl fmt::Display for ContractRevision {
110 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111 formatter.write_str(self.as_str())
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117#[non_exhaustive]
118pub enum IdentityError {
119 InvalidBoxId {
121 value: String,
123 },
124 InvalidCapabilityName {
126 value: String,
128 },
129 EmptyRevision,
131}
132
133impl fmt::Display for IdentityError {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::InvalidBoxId { value } => write!(formatter, "invalid box id: {value:?}"),
137 Self::InvalidCapabilityName { value } => {
138 write!(formatter, "invalid capability name: {value:?}")
139 }
140 Self::EmptyRevision => formatter.write_str("contract revision must not be empty"),
141 }
142 }
143}
144
145impl Error for IdentityError {}
146
147fn valid_identifier(value: &str, separator: u8) -> bool {
148 let mut bytes = value.bytes();
149 matches!(bytes.next(), Some(b'a'..=b'z'))
150 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == separator)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::{BoxId, CapabilityId, CapabilityName, ContractRevision, IdentityError};
156
157 #[test]
158 fn box_id_grammar_is_exact() {
159 for valid in ["a", "box", "box-1", "a0-b9", "box-", "a--b"] {
160 let id = BoxId::new(valid).unwrap();
161 assert_eq!(id.as_str(), valid);
162 assert_eq!(id.to_string(), valid);
163 }
164 for invalid in [
165 "", "A", "Box", "boX", "1box", "-box", "_box", "box%", "box name", "box.name",
166 "box_name", "boéx",
167 ] {
168 assert_eq!(
169 BoxId::new(invalid),
170 Err(IdentityError::InvalidBoxId {
171 value: invalid.into()
172 })
173 );
174 }
175 }
176
177 #[test]
178 fn capability_name_grammar_is_exact() {
179 for valid in ["a", "capability", "capability_1", "a0_b9", "name_", "a__b"] {
180 let name = CapabilityName::new(valid).unwrap();
181 assert_eq!(name.as_str(), valid);
182 assert_eq!(name.to_string(), valid);
183 }
184 for invalid in [
185 "",
186 "A",
187 "Name",
188 "naMe",
189 "1name",
190 "_name",
191 "-name",
192 "name%",
193 "name space",
194 "name.part",
195 "name-part",
196 "naïve",
197 ] {
198 assert_eq!(
199 CapabilityName::new(invalid),
200 Err(IdentityError::InvalidCapabilityName {
201 value: invalid.into()
202 })
203 );
204 }
205 }
206
207 #[test]
208 fn capability_id_joins_and_exposes_both_segments() {
209 let box_id = BoxId::new("billing-box").unwrap();
210 let name = CapabilityName::new("create_invoice").unwrap();
211 let id = CapabilityId::new(box_id.clone(), name.clone());
212
213 assert_eq!(id.box_id(), &box_id);
214 assert_eq!(id.name(), &name);
215 assert_eq!(id.to_string(), "billing-box.create_invoice");
216 }
217
218 #[test]
219 fn contract_revision_is_non_empty_and_otherwise_opaque() {
220 assert_eq!(ContractRevision::new(""), Err(IdentityError::EmptyRevision));
221 for spelling in ["sha256:abc/DEF", " vNEXT ", " "] {
222 let revision = ContractRevision::new(spelling).unwrap();
223 assert_eq!(revision.as_str(), spelling);
224 assert_eq!(revision.to_string(), spelling);
225 }
226 }
227
228 #[test]
229 fn identity_errors_are_equal_and_display_rejected_values() {
230 let box_error = BoxId::new("Bad.Box").unwrap_err();
231 assert_eq!(
232 box_error,
233 IdentityError::InvalidBoxId {
234 value: "Bad.Box".into()
235 }
236 );
237 assert_eq!(box_error.to_string(), "invalid box id: \"Bad.Box\"");
238
239 let name_error = CapabilityName::new("Bad-Name").unwrap_err();
240 assert_eq!(
241 name_error,
242 IdentityError::InvalidCapabilityName {
243 value: "Bad-Name".into()
244 }
245 );
246 assert_eq!(
247 name_error.to_string(),
248 "invalid capability name: \"Bad-Name\""
249 );
250 assert_eq!(
251 IdentityError::EmptyRevision.to_string(),
252 "contract revision must not be empty"
253 );
254 }
255
256 #[test]
257 fn identity_types_have_public_thread_safe_static_bounds() {
258 fn assert_bounds<T: Send + Sync + 'static>() {}
259
260 assert_bounds::<BoxId>();
261 assert_bounds::<CapabilityName>();
262 assert_bounds::<CapabilityId>();
263 assert_bounds::<ContractRevision>();
264 assert_bounds::<IdentityError>();
265 }
266}