edge-schema 0.1.0

Shared schema types for Wasmer Edge.
Documentation
use std::borrow::Cow;

/// Represents an entity kind as a string.
///
/// Contains 3 components: namespace, name and version.
/// Example: my.namespace/MyEntityKind.v1
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct EntityKind {
    value: Cow<'static, str>,
    name_start: usize,
    version_start: usize,
}

impl EntityKind {
    pub fn parse_parts(s: &str) -> Result<(usize, usize), EntityKindParseError> {
        const fn is_valid_kind_char(c: char) -> bool {
            match c {
                'a'..='z' | 'A'..='Z' | '0'..='9' => true,
                _ => false,
            }
        }

        let (rest, version) = match s.rsplit_once(".v") {
            Some(x) => x,
            None => return Err(EntityKindParseError::MissingVersion),
        };

        let (ns, name) = match rest.rsplit_once('/') {
            Some(x) => x,
            None => return Err(EntityKindParseError::MissingNamespace),
        };

        if ns.is_empty() {
            return Err(EntityKindParseError::MissingNamespace);
        }
        if let Some(c) = ns.chars().find(|c| !is_valid_kind_char(*c)) {
            return Err(EntityKindParseError::InvalidNamespaceCharacter(c));
        }

        if name.is_empty() {
            return Err(EntityKindParseError::MissingName);
        }
        if let Some(c) = name.chars().find(|c| !is_valid_kind_char(*c)) {
            return Err(EntityKindParseError::InvalidNameCharacter(c));
        }

        let len = s.len();
        let version_start = len - version.len();
        let name_start = version_start - 2 - name.len();

        Ok((name_start, version_start))
    }

    pub fn parse(s: impl Into<String>) -> Result<Self, EntityKindParseError> {
        let s = s.into();
        let (name_start, version_start) = Self::parse_parts(&s)?;

        Ok(Self {
            value: Cow::Owned(s),
            name_start,
            version_start,
        })
    }

    // pub const fn parse_static_or_panic(s: &'static str) -> Self {
    //     let (name_start, version_start) = match Self::parse_parts(s) {
    //         Ok(x) => x,
    //         Err(_err) => {
    //             panic!("failed to parse entity kind");
    //         }
    //     };

    //     Self {
    //         value: Cow::Borrowed(s),
    //         name_start,
    //         version_start,
    //     }
    // }

    pub fn namespace(&self) -> &str {
        &self.value[..self.name_start - 1]
    }

    pub fn name(&self) -> &str {
        &self.value[self.name_start..self.version_start - 2]
    }

    pub fn version(&self) -> &str {
        &self.value[self.version_start..]
    }
}

impl serde::Serialize for EntityKind {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(&self.value)
    }
}

impl<'de> serde::Deserialize<'de> for EntityKind {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        EntityKind::parse(s).map_err(serde::de::Error::custom)
    }
}

impl schemars::JsonSchema for EntityKind {
    fn schema_name() -> String {
        "EntityKind".to_string()
    }

    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
            instance_type: Some(schemars::schema::InstanceType::String.into()),
            ..Default::default()
        })
    }
}

impl std::str::FromStr for EntityKind {
    type Err = EntityKindParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        EntityKind::parse(s)
    }
}

/// Error that can occur when parsing an [`EntityKind`].
#[derive(Clone, Debug)]
pub enum EntityKindParseError {
    MissingVersion,
    MissingNamespace,
    MissingName,
    InvalidNamespaceCharacter(char),
    InvalidNameCharacter(char),
    InvalidVersion,
}

impl std::fmt::Display for EntityKindParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EntityKindParseError::MissingVersion => write!(f, "missing version"),
            EntityKindParseError::MissingNamespace => write!(f, "missing namespace"),
            EntityKindParseError::MissingName => write!(f, "missing name"),
            EntityKindParseError::InvalidNamespaceCharacter(c) => {
                write!(f, "invalid character in namespace: '{c}'")
            }
            EntityKindParseError::InvalidNameCharacter(c) => {
                write!(f, "invalid character in name: '{}'", c)
            }
            EntityKindParseError::InvalidVersion => write!(f, "invalid version"),
        }
    }
}

impl std::error::Error for EntityKindParseError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_entity_kind() {
        let e = EntityKind::parse("a/b.v1").unwrap();
        assert_eq!(e.namespace(), "a");
        assert_eq!(e.name(), "b");
        assert_eq!(e.version(), "1");
    }
}