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;
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct EntityMeta {
pub uid: Option<Uuid>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub labels: HashMap<String, String>,
#[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 {
uid: None,
name: name.into(),
description: None,
labels: Default::default(),
annotations: Default::default(),
parent: None,
}
}
pub fn with_uid(mut self, uid: Uuid) -> Self {
self.uid = Some(uid);
self
}
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
}
}
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct Entity<D, C = serde_json::Value> {
pub meta: EntityMeta,
pub spec: D,
#[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 fn uid(&self) -> Option<Uuid> {
self.meta.uid
}
}
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 {
EntityUri::parse(self.uri()).unwrap()
}
}
impl<D, C> Entity<D, C>
where
D: EntityDescriptorConst + serde::Serialize,
C: serde::Serialize,
{
pub fn to_json_map(&self) -> Result<serde_json::Value, serde_json::Error> {
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)
}
pub fn to_yaml_map(&self) -> Result<serde_yaml::Mapping, serde_yaml::Error> {
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)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
let map = self.to_yaml_map()?;
serde_yaml::to_string(&map)
}
pub fn to_generic(&self) -> Result<GenericEntity, serde_json::Error> {
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,
})
}
}
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct GenericEntity {
pub kind: String,
pub meta: EntityMeta,
pub spec: serde_json::Value,
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)
}
}
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct FullEntity<D, S = (), C = serde_json::Value> {
pub meta: EntityMeta,
pub spec: D,
pub children: Option<Vec<C>>,
pub state: EntityState<S>,
}
impl<D, S, C> FullEntity<D, S, C> {
pub fn uid(&self) -> Uuid {
self.meta.uid.unwrap_or_default()
}
pub fn with_main_state(mut self, state: S) -> Self {
let now = OffsetDateTime::now_utc();
let state = if let Some(mut s) = self.state.main.take() {
s.updated_at = now;
s.state_version += 1;
s.data = state;
s
} else {
EntityStateComponent {
state_version: 1,
updated_at: now,
data: state,
}
};
self.state.main = Some(state);
self
}
}
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct EntityState<S = ()> {
pub entity_version: u64,
pub parent_uid: Option<Uuid>,
#[serde(with = "time::serde::timestamp")]
#[schemars(with = "u64")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::timestamp")]
#[schemars(with = "u64")]
pub updated_at: OffsetDateTime,
pub main: Option<EntityStateComponent<S>>,
pub components: HashMap<String, EntityStateComponent>,
}
#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
pub struct EntityStateComponent<T = serde_json::Value> {
pub state_version: u64,
#[serde(with = "time::serde::timestamp")]
#[schemars(with = "u64")]
pub updated_at: OffsetDateTime,
pub data: T,
}
pub trait EntityDescriptorConst {
const NAMESPACE: &'static str;
const NAME: &'static str;
const VERSION: &'static str;
const KIND: &'static str;
type Spec: Serialize + DeserializeOwned + JsonSchema + Clone + PartialEq + Eq + std::fmt::Debug;
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)
}
fn type_name() -> String {
format!("{}-{}-v{}", Self::NAMESPACE, Self::NAME, Self::VERSION)
}
fn build_type_descriptor() -> Entity<EntityTypeSpec>
where
Self: JsonSchema + Sized,
{
EntityTypeSpec::build_for_type::<Self>()
}
}
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)
}