edge_schema/schema/entity/
uri.rs

1/// Represents an entity URI.
2///
3/// Must start with the [`EntityKind`], followed by a colon (:) and the path to
4/// the entity.
5///
6/// The path may either be just a name, which must refer to an entity in the same
7/// scope, a full path to the entity, separated by slashes, or just a UUID.
8///
9/// Example: my.namespace/MyEntityKind.v1:my-entity-name
10/// Example: my.namespace/MyEntityKind.v1:parent/middleman/my-entity-name
11/// Example: my.namespace/MyEntityKind.v1:parent/middleman/my-entity-name
12#[derive(Clone)]
13pub struct EntityUri {
14    value: String,
15
16    type_start: usize,
17    version_start: usize,
18    name_start: usize,
19}
20
21impl PartialEq for EntityUri {
22    fn eq(&self, other: &Self) -> bool {
23        self.value == other.value
24    }
25}
26
27impl std::fmt::Debug for EntityUri {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_tuple("EntityUri").field(&self.value).finish()
30    }
31}
32
33impl Eq for EntityUri {}
34
35impl PartialOrd for EntityUri {
36    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
37        Some(self.value.cmp(&other.value))
38    }
39}
40
41impl Ord for EntityUri {
42    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
43        self.value.cmp(&other.value)
44    }
45}
46
47impl std::hash::Hash for EntityUri {
48    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
49        self.value.hash(state);
50    }
51}
52
53impl EntityUri {
54    pub fn as_str(&self) -> &str {
55        &self.value
56    }
57
58    /// Allowed because this should be used over the Display impl...
59    #[allow(clippy::inherent_to_string_shadow_display)]
60    pub fn to_string(&self) -> String {
61        self.value.clone()
62    }
63
64    pub fn into_string(self) -> String {
65        self.value
66    }
67
68    pub fn kind(&self) -> &str {
69        &self.value[..self.name_start - 1]
70    }
71
72    pub fn namespace(&self) -> &str {
73        &self.value[..self.type_start - 1]
74    }
75
76    pub fn entity_type(&self) -> &str {
77        &self.value[self.type_start..self.version_start - 2]
78    }
79
80    pub fn version(&self) -> &str {
81        &self.value[self.version_start..self.name_start - 1]
82    }
83
84    pub fn name(&self) -> &str {
85        &self.value[self.name_start..]
86    }
87
88    pub fn new_kind_name(kind: &str, name: &str) -> Result<Self, EntityUriParseError> {
89        // TODO(theduke): this should be more efficient!
90        Self::parse(format!("{}:{}", kind, name))
91    }
92
93    pub fn parse(s: impl Into<String>) -> Result<Self, EntityUriParseError> {
94        let s = s.into();
95        let (kind, name) = s.split_once(':').ok_or_else(|| {
96            EntityUriParseError::new(s.clone(), EntityUriParseErrorKind::MissingKindNameSeparator)
97        })?;
98
99        let (ns, kind) = kind.split_once('/').ok_or_else(|| {
100            EntityUriParseError::new(
101                s.clone(),
102                EntityUriParseErrorKind::MissingKindNamespaceSeparator,
103            )
104        })?;
105
106        let (ty, version) = kind.split_once('.').ok_or_else(|| {
107            EntityUriParseError::new(s.clone(), EntityUriParseErrorKind::InvalidType)
108        })?;
109
110        if !is_valid_namespace(ns) {
111            return Err(EntityUriParseError::new(
112                s,
113                EntityUriParseErrorKind::InvalidNamespace,
114            ));
115        }
116        if !is_valid_type_name(ty) {
117            return Err(EntityUriParseError::new(
118                s,
119                EntityUriParseErrorKind::InvalidType,
120            ));
121        }
122        if !is_valid_version(version) {
123            return Err(EntityUriParseError::new(
124                s,
125                EntityUriParseErrorKind::InvalidTypeVersion,
126            ));
127        }
128        if !is_valid_name(name) {
129            return Err(EntityUriParseError::new(
130                s,
131                EntityUriParseErrorKind::InvalidName,
132            ));
133        }
134
135        let type_start = ns.len() + 1;
136        let version_start = type_start + ty.len() + 2;
137        let name_start = version_start + version.len();
138
139        Ok(Self {
140            value: s,
141            type_start,
142            version_start,
143            name_start,
144        })
145    }
146}
147
148impl std::fmt::Display for EntityUri {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        self.value.fmt(f)
151    }
152}
153
154fn is_valid_namespace(s: &str) -> bool {
155    s.split('.').all(|p| {
156        !p.is_empty()
157            && p.chars().all(|c| match c {
158                'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => true,
159                _ => false,
160            })
161    })
162}
163
164fn is_valid_type_name(s: &str) -> bool {
165    !s.is_empty()
166        && s.chars().all(|c| match c {
167            'a'..='z' | 'A'..='Z' | '0'..='9' => true,
168            _ => false,
169        })
170}
171
172fn is_valid_version(s: &str) -> bool {
173    !s.is_empty()
174        && s.chars().all(|c| match c {
175            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => true,
176            _ => false,
177        })
178}
179
180fn is_valid_name(name: &str) -> bool {
181    !name.is_empty()
182        && name.chars().all(|c| match c {
183            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '+' => true,
184            _ => false,
185        })
186}
187
188impl std::str::FromStr for EntityUri {
189    type Err = EntityUriParseError;
190
191    fn from_str(s: &str) -> Result<Self, Self::Err> {
192        Self::parse(s.to_string())
193    }
194}
195
196impl serde::Serialize for EntityUri {
197    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198        serializer.serialize_str(&self.value)
199    }
200}
201
202impl<'de> serde::Deserialize<'de> for EntityUri {
203    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
204        let value = String::deserialize(deserializer)?;
205
206        Self::parse(value).map_err(serde::de::Error::custom)
207    }
208}
209
210impl schemars::JsonSchema for EntityUri {
211    fn schema_name() -> String {
212        "EntityUri".to_string()
213    }
214
215    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
216        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
217            instance_type: Some(schemars::schema::InstanceType::String.into()),
218            ..Default::default()
219        })
220    }
221}
222
223#[derive(Clone, Debug)]
224pub struct EntityUriParseError {
225    value: String,
226    kind: EntityUriParseErrorKind,
227}
228
229impl EntityUriParseError {
230    pub fn new(value: impl Into<String>, kind: EntityUriParseErrorKind) -> Self {
231        Self {
232            value: value.into(),
233            kind,
234        }
235    }
236}
237
238impl std::fmt::Display for EntityUriParseError {
239    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        write!(_f, "invalid entity URI: '{}' ({:?})", self.value, self.kind)
241    }
242}
243
244impl std::error::Error for EntityUriParseError {}
245
246#[derive(Clone, Debug)]
247pub enum EntityUriParseErrorKind {
248    MissingKindNameSeparator,
249    MissingKindNamespaceSeparator,
250    InvalidNamespace,
251    InvalidType,
252    InvalidName,
253    InvalidTypeVersion,
254}
255
256impl std::fmt::Display for EntityUriParseErrorKind {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        match self {
259            EntityUriParseErrorKind::MissingKindNameSeparator => {
260                write!(f, "missing kind/name separator")
261            }
262            EntityUriParseErrorKind::MissingKindNamespaceSeparator => {
263                write!(f, "missing kind/namespace separator")
264            }
265            EntityUriParseErrorKind::InvalidNamespace => write!(f, "invalid namespace"),
266            EntityUriParseErrorKind::InvalidType => write!(f, "invalid type"),
267            EntityUriParseErrorKind::InvalidName => write!(f, "invalid name"),
268            EntityUriParseErrorKind::InvalidTypeVersion => write!(f, "invalid type version"),
269        }
270    }
271}
272
273/// Represents either an inline entity definition, or a reference to an entity.
274#[derive(
275    serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq, Eq, Clone, Debug,
276)]
277pub enum EntityOrRef<T> {
278    #[serde(rename = "ref")]
279    Ref(EntityUri),
280    #[serde(rename = "item")]
281    Item(T),
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_entity_uri_parse() {
290        let u = EntityUri::parse("my-ns.com/Ent.v1:lala123".to_string()).unwrap();
291        assert_eq!(u.namespace(), "my-ns.com");
292        assert_eq!(u.entity_type(), "Ent");
293        assert_eq!(u.version(), "1");
294        assert_eq!(u.name(), "lala123");
295    }
296}