edge-schema 0.1.0

Shared schema types for Wasmer Edge.
Documentation
/// Represents an entity URI.
///
/// Must start with the [`EntityKind`], followed by a colon (:) and the path to
/// the entity.
///
/// The path may either be just a name, which must refer to an entity in the same
/// scope, a full path to the entity, separated by slashes, or just a UUID.
///
/// Example: my.namespace/MyEntityKind.v1:my-entity-name
/// Example: my.namespace/MyEntityKind.v1:parent/middleman/my-entity-name
/// Example: my.namespace/MyEntityKind.v1:parent/middleman/my-entity-name
#[derive(Clone)]
pub struct EntityUri {
    value: String,

    type_start: usize,
    version_start: usize,
    name_start: usize,
}

impl PartialEq for EntityUri {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

impl std::fmt::Debug for EntityUri {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("EntityUri").field(&self.value).finish()
    }
}

impl Eq for EntityUri {}

impl PartialOrd for EntityUri {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.value.cmp(&other.value))
    }
}

impl Ord for EntityUri {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.value.cmp(&other.value)
    }
}

impl std::hash::Hash for EntityUri {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.value.hash(state);
    }
}

impl EntityUri {
    pub fn as_str(&self) -> &str {
        &self.value
    }

    /// Allowed because this should be used over the Display impl...
    #[allow(clippy::inherent_to_string_shadow_display)]
    pub fn to_string(&self) -> String {
        self.value.clone()
    }

    pub fn into_string(self) -> String {
        self.value
    }

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

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

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

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

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

    pub fn new_kind_name(kind: &str, name: &str) -> Result<Self, EntityUriParseError> {
        // TODO(theduke): this should be more efficient!
        Self::parse(format!("{}:{}", kind, name))
    }

    pub fn parse(s: impl Into<String>) -> Result<Self, EntityUriParseError> {
        let s = s.into();
        let (kind, name) = s.split_once(':').ok_or_else(|| {
            EntityUriParseError::new(s.clone(), EntityUriParseErrorKind::MissingKindNameSeparator)
        })?;

        let (ns, kind) = kind.split_once('/').ok_or_else(|| {
            EntityUriParseError::new(
                s.clone(),
                EntityUriParseErrorKind::MissingKindNamespaceSeparator,
            )
        })?;

        let (ty, version) = kind.split_once('.').ok_or_else(|| {
            EntityUriParseError::new(s.clone(), EntityUriParseErrorKind::InvalidType)
        })?;

        if !is_valid_namespace(ns) {
            return Err(EntityUriParseError::new(
                s,
                EntityUriParseErrorKind::InvalidNamespace,
            ));
        }
        if !is_valid_type_name(ty) {
            return Err(EntityUriParseError::new(
                s,
                EntityUriParseErrorKind::InvalidType,
            ));
        }
        if !is_valid_version(version) {
            return Err(EntityUriParseError::new(
                s,
                EntityUriParseErrorKind::InvalidTypeVersion,
            ));
        }
        if !is_valid_name(name) {
            return Err(EntityUriParseError::new(
                s,
                EntityUriParseErrorKind::InvalidName,
            ));
        }

        let type_start = ns.len() + 1;
        let version_start = type_start + ty.len() + 2;
        let name_start = version_start + version.len();

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

impl std::fmt::Display for EntityUri {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.value.fmt(f)
    }
}

fn is_valid_namespace(s: &str) -> bool {
    s.split('.').all(|p| {
        !p.is_empty()
            && p.chars().all(|c| match c {
                'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => true,
                _ => false,
            })
    })
}

fn is_valid_type_name(s: &str) -> bool {
    !s.is_empty()
        && s.chars().all(|c| match c {
            'a'..='z' | 'A'..='Z' | '0'..='9' => true,
            _ => false,
        })
}

fn is_valid_version(s: &str) -> bool {
    !s.is_empty()
        && s.chars().all(|c| match c {
            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => true,
            _ => false,
        })
}

fn is_valid_name(name: &str) -> bool {
    !name.is_empty()
        && name.chars().all(|c| match c {
            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '+' => true,
            _ => false,
        })
}

impl std::str::FromStr for EntityUri {
    type Err = EntityUriParseError;

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

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

impl<'de> serde::Deserialize<'de> for EntityUri {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let value = String::deserialize(deserializer)?;

        Self::parse(value).map_err(serde::de::Error::custom)
    }
}

impl schemars::JsonSchema for EntityUri {
    fn schema_name() -> String {
        "EntityUri".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()
        })
    }
}

#[derive(Clone, Debug)]
pub struct EntityUriParseError {
    value: String,
    kind: EntityUriParseErrorKind,
}

impl EntityUriParseError {
    pub fn new(value: impl Into<String>, kind: EntityUriParseErrorKind) -> Self {
        Self {
            value: value.into(),
            kind,
        }
    }
}

impl std::fmt::Display for EntityUriParseError {
    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(_f, "invalid entity URI: '{}' ({:?})", self.value, self.kind)
    }
}

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

#[derive(Clone, Debug)]
pub enum EntityUriParseErrorKind {
    MissingKindNameSeparator,
    MissingKindNamespaceSeparator,
    InvalidNamespace,
    InvalidType,
    InvalidName,
    InvalidTypeVersion,
}

impl std::fmt::Display for EntityUriParseErrorKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            EntityUriParseErrorKind::MissingKindNameSeparator => {
                write!(f, "missing kind/name separator")
            }
            EntityUriParseErrorKind::MissingKindNamespaceSeparator => {
                write!(f, "missing kind/namespace separator")
            }
            EntityUriParseErrorKind::InvalidNamespace => write!(f, "invalid namespace"),
            EntityUriParseErrorKind::InvalidType => write!(f, "invalid type"),
            EntityUriParseErrorKind::InvalidName => write!(f, "invalid name"),
            EntityUriParseErrorKind::InvalidTypeVersion => write!(f, "invalid type version"),
        }
    }
}

/// Represents either an inline entity definition, or a reference to an entity.
#[derive(
    serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq, Eq, Clone, Debug,
)]
pub enum EntityOrRef<T> {
    #[serde(rename = "ref")]
    Ref(EntityUri),
    #[serde(rename = "item")]
    Item(T),
}

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

    #[test]
    fn test_entity_uri_parse() {
        let u = EntityUri::parse("my-ns.com/Ent.v1:lala123".to_string()).unwrap();
        assert_eq!(u.namespace(), "my-ns.com");
        assert_eq!(u.entity_type(), "Ent");
        assert_eq!(u.version(), "1");
        assert_eq!(u.name(), "lala123");
    }
}