edge-schema 0.1.0

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

use super::{entity::EntityDescriptorConst, AnyEntity, WorkloadV2};

/// Describes a backend application.
///
/// Will usually be converted from [`super::AppConfigV1`].
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct AppVersionV1Spec {
    pub app_id: String,
    pub app_version_id: String,

    /// A list of alias names for the app.
    /// Aliases can be used to access the app through app domains.
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub aliases: Vec<String>,

    /// The workload to execute.
    pub workload: WorkloadV2,
}

#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct AppVersionV1State {}

impl EntityDescriptorConst for AppVersionV1Spec {
    const NAMESPACE: &'static str = "wasmer.io";
    const NAME: &'static str = "AppVersion";
    const VERSION: &'static str = "1";
    const KIND: &'static str = "wasmer.io/AppVersion.v1";

    type Spec = AppVersionV1Spec;
    type State = AppVersionV1State;
}

pub type AppVersionV1 = super::Entity<AppVersionV1Spec, AnyEntity>;

/// Parse either a deprecated AppV1 or a new AppVersionV1 from yaml.
pub fn parse_upcast_app_version_yaml(yaml: &str) -> Result<AppVersionV1, anyhow::Error> {
    let value: serde_yaml::Value =
        serde_yaml::from_str(yaml).context("could not parse app version: invalid yaml")?;

    let kind = value.get("kind").and_then(|x| x.as_str());

    if kind == Some(AppVersionV1Spec::KIND) {
        serde_yaml::from_value::<AppVersionV1>(value)
            .context("could not parse app version: invalid app v1 schema")
    } else if kind == Some(super::AppV1Spec::KIND) {
        let mut app: super::AppV1 = serde_yaml::from_value(value)
            .context("could not parse app version: invalid app v1 schema")?;

        let annotations = super::AppMeta::try_from_annotations(&app.meta.annotations)?;
        app.meta
            .annotations
            .remove(super::AppMeta::ANNOTATION_BACKEND_APP_ID);
        app.meta
            .annotations
            .remove(super::AppMeta::ANNOTATION_BACKEND_APP_VERSION_ID);

        Ok(AppVersionV1 {
            meta: app.meta,
            spec: AppVersionV1Spec {
                app_id: annotations.app_id,
                app_version_id: annotations.app_version_id,
                aliases: app.spec.aliases,
                workload: app.spec.workload,
            },
            children: None,
        })
    } else if let Some(other) = kind {
        anyhow::bail!("could not parse app version: unknown kind: {other}");
    } else {
        anyhow::bail!("could not parse app version: no kind field in yaml");
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use crate::schema::EntityMeta;

    use super::*;

    /// Tests serilization and deserialization of the [`AppV1`] struct.
    #[test]
    fn test_deser_app_v1_sparse() {
        let inp = r#"
kind: wasmer.io/App.v1
meta:
  name: my-app
spec:
  app_id: da_123
  app_version_id: dav_123
  workload:
    source: theduke/amaze
"#;

        let a1 = serde_yaml::from_str::<AppVersionV1>(inp).unwrap();

        assert_eq!(
            a1,
            AppVersionV1 {
                meta: EntityMeta::new("my-app"),
                spec: AppVersionV1Spec {
                    app_id: "da_123".to_string(),
                    app_version_id: "dav_123".to_string(),
                    aliases: Vec::new(),
                    workload: crate::schema::WorkloadV2 {
                        source: "theduke/amaze".parse().unwrap(),
                        capabilities: Default::default(),
                    },
                },
                children: None,
            },
        );
    }

    #[test]
    fn test_deser_app_v1_full() {
        let inp = r#"
kind: wasmer.io/App.v1
meta:
  name: my-app
  description: hello
  labels:
    "my/label": "value"
  annotations:
    "my/annotation": {nested: [1, 2, 3]}
spec:
  app_id: da_123
  app_version_id: dav_123
  aliases:
    - a
    - b
  workload:
    source: "theduke/my-app"
"#;

        let a1 = serde_yaml::from_str::<AppVersionV1>(inp).unwrap();

        let expected = AppVersionV1 {
            meta: EntityMeta {
                uid: None,
                name: "my-app".to_string(),
                description: Some("hello".to_string()),
                labels: vec![("my/label".to_string(), "value".to_string())]
                    .into_iter()
                    .collect(),
                annotations: vec![(
                    "my/annotation".to_string(),
                    serde_json::json!({
                        "nested": [1, 2, 3],
                    }),
                )]
                .into_iter()
                .collect(),
                parent: None,
            },
            spec: AppVersionV1Spec {
                app_id: "da_123".to_string(),
                app_version_id: "dav_123".to_string(),
                aliases: vec!["a".to_string(), "b".to_string()],
                workload: WorkloadV2 {
                    source: "theduke/my-app".parse().unwrap(),
                    capabilities: Default::default(),
                },
            },
            children: None,
        };

        assert_eq!(a1, expected);
    }
}