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#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
19pub struct EntityMeta {
20 pub uid: Option<Uuid>,
22
23 pub name: String,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
32 pub description: Option<String>,
33
34 #[serde(default)]
37 #[serde(skip_serializing_if = "HashMap::is_empty")]
38 pub labels: HashMap<String, String>,
39
40 #[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#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
83pub struct Entity<D, C = serde_json::Value> {
84 pub meta: EntityMeta,
86 pub spec: D,
88 #[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 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 pub fn to_json_map(&self) -> Result<serde_json::Value, serde_json::Error> {
143 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 pub fn to_yaml_map(&self) -> Result<serde_yaml::Mapping, serde_yaml::Error> {
161 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 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 pub fn to_generic(&self) -> Result<GenericEntity, serde_json::Error> {
180 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#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
194pub struct GenericEntity {
195 pub kind: String,
196
197 pub meta: EntityMeta,
199 pub spec: serde_json::Value,
201 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#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
217pub struct FullEntity<D, S = (), C = serde_json::Value> {
218 pub meta: EntityMeta,
220 pub spec: D,
222 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#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
259pub struct EntityState<S = ()> {
260 pub entity_version: u64,
263
264 pub parent_uid: Option<Uuid>,
267
268 #[serde(with = "time::serde::timestamp")]
270 #[schemars(with = "u64")]
271 pub created_at: OffsetDateTime,
272 #[serde(with = "time::serde::timestamp")]
275 #[schemars(with = "u64")]
276 pub updated_at: OffsetDateTime,
277
278 pub main: Option<EntityStateComponent<S>>,
280
281 pub components: HashMap<String, EntityStateComponent>,
283}
284
285#[derive(Serialize, Deserialize, JsonSchema, PartialEq, Eq, Clone, Debug)]
287pub struct EntityStateComponent<T = serde_json::Value> {
288 pub state_version: u64,
291 #[serde(with = "time::serde::timestamp")]
293 #[schemars(with = "u64")]
294 pub updated_at: OffsetDateTime,
295 pub data: T,
297}
298
299pub trait EntityDescriptorConst {
303 const NAMESPACE: &'static str;
304 const NAME: &'static str;
305 const VERSION: &'static str;
306 const KIND: &'static str;
307
308 type Spec: Serialize + DeserializeOwned + JsonSchema + Clone + PartialEq + Eq + std::fmt::Debug;
310 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 fn type_name() -> String {
327 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
339pub 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#[derive(Debug)]
361pub struct FileEntity {
362 pub path: std::path::PathBuf,
363 pub document_index: usize,
364 pub entity: GenericEntity,
365}
366
367pub 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
376fn 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}