edge-schema 0.1.0

Shared schema types for Wasmer Edge.
Documentation
use serde::{Deserialize, Serialize};

/// Parsed representation of a WebC package source.
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema, Hash)]
pub struct WebcIdent {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repository: Option<url::Url>,
    pub namespace: String,
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tag: Option<String>,
}

impl WebcIdent {
    /// Build the ident for a package.
    ///
    /// Format: NAMESPACE/NAME[@version|hash]
    ///
    /// If prefer_hash is true, the ident will use the signature hash instead of
    /// the version if both are available.
    pub fn build_identifier(&self) -> String {
        let mut ident = format!("{}/{}", self.namespace, self.name);

        if let Some(tag) = &self.tag {
            ident.push('@');
            ident.push_str(tag);
        }
        ident
    }

    /// The the url where the webc package can be downloaded.
    ///
    /// NOTE: returns [`Option::None`] if [`Self::repository`] is not set.
    ///
    /// Private packages will also require an auth token for downloading.
    pub fn build_download_url(&self) -> Option<url::Url> {
        let mut url = self.repository.as_ref()?.clone();
        let ident = self.build_identifier();
        let original_path = url.path().strip_suffix('/').unwrap_or(url.path());
        let final_path = format!("{original_path}/{ident}");

        url.set_path(&final_path);
        Some(url)
    }

    /// The the url where the webc package can be downloaded.
    ///
    /// Private packages will also require an auth token for downloading.
    pub fn build_download_url_with_default_registry(&self, default_reg: &url::Url) -> url::Url {
        let mut url = self
            .repository
            .as_ref()
            .cloned()
            .unwrap_or_else(|| default_reg.clone());
        let ident = self.build_identifier();
        let original_path = url.path().strip_suffix('/').unwrap_or(url.path());
        let final_path = format!("{original_path}/{ident}");

        url.set_path(&final_path);
        url
    }

    pub fn parse(value: &str) -> Result<Self, WebcParseError> {
        let (rest, tag_opt) = value
            .trim()
            .rsplit_once('@')
            .map(|(x, y)| (x, if y.is_empty() { None } else { Some(y) }))
            .unwrap_or((value, None));

        let mut parts = rest.rsplit('/');

        let name = parts
            .next()
            .map(|x| x.trim())
            .filter(|x| !x.is_empty())
            .ok_or_else(|| WebcParseError::new(value, "package name is required"))?;

        let namespace = parts
            .next()
            .map(|x| x.trim())
            .filter(|x| !x.is_empty())
            .ok_or_else(|| WebcParseError::new(value, "package namespace is required"))?;

        let rest = parts.rev().collect::<Vec<_>>().join("/");
        let repository = if rest.is_empty() {
            None
        } else {
            let registry = rest.trim();
            let full_registry =
                if registry.starts_with("http://") || registry.starts_with("https://") {
                    registry.to_string()
                } else {
                    format!("https://{}", registry)
                };

            let registry_url = url::Url::parse(&full_registry)
                .map_err(|e| WebcParseError::new(value, format!("invalid registry url: {}", e)))?;
            Some(registry_url)
        };

        Ok(Self {
            repository,
            namespace: namespace.to_string(),
            name: name.to_string(),
            tag: tag_opt.map(|x| x.to_string()),
        })
    }
}

impl std::fmt::Display for WebcIdent {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(url) = self.build_download_url() {
            write!(f, "{}", url)
        } else {
            write!(f, "{}", self.build_identifier())
        }
    }
}

/// Wrapper around [`WebcPackageIdentifierV1`].
///
/// The inner value is serialized and deserialized as a [`String`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StringWebcIdent(pub WebcIdent);

impl StringWebcIdent {
    pub fn parse(value: &str) -> Result<Self, WebcParseError> {
        Ok(Self(WebcIdent::parse(value)?))
    }
}

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

impl schemars::JsonSchema for StringWebcIdent {
    fn schema_name() -> String {
        "StringWebcPackageIdent".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 From<StringWebcIdent> for WebcIdent {
    fn from(x: StringWebcIdent) -> Self {
        x.0
    }
}

impl From<WebcIdent> for StringWebcIdent {
    fn from(x: WebcIdent) -> Self {
        Self(x)
    }
}

impl serde::Serialize for StringWebcIdent {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let val = self.0.to_string();
        serializer.serialize_str(&val)
    }
}

impl<'de> serde::Deserialize<'de> for StringWebcIdent {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let ident = WebcIdent::parse(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
        Ok(Self(ident))
    }
}

impl std::str::FromStr for StringWebcIdent {
    type Err = WebcParseError;

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

#[derive(PartialEq, Eq, Debug)]
pub struct WebcParseError {
    value: String,
    message: String,
}

impl WebcParseError {
    fn new(value: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            value: value.into(),
            message: message.into(),
        }
    }
}

impl std::fmt::Display for WebcParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "could not parse webc package specifier '{}': {}",
            self.value, self.message
        )
    }
}

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

#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema)]
pub struct WebcPackagePathV1 {
    /// Volume the file is contained in.
    /// (webc packages can have multiple file volumes)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub volume: Option<String>,
    pub path: String,
}

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

    #[test]
    fn test_parse_webc_ident() {
        // Success cases.

        assert_eq!(
            WebcIdent::parse("ns/name").unwrap(),
            WebcIdent {
                repository: None,
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: None,
            }
        );

        assert_eq!(
            WebcIdent::parse("ns/name@").unwrap(),
            WebcIdent {
                repository: None,
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: None,
            },
            "empty tag should be parsed as None"
        );

        assert_eq!(
            WebcIdent::parse("ns/name@tag").unwrap(),
            WebcIdent {
                repository: None,
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: Some("tag".to_string()),
            }
        );

        assert_eq!(
            WebcIdent::parse("reg.com/ns/name").unwrap(),
            WebcIdent {
                repository: Some(url::Url::parse("https://reg.com").unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: None,
            }
        );

        assert_eq!(
            WebcIdent::parse("reg.com/ns/name@tag").unwrap(),
            WebcIdent {
                repository: Some(url::Url::parse("https://reg.com").unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: Some("tag".to_string()),
            }
        );

        assert_eq!(
            WebcIdent::parse("https://reg.com/ns/name").unwrap(),
            WebcIdent {
                repository: Some(url::Url::parse("https://reg.com").unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: None,
            }
        );

        assert_eq!(
            WebcIdent::parse("https://reg.com/ns/name@tag").unwrap(),
            WebcIdent {
                repository: Some(url::Url::parse("https://reg.com").unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: Some("tag".to_string()),
            }
        );

        assert_eq!(
            WebcIdent::parse("http://reg.com/ns/name").unwrap(),
            WebcIdent {
                repository: Some(url::Url::parse("http://reg.com").unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: None,
            }
        );

        assert_eq!(
            WebcIdent::parse("http://reg.com/ns/name@tag").unwrap(),
            WebcIdent {
                repository: Some(url::Url::parse("http://reg.com").unwrap()),
                namespace: "ns".to_string(),
                name: "name".to_string(),
                tag: Some("tag".to_string()),
            }
        );

        // Failure cases.

        assert_eq!(
            WebcIdent::parse("alpha"),
            Err(WebcParseError::new(
                "alpha",
                "package namespace is required"
            ))
        );

        assert_eq!(
            WebcIdent::parse(""),
            Err(WebcParseError::new("", "package name is required"))
        );
    }

    #[test]
    fn test_serde_serialize_webc_str_ident_with_repo() {
        // Serialize
        let ident = StringWebcIdent(WebcIdent {
            repository: Some(url::Url::parse("https://wapm.io").unwrap()),
            namespace: "ns".to_string(),
            name: "name".to_string(),
            tag: None,
        });

        let raw = serde_json::to_string(&ident).unwrap();
        assert_eq!(raw, "\"https://wapm.io/ns/name\"");

        let ident2 = serde_json::from_str::<StringWebcIdent>(&raw).unwrap();
        assert_eq!(ident, ident2);
    }

    #[test]
    fn test_serde_serialize_webc_str_ident_without_repo() {
        // Serialize
        let ident = StringWebcIdent(WebcIdent {
            repository: None,
            namespace: "ns".to_string(),
            name: "name".to_string(),
            tag: None,
        });

        let raw = serde_json::to_string(&ident).unwrap();
        assert_eq!(raw, "\"ns/name\"");

        let ident2 = serde_json::from_str::<StringWebcIdent>(&raw).unwrap();
        assert_eq!(ident, ident2);
    }
}