edge_schema/schema/entity/
mod.rs

1mod entity_type;
2mod kind;
3mod uri;
4
5pub use self::{entity_type::*, kind::*, uri::*};
6
7use std::{collections::HashMap, path::Path};
8
9use anyhow::Context;
10use schemars::JsonSchema;
11use serde::{de::DeserializeOwned, Deserialize, Serialize};
12use time::OffsetDateTime;
13use uuid::Uuid;
14
15/// Common entity metadata.
16///
17/// This data is not generic, and is the same for all entity kinds.
18#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
19pub struct EntityMeta {
20    /// Optional unique id.
21    pub uid: Option<Uuid>,
22
23    /// Name of the entity.
24    ///
25    /// This is only unique within the scope of the entity.
26    pub name: String,
27
28    /// Long description.
29    ///
30    /// Should be either plain text or markdown.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub description: Option<String>,
33
34    /// Labels are used to organize entities.
35    /// They are a set of simple key/value pairs.
36    #[serde(default)]
37    #[serde(skip_serializing_if = "HashMap::is_empty")]
38    pub labels: HashMap<String, String>,
39
40    /// Annotations are used to attach arbitrary metadata to entities.
41    /// They can contain arbitrary (json-encodable) data.
42    #[serde(default)]
43    #[serde(skip_serializing_if = "HashMap::is_empty")]
44    pub annotations: HashMap<String, serde_json::Value>,
45
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub parent: Option<EntityUri>,
48}
49
50impl EntityMeta {
51    pub fn new(name: impl Into<String>) -> Self {
52        Self {
53            uid: None,
54            name: name.into(),
55            description: None,
56            labels: Default::default(),
57            annotations: Default::default(),
58            parent: None,
59        }
60    }
61
62    pub fn with_uid(mut self, uid: Uuid) -> Self {
63        self.uid = Some(uid);
64        self
65    }
66
67    pub fn with_annotations<I, K, V>(mut self, annotations: I) -> Self
68    where
69        I: IntoIterator<Item = (K, V)>,
70        K: Into<String>,
71        V: Into<serde_json::Value>,
72    {
73        self.annotations = annotations
74            .into_iter()
75            .map(|(k, v)| (k.into(), v.into()))
76            .collect();
77        self
78    }
79}
80
81/// An entity with associated data.
82#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
83pub struct Entity<D, C = serde_json::Value> {
84    /// Common entity metadata.
85    pub meta: EntityMeta,
86    /// Specification of the entity.
87    pub spec: D,
88    /// Inline child entity specs.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub children: Option<Vec<C>>,
91}
92
93impl<D> Entity<D> {
94    pub fn new_with_name(name: impl Into<String>, spec: D) -> Self {
95        Self {
96            meta: EntityMeta::new(name),
97            spec,
98            children: None,
99        }
100    }
101
102    pub fn try_map_spec<F, O, E>(self, f: F) -> Result<Entity<O>, E>
103    where
104        F: FnOnce(D) -> Result<O, E>,
105    {
106        Ok(Entity {
107            meta: self.meta,
108            spec: f(self.spec)?,
109            children: self.children,
110        })
111    }
112
113    pub fn uid(&self) -> Option<Uuid> {
114        self.meta.uid
115    }
116}
117
118pub type JsonEntity = Entity<serde_json::Value, serde_json::Value>;
119
120impl<D, C> Entity<D, C>
121where
122    D: EntityDescriptorConst,
123{
124    pub fn uri(&self) -> String {
125        format!("{}:{}", D::KIND, self.meta.name)
126    }
127
128    pub fn build_uri(&self) -> EntityUri {
129        // NOTE: using unwrap here because an invalid kind in Self::KIND is a
130        // user error.
131        EntityUri::parse(self.uri()).unwrap()
132    }
133}
134
135impl<D, C> Entity<D, C>
136where
137    D: EntityDescriptorConst + serde::Serialize,
138    C: serde::Serialize,
139{
140    /// Convert this type to yaml, injecting the kind into the output.
141    // TODO: make this redundant with a custom Serialize impl!
142    pub fn to_json_map(&self) -> Result<serde_json::Value, serde_json::Error> {
143        // Constructing a custom object to properly order the fields.
144        // (kind, then meta, then spec)
145        let mut map = serde_json::Value::Object(Default::default());
146        map["kind"] = D::KIND.into();
147        map["meta"] = serde_json::to_value(&self.meta)?;
148        map["spec"] = serde_json::to_value(&self.spec)?;
149
150        Ok(map)
151    }
152
153    pub fn to_json(&self) -> Result<String, serde_json::Error> {
154        let map = self.to_json_map()?;
155        serde_json::to_string_pretty(&map)
156    }
157
158    /// Convert this type to yaml, injecting the kind into the output.
159    // TODO: make this redundant with a custom Serialize impl!
160    pub fn to_yaml_map(&self) -> Result<serde_yaml::Mapping, serde_yaml::Error> {
161        // Constructing a custom object to properly order the fields.
162        // (kind, then meta, then spec)
163        let mut map = serde_yaml::Mapping::new();
164        map.insert("kind".into(), D::KIND.into());
165        map.insert("meta".into(), serde_yaml::to_value(&self.meta)?);
166        map.insert("spec".into(), serde_yaml::to_value(&self.spec)?);
167
168        Ok(map)
169    }
170
171    /// Convert this type to yaml, injecting the kind into the output.
172    // TODO: make this redundant with a custom Serialize impl!
173    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
174        let map = self.to_yaml_map()?;
175        serde_yaml::to_string(&map)
176    }
177
178    /// Converts this type into a generic entity
179    pub fn to_generic(&self) -> Result<GenericEntity, serde_json::Error> {
180        // TODO: @Christoph - the children parser needs to be implemented
181        assert!(self.children.is_none());
182
183        Ok(GenericEntity {
184            kind: D::KIND.to_string(),
185            meta: self.meta.clone(),
186            spec: serde_json::to_value(&self.spec)?,
187            children: None,
188        })
189    }
190}
191
192/// Generic, untyped entity.
193#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
194pub struct GenericEntity {
195    pub kind: String,
196
197    /// Common entity metadata.
198    pub meta: EntityMeta,
199    /// Specification of the entity.
200    pub spec: serde_json::Value,
201    /// Inline child entity specs.
202    pub children: Option<Vec<GenericEntity>>,
203}
204
205impl GenericEntity {
206    pub fn build_uri_str(&self) -> String {
207        format!("{}:{}", self.kind, self.meta.name)
208    }
209
210    pub fn build_uri(&self) -> Result<EntityUri, EntityUriParseError> {
211        EntityUri::new_kind_name(&self.kind, &self.meta.name)
212    }
213}
214
215/// An entity with associated data, including state.
216#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
217pub struct FullEntity<D, S = (), C = serde_json::Value> {
218    /// Common entity metadata.
219    pub meta: EntityMeta,
220    /// Specification of the entity.
221    pub spec: D,
222    /// Inline child entity specs.
223    pub children: Option<Vec<C>>,
224    pub state: EntityState<S>,
225}
226
227impl<D, S, C> FullEntity<D, S, C> {
228    pub fn uid(&self) -> Uuid {
229        self.meta.uid.unwrap_or_default()
230    }
231
232    pub fn with_main_state(mut self, state: S) -> Self {
233        let now = OffsetDateTime::now_utc();
234        let state = if let Some(mut s) = self.state.main.take() {
235            s.updated_at = now;
236            s.state_version += 1;
237            s.data = state;
238            s
239        } else {
240            EntityStateComponent {
241                state_version: 1,
242                updated_at: now,
243                data: state,
244            }
245        };
246        self.state.main = Some(state);
247        self
248    }
249}
250
251/// State of an entity.
252///
253/// Contains a `main` state, which will be managed by the owning service that
254/// manages the entity.
255///
256/// Additional services may inject their own state, which will be found in
257/// [`Self::components`].
258#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
259pub struct EntityState<S = ()> {
260    /// Version of the entity.
261    /// All modifications to metadata or spec will increment this version.
262    pub entity_version: u64,
263
264    /// UUID of the parent entity.
265    /// This is only set if the entity is a child of another entity.
266    pub parent_uid: Option<Uuid>,
267
268    /// Creation timestamp.
269    #[serde(with = "time::serde::timestamp")]
270    #[schemars(with = "u64")]
271    pub created_at: OffsetDateTime,
272    /// Last update timestamp.
273    /// Will be set on each metadata or spec change, but not on state changes.
274    #[serde(with = "time::serde::timestamp")]
275    #[schemars(with = "u64")]
276    pub updated_at: OffsetDateTime,
277
278    /// The primary state of the entity, managed by the owning service.
279    pub main: Option<EntityStateComponent<S>>,
280
281    /// Additional entity states, managed by services other than the entity owners.
282    pub components: HashMap<String, EntityStateComponent>,
283}
284
285/// Single component of an entities state.
286#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
287pub struct EntityStateComponent<T = serde_json::Value> {
288    /// Version of this state.
289    /// Will be incremented on each change.
290    pub state_version: u64,
291    /// Update timestamp.
292    #[serde(with = "time::serde::timestamp")]
293    #[schemars(with = "u64")]
294    pub updated_at: OffsetDateTime,
295    /// The actual state data.
296    pub data: T,
297}
298
299/// A marker trait for entity types.
300///
301/// Should be implementes on the struct representing the entities spec.
302pub trait EntityDescriptorConst {
303    const NAMESPACE: &'static str;
304    const NAME: &'static str;
305    const VERSION: &'static str;
306    const KIND: &'static str;
307
308    /// Entity specification.
309    type Spec: Serialize + DeserializeOwned + JsonSchema + Clone + PartialEq + Eq + std::fmt::Debug;
310    /// The main entity state.
311    type State: Serialize + DeserializeOwned + JsonSchema + Clone + PartialEq + Eq + std::fmt::Debug;
312
313    fn json_schema() -> schemars::schema::RootSchema {
314        schemars::schema_for!(Entity<Self::Spec>)
315    }
316
317    fn build_uri_str(name: &str) -> String {
318        format!("{}:{}", Self::KIND, name)
319    }
320
321    fn build_uri(name: &str) -> Result<EntityUri, EntityUriParseError> {
322        EntityUri::new_kind_name(Self::KIND, name)
323    }
324
325    /// Build the name that is used for the EntityTypeSpec representing this type.
326    fn type_name() -> String {
327        // TODO: this should be an additional const...
328        format!("{}-{}-v{}", Self::NAMESPACE, Self::NAME, Self::VERSION)
329    }
330
331    fn build_type_descriptor() -> Entity<EntityTypeSpec>
332    where
333        Self: JsonSchema + Sized,
334    {
335        EntityTypeSpec::build_for_type::<Self>()
336    }
337}
338
339/// Deserialize a typed entity from YAML.
340pub fn deserialize_entity_yaml_typed<T>(input: &str) -> Result<Entity<T>, anyhow::Error>
341where
342    T: EntityDescriptorConst + DeserializeOwned,
343{
344    let raw: serde_yaml::Value = serde_yaml::from_str(input).context("invalid YAML")?;
345    let kind = raw
346        .get("kind")
347        .context("missing 'kind' field in yaml")?
348        .as_str()
349        .context("'kind' field is not a string")?;
350
351    if kind != T::KIND {
352        anyhow::bail!("expected kind '{}' but got '{}'", T::KIND, kind);
353    }
354
355    let out = serde_yaml::from_value(raw).context("could not deserialize to entity data")?;
356    Ok(out)
357}
358
359/// Entity loaded from a file.
360#[derive(Debug)]
361pub struct FileEntity {
362    pub path: std::path::PathBuf,
363    pub document_index: usize,
364    pub entity: GenericEntity,
365}
366
367/// Load fixtures from YAML files in the specified directory.
368/// Descends into subdirectories.
369/// Each file can contain multiple entities as separate YAML documents.
370pub fn load_fixture_dir(dir: &Path) -> Result<Vec<FileEntity>, anyhow::Error> {
371    let mut fixtures = Vec::new();
372    load_fixture_dir_recursive(dir, &mut fixtures)?;
373    Ok(fixtures)
374}
375
376/// Recursive helper for [`load_fixture_dir`]
377fn load_fixture_dir_recursive(
378    dir: &Path,
379    fixtures: &mut Vec<FileEntity>,
380) -> Result<(), anyhow::Error> {
381    let iter = std::fs::read_dir(dir).with_context(|| {
382        format!(
383            "could not read confdb fixture directory '{}'",
384            dir.display()
385        )
386    })?;
387
388    for res in iter {
389        let entry = res.with_context(|| {
390            format!(
391                "could not read confdb fixture directory '{}'",
392                dir.display()
393            )
394        })?;
395        let path = entry.path();
396        let extension = path
397            .extension()
398            .and_then(|e| e.to_str())
399            .unwrap_or_default();
400
401        let ty = entry.file_type().with_context(|| {
402            format!(
403                "could not read confdb fixture directory '{}'",
404                dir.display()
405            )
406        })?;
407
408        if ty.is_dir() {
409            load_fixture_dir_recursive(&entry.path(), fixtures)?;
410        } else if ty.is_file() && (extension == "yaml" || extension == "yml") {
411            let contents = std::fs::read_to_string(&entry.path()).with_context(|| {
412                format!(
413                    "could not read confdb fixture file '{}'",
414                    entry.path().display()
415                )
416            })?;
417
418            for (index, deser) in serde_yaml::Deserializer::from_str(&contents).enumerate() {
419                let entity = GenericEntity::deserialize(deser).with_context(|| {
420                    format!(
421                        "could not parse document  {index} in confdb fixture file '{}'",
422                        path.display(),
423                    )
424                })?;
425
426                fixtures.push(FileEntity {
427                    path: path.clone(),
428                    document_index: index,
429                    entity,
430                });
431            }
432        }
433    }
434
435    Ok(())
436}