wasmer-deploy-schema 0.0.21

Utilty crate that holds shared types and logic used in Wasmer Deploy.
Documentation
mod entity_type;
mod kind;
mod uri;

pub use self::{entity_type::*, kind::*, uri::*};

use std::collections::HashMap;

use anyhow::Context;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;

/// Common entity metadata.
///
/// This data is not generic, and is the same for all entity kinds.
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct EntityMeta {
    /// Name of the entity.
    ///
    /// This is only unique within the scope of the entity.
    pub name: String,

    /// Long description.
    ///
    /// Should be either plain text or markdown.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Labels are used to organize entities.
    /// They are a set of simple key/value pairs.
    #[serde(default)]
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub labels: HashMap<String, String>,

    /// Annotations are used to attach arbitrary metadata to entities.
    /// They can contain arbitrary (json-encodable) data.
    #[serde(default)]
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub annotations: HashMap<String, serde_json::Value>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent: Option<EntityUri>,
}

impl EntityMeta {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            description: None,
            labels: Default::default(),
            annotations: Default::default(),
            parent: None,
        }
    }

    pub fn with_annotations<I, K, V>(mut self, annotations: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<serde_json::Value>,
    {
        self.annotations = annotations
            .into_iter()
            .map(|(k, v)| (k.into(), v.into()))
            .collect();
        self
    }
}

/// An entity with associated data.
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct Entity<D, C = serde_json::Value> {
    /// Common entity metadata.
    pub meta: EntityMeta,
    /// Specification of the entity.
    pub spec: D,
    /// Inline child entity specs.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children: Option<Vec<C>>,
}

impl<D> Entity<D> {
    pub fn new_with_name(name: impl Into<String>, spec: D) -> Self {
        Self {
            meta: EntityMeta::new(name),
            spec,
            children: None,
        }
    }

    pub fn try_map_spec<F, O, E>(self, f: F) -> Result<Entity<O>, E>
    where
        F: FnOnce(D) -> Result<O, E>,
    {
        Ok(Entity {
            meta: self.meta,
            spec: f(self.spec)?,
            children: self.children,
        })
    }
}

pub type JsonEntity = Entity<serde_json::Value, serde_json::Value>;

impl<D, C> Entity<D, C>
where
    D: EntityDescriptorConst,
{
    pub fn uri(&self) -> String {
        format!("{}:{}", D::KIND, self.meta.name)
    }

    pub fn build_uri(&self) -> EntityUri {
        // NOTE: using unwrap here because an invalid kind in Self::KIND is a
        // user error.
        EntityUri::parse(self.uri()).unwrap()
    }
}

impl<D, C> Entity<D, C>
where
    D: EntityDescriptorConst + serde::Serialize,
    C: serde::Serialize,
{
    /// Convert this type to yaml, injecting the kind into the output.
    // TODO: make this redundant with a custom Serialize impl!
    pub fn to_json_map(&self) -> Result<serde_json::Value, serde_json::Error> {
        // Constructing a custom object to properly order the fields.
        // (kind, then meta, then spec)
        let mut map = serde_json::Value::Object(Default::default());
        map["kind"] = D::KIND.into();
        map["meta"] = serde_json::to_value(&self.meta)?;
        map["spec"] = serde_json::to_value(&self.spec)?;

        Ok(map)
    }

    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        let map = self.to_json_map()?;
        serde_json::to_string_pretty(&map)
    }

    /// Convert this type to yaml, injecting the kind into the output.
    // TODO: make this redundant with a custom Serialize impl!
    pub fn to_yaml_map(&self) -> Result<serde_yaml::Mapping, serde_yaml::Error> {
        // Constructing a custom object to properly order the fields.
        // (kind, then meta, then spec)
        let mut map = serde_yaml::Mapping::new();
        map.insert("kind".into(), D::KIND.into());
        map.insert("meta".into(), serde_yaml::to_value(&self.meta)?);
        map.insert("spec".into(), serde_yaml::to_value(&self.spec)?);

        Ok(map)
    }

    /// Convert this type to yaml, injecting the kind into the output.
    // TODO: make this redundant with a custom Serialize impl!
    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
        let map = self.to_yaml_map()?;
        serde_yaml::to_string(&map)
    }

    /// Converts this type into a generic entity
    pub fn to_generic(&self) -> Result<GenericEntity, serde_json::Error> {
        // TODO: @Christoph - the children parser needs to be implemented
        assert!(self.children.is_none());

        Ok(GenericEntity {
            kind: D::KIND.to_string(),
            meta: self.meta.clone(),
            spec: serde_json::to_value(&self.spec)?,
            children: None,
        })
    }
}

/// Generic, untyped entity.
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct GenericEntity {
    pub kind: String,

    /// Common entity metadata.
    pub meta: EntityMeta,
    /// Specification of the entity.
    pub spec: serde_json::Value,
    /// Inline child entity specs.
    pub children: Option<Vec<GenericEntity>>,
}

impl GenericEntity {
    pub fn build_uri_str(&self) -> String {
        format!("{}:{}", self.kind, self.meta.name)
    }

    pub fn build_uri(&self) -> Result<EntityUri, EntityUriParseError> {
        EntityUri::new_kind_name(&self.kind, &self.meta.name)
    }
}

/// An entity with associated data, including state.
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct FullEntity<D, S = (), C = serde_json::Value> {
    /// Common entity metadata.
    pub meta: EntityMeta,
    /// Specification of the entity.
    pub spec: D,
    /// Inline child entity specs.
    pub children: Option<Vec<C>>,
    pub state: EntityState<S>,
}

/// State of an entity.
///
/// Contains a `main` state, which will be managed by the owning service that
/// manages the entity.
///
/// Additional services may inject their own state, which will be found in
/// [`Self::components`].
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct EntityState<S = ()> {
    /// Globally unique UUID.
    pub uid: Uuid,

    /// Version of the entity.
    /// All modifications to metadata or spec will increment this version.
    pub entity_version: u64,

    /// UUID of the parent entity.
    /// This is only set if the entity is a child of another entity.
    pub parent_uid: Option<Uuid>,

    /// Creation timestamp.
    #[serde(deserialize_with = "time::serde::timestamp::deserialize")]
    #[schemars(with = "u64")]
    pub created_at: OffsetDateTime,
    /// Last update timestamp.
    /// Will be set on each metadata or spec change, but not on state changes.
    #[serde(serialize_with = "time::serde::timestamp::serialize")]
    #[schemars(with = "u64")]
    pub updated_at: OffsetDateTime,

    /// The primary state of the entity, managed by the owning service.
    pub main: Option<EntityStateComponent<S>>,

    /// Additional entity states, managed by services other than the entity owners.
    pub components: HashMap<String, EntityStateComponent>,
}

/// Single component of an entities state.
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct EntityStateComponent<T = serde_json::Value> {
    /// Version of this state.
    /// Will be incremented on each change.
    pub state_version: u64,
    /// Update timestamp.
    #[serde(with = "time::serde::timestamp")]
    #[schemars(with = "u64")]
    pub updated_at: OffsetDateTime,
    /// The actual state data.
    pub data: T,
}

/// A marker trait for entity types.
///
/// Should be implementes on the struct representing the entities spec.
pub trait EntityDescriptorConst {
    const NAMESPACE: &'static str;
    const NAME: &'static str;
    const VERSION: &'static str;
    const KIND: &'static str;

    /// Entity specification.
    type Spec: Serialize + DeserializeOwned + JsonSchema + Clone + PartialEq + Eq + std::fmt::Debug;
    /// The main entity state.
    type State: Serialize + DeserializeOwned + JsonSchema + Clone + PartialEq + Eq + std::fmt::Debug;

    fn json_schema() -> schemars::schema::RootSchema {
        schemars::schema_for!(Entity<Self::Spec>)
    }

    fn build_uri_str(name: &str) -> String {
        format!("{}:{}", Self::KIND, name)
    }

    fn build_uri(name: &str) -> Result<EntityUri, EntityUriParseError> {
        EntityUri::new_kind_name(Self::KIND, name)
    }

    /// Build the name that is used for the EntityTypeSpec representing this type.
    fn type_name() -> String {
        // TODO: this should be an additional const...
        format!("{}-{}-v{}", Self::NAMESPACE, Self::NAME, Self::VERSION)
    }

    fn build_type_descriptor() -> Entity<EntityTypeSpec>
    where
        Self: JsonSchema + Sized,
    {
        EntityTypeSpec::build_for_type::<Self>()
    }
}

/// Deserialize a typed entity from YAML.
pub fn deserialize_entity_yaml_typed<T>(input: &str) -> Result<Entity<T>, anyhow::Error>
where
    T: EntityDescriptorConst + DeserializeOwned,
{
    let raw: serde_yaml::Value = serde_yaml::from_str(input).context("invalid YAML")?;
    let kind = raw
        .get("kind")
        .context("missing 'kind' field in yaml")?
        .as_str()
        .context("'kind' field is not a string")?;

    if kind != T::KIND {
        anyhow::bail!("expected kind '{}' but got '{}'", T::KIND, kind);
    }

    let out = serde_yaml::from_value(raw).context("could not deserialize to entity data")?;
    Ok(out)
}