edge-schema 0.1.0

Shared schema types for Wasmer Edge.
Documentation
use std::collections::HashMap;

use anyhow::Context;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

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

pub const APP_ID_PREFIX: &str = "da_";

/// Well-known annotations used by the backend.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct AppMeta {
    pub app_id: String,
    pub app_version_id: String,
}

impl AppMeta {
    /// Well-known annotation for specifying the app id.
    pub const ANNOTATION_BACKEND_APP_ID: &'static str = "wasmer.io/app_id";

    /// Well-known annotation for specifying the app version id.
    pub const ANNOTATION_BACKEND_APP_VERSION_ID: &'static str = "wasmer.io/app_version_id";

    /// Extract annotations from a generic annotation map.
    pub fn try_from_annotations(
        map: &HashMap<String, serde_json::Value>,
    ) -> Result<Self, anyhow::Error> {
        let app_id = map
            .get(Self::ANNOTATION_BACKEND_APP_ID)
            .context("missing annotation for app id")?
            .as_str()
            .context("app id annotation is not a string")?
            .to_string();

        let app_version_id = map
            .get(Self::ANNOTATION_BACKEND_APP_VERSION_ID)
            .context("missing annotation for version id")?
            .as_str()
            .context("version id annotation is not a string")?
            .to_string();

        Ok(Self {
            app_id,
            app_version_id,
        })
    }

    pub fn try_from_entity<T>(entity: &Entity<T>) -> Result<Self, anyhow::Error> {
        Self::try_from_annotations(&entity.meta.annotations)
    }

    pub fn new(app_id: String, app_version_id: String) -> Self {
        Self {
            app_id,
            app_version_id,
        }
    }

    pub fn to_annotations_map(self) -> HashMap<String, serde_json::Value> {
        let mut map = HashMap::new();
        map.insert(
            Self::ANNOTATION_BACKEND_APP_ID.to_string(),
            serde_json::Value::String(self.app_id),
        );
        map.insert(
            Self::ANNOTATION_BACKEND_APP_VERSION_ID.to_string(),
            serde_json::Value::String(self.app_version_id),
        );
        map
    }
}

/// Describes a backend application.
///
/// Will usually be converted from [`super::AppConfigV1`].
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct AppV1Spec {
    /// 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>,

    /// Domains where the app is accessible.
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub domains: Vec<String>,

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

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

impl EntityDescriptorConst for AppV1Spec {
    const NAMESPACE: &'static str = "wasmer.io";
    const NAME: &'static str = "App";
    const VERSION: &'static str = "1-alpha1";
    const KIND: &'static str = "wasmer.io/App.v1";

    type Spec = AppV1Spec;
    type State = AppStateV1;
}

pub type AppV1 = super::Entity<AppV1Spec, AnyEntity>;

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

    use crate::schema::{EntityMeta, EnvVarV1};

    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:
  workload:
    source: theduke/amaze
  domains:
    - a.com
    - b.com
"#;

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

        assert_eq!(
            a1,
            AppV1 {
                meta: EntityMeta::new("my-app"),
                spec: AppV1Spec {
                    aliases: Vec::new(),
                    domains: vec!["a.com".to_string(), "b.com".to_string()],
                    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:
  aliases:
    - a
    - b
  workload:
    source: "theduke/my-app"
"#;

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

        let expected = AppV1 {
            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: AppV1Spec {
                aliases: vec!["a".to_string(), "b".to_string()],
                domains: Vec::new(),
                workload: WorkloadV2 {
                    source: "theduke/my-app".parse().unwrap(),
                    capabilities: Default::default(),
                },
            },
            children: None,
        };

        assert_eq!(a1, expected,);
    }

    #[test]
    fn test_deser_app_v1_with_cli_and_env_cap() {
        let raw = r#"
kind: wasmer.io/App.v1
meta:
  description: ''
  name: christoph/pyenv-dump
spec:
  workload:
    capabilities:
      wasi:
        cli_args:
        - /src/main.py
        env_vars:
        - name: PORT
          value: '80'
    source: wasmer-tests/python-env-dump@0.3.6

"#;

        let a1 = serde_yaml::from_str::<AppV1>(raw).unwrap();

        let expected = AppV1 {
            meta: EntityMeta {
                uid: None,
                name: "christoph/pyenv-dump".to_string(),
                description: Some("".to_string()),
                labels: Default::default(),
                annotations: Default::default(),
                parent: None,
            },
            spec: AppV1Spec {
                aliases: Vec::new(),
                domains: Vec::new(),
                workload: WorkloadV2 {
                    source: "wasmer-tests/python-env-dump@0.3.6".parse().unwrap(),
                    capabilities: crate::schema::CapabilityMapV1 {
                        wasi: Some(crate::schema::CapabilityWasiV1 {
                            cli_args: Some(vec!["/src/main.py".to_string()]),
                            env_vars: Some(vec![EnvVarV1 {
                                name: "PORT".to_string(),
                                source: crate::schema::EnvVarSourceV1::Value("80".to_string()),
                            }]),
                        }),
                        ..Default::default()
                    },
                },
            },
            children: None,
        };

        assert_eq!(a1, expected,);
    }
}