wasmer_deploy_schema/schema/entity/
kind.rs

1use std::borrow::Cow;
2
3/// Represents an entity kind as a string.
4///
5/// Contains 3 components: namespace, name and version.
6/// Example: my.namespace/MyEntityKind.v1
7#[derive(PartialEq, Eq, Clone, Debug)]
8pub struct EntityKind {
9    value: Cow<'static, str>,
10    name_start: usize,
11    version_start: usize,
12}
13
14impl EntityKind {
15    pub fn parse_parts(s: &str) -> Result<(usize, usize), EntityKindParseError> {
16        const fn is_valid_kind_char(c: char) -> bool {
17            match c {
18                'a'..='z' | 'A'..='Z' | '0'..='9' => true,
19                _ => false,
20            }
21        }
22
23        let (rest, version) = match s.rsplit_once(".v") {
24            Some(x) => x,
25            None => return Err(EntityKindParseError::MissingVersion),
26        };
27
28        let (ns, name) = match rest.rsplit_once('/') {
29            Some(x) => x,
30            None => return Err(EntityKindParseError::MissingNamespace),
31        };
32
33        if ns.is_empty() {
34            return Err(EntityKindParseError::MissingNamespace);
35        }
36        if let Some(c) = ns.chars().find(|c| !is_valid_kind_char(*c)) {
37            return Err(EntityKindParseError::InvalidNamespaceCharacter(c));
38        }
39
40        if name.is_empty() {
41            return Err(EntityKindParseError::MissingName);
42        }
43        if let Some(c) = name.chars().find(|c| !is_valid_kind_char(*c)) {
44            return Err(EntityKindParseError::InvalidNameCharacter(c));
45        }
46
47        let len = s.len();
48        let version_start = len - version.len();
49        let name_start = version_start - 2 - name.len();
50
51        Ok((name_start, version_start))
52    }
53
54    pub fn parse(s: impl Into<String>) -> Result<Self, EntityKindParseError> {
55        let s = s.into();
56        let (name_start, version_start) = Self::parse_parts(&s)?;
57
58        Ok(Self {
59            value: Cow::Owned(s),
60            name_start,
61            version_start,
62        })
63    }
64
65    // pub const fn parse_static_or_panic(s: &'static str) -> Self {
66    //     let (name_start, version_start) = match Self::parse_parts(s) {
67    //         Ok(x) => x,
68    //         Err(_err) => {
69    //             panic!("failed to parse entity kind");
70    //         }
71    //     };
72
73    //     Self {
74    //         value: Cow::Borrowed(s),
75    //         name_start,
76    //         version_start,
77    //     }
78    // }
79
80    pub fn namespace(&self) -> &str {
81        &self.value[..self.name_start - 1]
82    }
83
84    pub fn name(&self) -> &str {
85        &self.value[self.name_start..self.version_start - 2]
86    }
87
88    pub fn version(&self) -> &str {
89        &self.value[self.version_start..]
90    }
91}
92
93impl serde::Serialize for EntityKind {
94    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95        serializer.serialize_str(&self.value)
96    }
97}
98
99impl<'de> serde::Deserialize<'de> for EntityKind {
100    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101        let s = String::deserialize(deserializer)?;
102        EntityKind::parse(s).map_err(serde::de::Error::custom)
103    }
104}
105
106impl schemars::JsonSchema for EntityKind {
107    fn schema_name() -> String {
108        "EntityKind".to_string()
109    }
110
111    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
112        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
113            instance_type: Some(schemars::schema::InstanceType::String.into()),
114            ..Default::default()
115        })
116    }
117}
118
119impl std::str::FromStr for EntityKind {
120    type Err = EntityKindParseError;
121
122    fn from_str(s: &str) -> Result<Self, Self::Err> {
123        EntityKind::parse(s)
124    }
125}
126
127/// Error that can occur when parsing an [`EntityKind`].
128#[derive(Clone, Debug)]
129pub enum EntityKindParseError {
130    MissingVersion,
131    MissingNamespace,
132    MissingName,
133    InvalidNamespaceCharacter(char),
134    InvalidNameCharacter(char),
135    InvalidVersion,
136}
137
138impl std::fmt::Display for EntityKindParseError {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            EntityKindParseError::MissingVersion => write!(f, "missing version"),
142            EntityKindParseError::MissingNamespace => write!(f, "missing namespace"),
143            EntityKindParseError::MissingName => write!(f, "missing name"),
144            EntityKindParseError::InvalidNamespaceCharacter(c) => {
145                write!(f, "invalid character in namespace: '{c}'")
146            }
147            EntityKindParseError::InvalidNameCharacter(c) => {
148                write!(f, "invalid character in name: '{}'", c)
149            }
150            EntityKindParseError::InvalidVersion => write!(f, "invalid version"),
151        }
152    }
153}
154
155impl std::error::Error for EntityKindParseError {}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn parse_entity_kind() {
163        let e = EntityKind::parse("a/b.v1").unwrap();
164        assert_eq!(e.namespace(), "a");
165        assert_eq!(e.name(), "b");
166        assert_eq!(e.version(), "1");
167    }
168}