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