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/* === Macros === */
29
30/* === Type Aliases === */
31
32/* === Structs === */
33
34#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
35#[serde(try_from = "String", into = "String")]
36pub struct QualifiedId(String);
37
38#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
39#[error("identity `{value}` must be a lowercase portable owner segment followed by dotted local segments")]
40pub struct IdentityError {
41    pub value: String,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
45#[serde(transparent)]
46pub struct DefinitionId(String);
47
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct Requirement {
50    pub id: String,
51    pub version: semver::VersionReq,
52}
53
54/* === Enums === */
55
56/* === Traits === */
57
58// The Stable Identity And Semantic Version Of A Declared Definition.
59pub trait Identified {
60    fn definition() -> (DefinitionId, u32);
61}
62
63/* === Implementations === */
64
65impl QualifiedId {
66    pub fn parse(value: &str) -> Result<Self, IdentityError> {
67        let Some((owner, local)) = value.split_once('.') else {
68            return Err(IdentityError { value: value.to_owned() });
69        };
70        Self::qualify(owner, local)
71    }
72
73    pub fn qualify(owner: &str, local: &str) -> Result<Self, IdentityError> {
74        if !segment(owner) || !dotted(local) {
75            return Err(IdentityError { value: format!("{owner}.{local}") });
76        }
77        Ok(Self(format!("{owner}.{local}")))
78    }
79
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83}
84
85impl fmt::Display for QualifiedId {
86    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87        formatter.write_str(self.as_str())
88    }
89}
90
91impl TryFrom<String> for QualifiedId {
92    type Error = IdentityError;
93    fn try_from(value: String) -> Result<Self, Self::Error> {
94        Self::parse(&value)
95    }
96}
97
98impl From<QualifiedId> for String {
99    fn from(value: QualifiedId) -> Self {
100        value.0
101    }
102}
103
104impl DefinitionId {
105    pub fn new(id: impl Into<String>) -> Self {
106        Self(id.into())
107    }
108
109    pub fn as_str(&self) -> &str {
110        &self.0
111    }
112}
113
114// The Unit Definition Of A Catalog That Declares Nothing.
115impl Identified for () {
116    fn definition() -> (DefinitionId, u32) {
117        (DefinitionId::new("harmos:unit"), 1)
118    }
119}
120
121impl fmt::Display for DefinitionId {
122    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
123        formatter.write_str(&self.0)
124    }
125}
126
127/* === Public Functions === */
128
129pub const fn segment(value: &str) -> bool {
130    let bytes = value.as_bytes();
131    if bytes.is_empty() || !bytes[0].is_ascii_lowercase() {
132        return false;
133    }
134    let mut index = 1;
135    while index < bytes.len() {
136        if !(bytes[index].is_ascii_lowercase() || bytes[index].is_ascii_digit() || bytes[index] == b'_') {
137            return false;
138        }
139        index += 1;
140    }
141    true
142}
143
144// One Or More Portable Segments Joined By Single Dots.
145pub const fn dotted(value: &str) -> bool {
146    let bytes = value.as_bytes();
147    let mut start = 0;
148    let mut index = 0;
149    while index <= bytes.len() {
150        if index == bytes.len() || bytes[index] == b'.' {
151            let length = index - start;
152            if length == 0 || !bytes[start].is_ascii_lowercase() {
153                return false;
154            }
155            let mut cursor = start + 1;
156            while cursor < index {
157                if !(bytes[cursor].is_ascii_lowercase() || bytes[cursor].is_ascii_digit() || bytes[cursor] == b'_') {
158                    return false;
159                }
160                cursor += 1;
161            }
162            start = index + 1;
163        }
164        index += 1;
165    }
166    true
167}
168
169pub const fn artifact_subset(id: &str) -> bool {
170    let bytes = id.as_bytes();
171    if bytes.is_empty() || !bytes[0].is_ascii_lowercase() {
172        return false;
173    }
174
175    let mut index = 0;
176    while index < bytes.len() {
177        let byte = bytes[index];
178        if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'.' | b'_')) {
179            return false;
180        }
181        index += 1;
182    }
183
184    true
185}
186
187pub const fn qualified_len(owner: &str, name: &str) -> usize {
188    owner.len() + 1 + name.len()
189}
190
191pub const fn qualify<const N: usize>(owner: &str, name: &str) -> [u8; N] {
192    assert!(N == qualified_len(owner, name));
193    let mut output = [0; N];
194    let mut cursor = 0;
195    let owner = owner.as_bytes();
196    let mut index = 0;
197    while index < owner.len() {
198        output[cursor] = owner[index];
199        cursor += 1;
200        index += 1;
201    }
202    output[cursor] = b'.';
203    cursor += 1;
204    let name = name.as_bytes();
205    let mut index = 0;
206    while index < name.len() {
207        output[cursor] = name[index];
208        cursor += 1;
209        index += 1;
210    }
211
212    output
213}
214
215pub const fn qualified(bytes: &'static [u8]) -> &'static str {
216    match core::str::from_utf8(bytes) {
217        Ok(id) => id,
218        Err(_) => panic!("a qualified id is built from two portable names"),
219    }
220}
221
222pub fn stream_subset(name: &str) -> bool {
223    name.split('.').all(|segment| {
224        let mut characters = segment.chars();
225        characters.next().is_some_and(|first| first == '_' || first.is_ascii_alphabetic())
226            && characters.all(|character| character == '_' || character == '-' || character.is_ascii_alphanumeric())
227    })
228}
229
230/* === Private Functions === */
231
232/* === Tests === */
233
234#[cfg(test)]
235mod tests {
236    use super::QualifiedId;
237
238    #[test]
239    fn qualified_segments() {
240        assert_eq!(QualifiedId::qualify("host", "read_2").unwrap().as_str(), "host.read_2");
241        assert_eq!(QualifiedId::parse("host.storage.read_file").unwrap().as_str(), "host.storage.read_file");
242        for value in ["read", "host..read", ".read", "host.", "Host.read", "host.read-file", "host.2read", "host.read\0"] {
243            assert!(QualifiedId::parse(value).is_err(), "{value:?}");
244        }
245        assert!(QualifiedId::qualify("owner.other", "read").is_err());
246    }
247
248    #[test]
249    fn dotted_local_segments() {
250        for value in ["read", "storage.read_file", "host.operations.cancel", "a0.b_2"] {
251            assert!(super::dotted(value), "{value:?}");
252        }
253        for value in ["", ".read", "read.", "read..file", "Read", "read.2file", "read.file-name", "read.é"] {
254            assert!(!super::dotted(value), "{value:?}");
255        }
256    }
257}