Skip to main content

harmos_domain/
identity.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14pub use crate::message::{Digest, Fingerprint};
15
16/* === Imports === */
17
18// Standard Library Imports
19use std::fmt;
20
21// Third Party Imports
22use serde::{Deserialize, Serialize};
23
24// Local Imports
25
26/* === Constants === */
27
28// The Shared Owner And Local Operation Segment Alphabet.
29pub const SEGMENT_INITIAL: &str = "abcdefghijklmnopqrstuvwxyz";
30pub const SEGMENT_EXTRA: &str = "0123456789_";
31
32/* === Macros === */
33
34/* === Type Aliases === */
35
36/* === Structs === */
37
38#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39#[serde(try_from = "String", into = "String")]
40pub struct QualifiedId(String);
41
42#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
43#[error("identity `{value}` must be a lowercase portable owner segment followed by dotted local segments")]
44pub struct IdentityError {
45    pub value: String,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
49#[serde(transparent)]
50pub struct DefinitionId(String);
51
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct Requirement {
54    pub id: String,
55    pub version: semver::VersionReq,
56}
57
58/* === Enums === */
59
60/* === Traits === */
61
62// The Stable Identity And Semantic Version Of A Declared Definition.
63pub trait Identified {
64    fn definition() -> (DefinitionId, u32);
65}
66
67/* === Implementations === */
68
69impl QualifiedId {
70    pub fn parse(value: &str) -> Result<Self, IdentityError> {
71        let Some((owner, local)) = value.split_once('.') else {
72            return Err(IdentityError { value: value.to_owned() });
73        };
74        Self::qualify(owner, local)
75    }
76
77    pub fn qualify(owner: &str, local: &str) -> Result<Self, IdentityError> {
78        if !segment(owner) || !dotted(local) {
79            return Err(IdentityError { value: format!("{owner}.{local}") });
80        }
81        Ok(Self(format!("{owner}.{local}")))
82    }
83
84    pub fn as_str(&self) -> &str {
85        &self.0
86    }
87}
88
89impl fmt::Display for QualifiedId {
90    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        formatter.write_str(self.as_str())
92    }
93}
94
95impl TryFrom<String> for QualifiedId {
96    type Error = IdentityError;
97    fn try_from(value: String) -> Result<Self, Self::Error> {
98        Self::parse(&value)
99    }
100}
101
102impl From<QualifiedId> for String {
103    fn from(value: QualifiedId) -> Self {
104        value.0
105    }
106}
107
108impl DefinitionId {
109    pub fn new(id: impl Into<String>) -> Self {
110        Self(id.into())
111    }
112
113    pub fn as_str(&self) -> &str {
114        &self.0
115    }
116}
117
118// The Unit Definition Of A Catalog That Declares Nothing.
119impl Identified for () {
120    fn definition() -> (DefinitionId, u32) {
121        (DefinitionId::new("harmos:unit"), 1)
122    }
123}
124
125impl fmt::Display for DefinitionId {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        formatter.write_str(&self.0)
128    }
129}
130
131/* === Public Functions === */
132
133pub const fn segment(value: &str) -> bool {
134    let bytes = value.as_bytes();
135    segment_bytes(bytes, 0, bytes.len())
136}
137
138// One Or More Portable Segments Joined By Single Dots.
139pub const fn dotted(value: &str) -> bool {
140    let bytes = value.as_bytes();
141    let mut start = 0;
142    let mut index = 0;
143    while index <= bytes.len() {
144        if index == bytes.len() || bytes[index] == b'.' {
145            if !segment_bytes(bytes, start, index) {
146                return false;
147            }
148            start = index + 1;
149        }
150        index += 1;
151    }
152    true
153}
154
155pub const fn artifact_subset(id: &str) -> bool {
156    let bytes = id.as_bytes();
157    if bytes.is_empty() || !contains(SEGMENT_INITIAL, bytes[0]) {
158        return false;
159    }
160
161    let mut index = 0;
162    while index < bytes.len() {
163        let byte = bytes[index];
164        if !(contains(SEGMENT_INITIAL, byte) || contains(SEGMENT_EXTRA, byte) || matches!(byte, b'-' | b'.')) {
165            return false;
166        }
167        index += 1;
168    }
169
170    true
171}
172
173pub const fn qualified_len(owner: &str, name: &str) -> usize {
174    owner.len() + 1 + name.len()
175}
176
177pub const fn qualify<const N: usize>(owner: &str, name: &str) -> [u8; N] {
178    assert!(N == qualified_len(owner, name));
179    let mut output = [0; N];
180    let mut cursor = 0;
181    let owner = owner.as_bytes();
182    let mut index = 0;
183    while index < owner.len() {
184        output[cursor] = owner[index];
185        cursor += 1;
186        index += 1;
187    }
188    output[cursor] = b'.';
189    cursor += 1;
190    let name = name.as_bytes();
191    let mut index = 0;
192    while index < name.len() {
193        output[cursor] = name[index];
194        cursor += 1;
195        index += 1;
196    }
197
198    output
199}
200
201pub const fn qualified(bytes: &'static [u8]) -> &'static str {
202    match core::str::from_utf8(bytes) {
203        Ok(id) => id,
204        Err(_) => panic!("a qualified id is built from two portable names"),
205    }
206}
207
208pub const fn stream_subset(name: &str) -> bool {
209    let bytes = name.as_bytes();
210    let mut start = 0;
211    let mut index = 0;
212    while index <= bytes.len() {
213        if index == bytes.len() || bytes[index] == b'.' {
214            let valid = if start == 0 { segment_bytes(bytes, start, index) } else { stream_local_bytes(bytes, start, index) };
215            if !valid {
216                return false;
217            }
218            start = index + 1;
219        }
220        index += 1;
221    }
222    true
223}
224
225/* === Private Functions === */
226
227const fn contains(alphabet: &str, byte: u8) -> bool {
228    let bytes = alphabet.as_bytes();
229    let mut index = 0;
230    while index < bytes.len() {
231        if bytes[index] == byte {
232            return true;
233        }
234        index += 1;
235    }
236    false
237}
238
239const fn segment_bytes(bytes: &[u8], start: usize, end: usize) -> bool {
240    if start == end || !contains(SEGMENT_INITIAL, bytes[start]) {
241        return false;
242    }
243    let mut index = start + 1;
244    while index < end {
245        if !contains(SEGMENT_INITIAL, bytes[index]) && !contains(SEGMENT_EXTRA, bytes[index]) {
246            return false;
247        }
248        index += 1;
249    }
250    true
251}
252
253const fn stream_local_bytes(bytes: &[u8], start: usize, end: usize) -> bool {
254    if start == end || !(bytes[start] == b'_' || bytes[start].is_ascii_alphabetic()) {
255        return false;
256    }
257    let mut index = start + 1;
258    while index < end {
259        if !(matches!(bytes[index], b'_' | b'-') || bytes[index].is_ascii_alphanumeric()) {
260            return false;
261        }
262        index += 1;
263    }
264    true
265}
266
267/* === Tests === */
268
269#[cfg(test)]
270mod tests {
271    use super::QualifiedId;
272
273    #[test]
274    fn owner_parity() {
275        let cases: Vec<(String, bool)> = serde_json::from_str(include_str!("../tests/fixtures/owner-identities.json")).unwrap();
276        for (owner, valid) in cases {
277            assert_eq!(super::segment(&owner), valid, "owner {owner:?}");
278            assert_eq!(QualifiedId::qualify(&owner, "storage.read_file").is_ok(), valid, "qualified {owner:?}");
279            for facility in ["guest", "sidecar"] {
280                assert_eq!(crate::inventory::contracts::valid_identity(facility, &owner), valid, "{facility} {owner:?}");
281                let publications = crate::inventory::publications::Manifest {
282                    facility: facility.to_owned(),
283                    owner: owner.clone(),
284                    streams: Vec::new(),
285                };
286                assert_eq!(
287                    crate::inventory::publications::select(vec![publications], facility, &owner).is_ok(),
288                    valid,
289                    "publications {owner:?}"
290                );
291                let observers = crate::inventory::observers::Manifest {
292                    facility: facility.to_owned(),
293                    owner: owner.clone(),
294                    observers: Vec::new(),
295                };
296                assert_eq!(
297                    crate::inventory::observers::select_manifest(vec![observers], facility, &owner, &[]).is_ok(),
298                    valid,
299                    "observers {owner:?}"
300                );
301            }
302            if !owner.contains('.') {
303                assert_eq!(super::stream_subset(&format!("{owner}.sample")), valid, "stream {owner:?}");
304            }
305        }
306    }
307
308    #[test]
309    fn qualified_segments() {
310        assert_eq!(QualifiedId::qualify("host", "read_2").unwrap().as_str(), "host.read_2");
311        assert_eq!(QualifiedId::parse("host.storage.read_file").unwrap().as_str(), "host.storage.read_file");
312        for value in ["read", "host..read", ".read", "host.", "Host.read", "host.read-file", "host.2read", "host.read\0"] {
313            assert!(QualifiedId::parse(value).is_err(), "{value:?}");
314        }
315        assert!(QualifiedId::qualify("owner.other", "read").is_err());
316    }
317
318    #[test]
319    fn dotted_local_segments() {
320        for value in ["read", "storage.read_file", "host.operations.cancel", "a0.b_2"] {
321            assert!(super::dotted(value), "{value:?}");
322        }
323        for value in ["", ".read", "read.", "read..file", "Read", "read.2file", "read.file-name", "read.é"] {
324            assert!(!super::dotted(value), "{value:?}");
325        }
326    }
327}