1use alembic_core::{key_string, JsonMap, Key, Object, Schema, TypeName, Uid};
4use anyhow::{anyhow, Result};
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12#[serde(untagged)]
13pub enum BackendId {
14 Int(u64),
15 String(String),
16}
17
18impl fmt::Display for BackendId {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 BackendId::Int(id) => write!(f, "{}", id),
22 BackendId::String(id) => write!(f, "{}", id),
23 }
24 }
25}
26
27impl From<u64> for BackendId {
28 fn from(id: u64) -> Self {
29 BackendId::Int(id)
30 }
31}
32
33impl From<String> for BackendId {
34 fn from(id: String) -> Self {
35 BackendId::String(id)
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)]
41pub struct FieldChange {
42 pub field: String,
44 pub from: serde_json::Value,
46 pub to: serde_json::Value,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)]
52#[serde(tag = "op", rename_all = "snake_case")]
53pub enum Op {
54 Create {
56 uid: Uid,
57 type_name: TypeName,
58 desired: Object,
59 },
60 Update {
62 uid: Uid,
63 type_name: TypeName,
64 desired: Object,
65 changes: Vec<FieldChange>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 backend_id: Option<BackendId>,
68 },
69 Delete {
71 uid: Uid,
72 type_name: TypeName,
73 key: Key,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 backend_id: Option<BackendId>,
76 },
77}
78
79impl Op {
80 pub fn uid(&self) -> Uid {
82 match self {
83 Op::Create { uid, .. } => *uid,
84 Op::Update { uid, .. } => *uid,
85 Op::Delete { uid, .. } => *uid,
86 }
87 }
88
89 pub fn type_name(&self) -> &TypeName {
91 match self {
92 Op::Create { type_name, .. } => type_name,
93 Op::Update { type_name, .. } => type_name,
94 Op::Delete { type_name, .. } => type_name,
95 }
96 }
97
98 pub fn hashed(&self) -> u64 {
99 stable_json_hash(self)
100 }
101}
102
103pub(crate) fn stable_json_hash<T: Serialize>(value: &T) -> u64 {
108 let bytes = serde_json::to_vec(value).expect("engine value serializes to json");
111 uuid::Uuid::new_v5(&alembic_core::ALEMBIC_UID_NAMESPACE, &bytes)
112 .as_u64_pair()
113 .0
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Plan {
119 pub schema: Schema,
121 pub ops: Vec<Op>,
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub summary: Option<PlanSummary>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub schema_preview: Option<ProvisionReport>,
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct PlanSummary {
135 pub create: usize,
137 pub update: usize,
139 pub delete: usize,
141}
142
143impl Plan {
144 pub fn summary(&self) -> PlanSummary {
146 let mut summary = PlanSummary::default();
147 for op in &self.ops {
148 match op {
149 Op::Create { .. } => summary.create += 1,
150 Op::Update { .. } => summary.update += 1,
151 Op::Delete { .. } => summary.delete += 1,
152 }
153 }
154 summary
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct ObservedObject {
161 pub type_name: TypeName,
163 pub key: Key,
165 pub attrs: JsonMap,
167 pub backend_id: Option<BackendId>,
169}
170
171#[derive(Debug, Default, Clone)]
173pub struct ObservedState {
174 pub by_backend_id: BTreeMap<(TypeName, BackendId), ObservedObject>,
176 pub by_key: BTreeMap<(TypeName, String), ObservedObject>,
178}
179
180impl ObservedState {
181 pub fn insert(&mut self, object: ObservedObject) -> Result<()> {
184 if let Some(id) = &object.backend_id {
185 let key = (object.type_name.clone(), id.clone());
186 if self.by_backend_id.contains_key(&key) {
187 return Err(anyhow!(
188 "ObservedState already contains an object with backend id {} for type {}",
189 id,
190 object.type_name
191 ));
192 }
193 self.by_backend_id.insert(key, object.clone());
194 }
195
196 let key = (object.type_name.clone(), key_string(&object.key));
197 if self.by_key.contains_key(&key) {
198 return Err(anyhow!(
199 "ObservedState already contains an object with natural key {:?}",
200 key
201 ));
202 }
203 self.by_key.insert(key, object);
204
205 Ok(())
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct AppliedOp {
212 pub uid: Uid,
214 pub type_name: TypeName,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub backend_id: Option<BackendId>,
219}
220
221#[derive(Debug, Clone, Default, Serialize, Deserialize)]
223pub struct ApplyReport {
224 pub applied: Vec<AppliedOp>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub previously_applied_count: Option<usize>,
229 #[serde(default)]
231 pub provision: ProvisionReport,
232}
233
234#[derive(Debug, Clone, Default, Serialize, Deserialize)]
236pub struct ProvisionReport {
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub created_fields: Vec<String>,
240 #[serde(default, skip_serializing_if = "Vec::is_empty")]
242 pub created_tags: Vec<String>,
243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
245 pub created_object_types: Vec<String>,
246 #[serde(default, skip_serializing_if = "Vec::is_empty")]
248 pub created_object_fields: Vec<String>,
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
251 pub deprecated_object_types: Vec<String>,
252 #[serde(default, skip_serializing_if = "Vec::is_empty")]
254 pub deprecated_object_fields: Vec<String>,
255 #[serde(default, skip_serializing_if = "Vec::is_empty")]
257 pub deleted_object_types: Vec<String>,
258 #[serde(default, skip_serializing_if = "Vec::is_empty")]
260 pub deleted_object_fields: Vec<String>,
261}
262
263impl ProvisionReport {
264 pub fn is_empty(&self) -> bool {
265 self.created_fields.is_empty()
266 && self.created_tags.is_empty()
267 && self.created_object_types.is_empty()
268 && self.created_object_fields.is_empty()
269 && self.deprecated_object_types.is_empty()
270 && self.deprecated_object_fields.is_empty()
271 && self.deleted_object_types.is_empty()
272 && self.deleted_object_fields.is_empty()
273 }
274}
275
276impl fmt::Display for ProvisionReport {
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 if self.is_empty() {
279 return write!(f, "no schema changes");
280 }
281
282 let mut first = true;
283 let sections: &[(&str, &[String])] = &[
284 ("fields created", &self.created_fields),
285 ("tags created", &self.created_tags),
286 ("object types created", &self.created_object_types),
287 ("object fields created", &self.created_object_fields),
288 ("object types deprecated", &self.deprecated_object_types),
289 ("object fields deprecated", &self.deprecated_object_fields),
290 ("object types deleted", &self.deleted_object_types),
291 ("object fields deleted", &self.deleted_object_fields),
292 ];
293
294 for (label, items) in sections {
295 if items.is_empty() {
296 continue;
297 }
298 if !first {
299 write!(f, ", ")?;
300 }
301 write!(f, "{} {label}", items.len())?;
302 first = false;
303 }
304
305 Ok(())
306 }
307}
308
309#[async_trait]
311pub trait Observer: Send + Sync {
312 async fn read(
313 &self,
314 schema: &Schema,
315 types: &[TypeName],
316 state: &crate::state::StateStore,
317 ) -> anyhow::Result<ObservedState>;
318}
319
320#[async_trait]
322pub trait Emitter: Send + Sync {
323 async fn write(
324 &self,
325 schema: &Schema,
326 ops: &[Op],
327 state: &crate::state::StateStore,
328 ) -> anyhow::Result<ApplyReport>;
329}
330
331#[async_trait]
333pub trait Adapter: Observer + Emitter {
334 async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
335 Ok(ProvisionReport::default())
336 }
337
338 async fn preview_schema(&self, _schema: &Schema) -> anyhow::Result<Option<ProvisionReport>> {
343 Ok(None)
344 }
345}
346
347pub enum Backend {
349 Observer(Box<dyn Observer>),
351 Emitter(Box<dyn Emitter>),
353 Adapter(Box<dyn Adapter>),
355}
356
357impl Backend {
358 pub fn observer(&self) -> anyhow::Result<&dyn Observer> {
359 match self {
360 Backend::Observer(observer) => Ok(observer.as_ref()),
361 Backend::Adapter(adapter) => Ok(adapter.as_ref()),
362 Backend::Emitter(_) => Err(anyhow::anyhow!(
363 "backend is write-only; it cannot observe state"
364 )),
365 }
366 }
367
368 pub fn emitter(&self) -> anyhow::Result<&dyn Emitter> {
369 match self {
370 Backend::Emitter(emitter) => Ok(emitter.as_ref()),
371 Backend::Adapter(adapter) => Ok(adapter.as_ref()),
372 Backend::Observer(_) => Err(anyhow::anyhow!(
373 "backend is read-only; it cannot apply changes"
374 )),
375 }
376 }
377
378 pub fn adapter(&self) -> anyhow::Result<&dyn Adapter> {
379 match self {
380 Backend::Adapter(adapter) => Ok(adapter.as_ref()),
381 Backend::Observer(_) => Err(anyhow::anyhow!(
382 "backend is read-only; it cannot provision schema"
383 )),
384 Backend::Emitter(_) => Err(anyhow::anyhow!(
385 "backend is write-only; it cannot provision schema"
386 )),
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use alembic_core::{Key, TypeName, Uid};
395
396 #[test]
397 fn backend_id_serialization() {
398 let int_id = BackendId::Int(123);
399 let json = serde_json::to_string(&int_id).unwrap();
400 assert_eq!(json, "123");
401 let back: BackendId = serde_json::from_str(&json).unwrap();
402 assert_eq!(back, int_id);
403
404 let str_id = BackendId::String("uuid".to_string());
405 let json = serde_json::to_string(&str_id).unwrap();
406 assert_eq!(json, "\"uuid\"");
407 let back: BackendId = serde_json::from_str(&json).unwrap();
408 assert_eq!(back, str_id);
409 }
410
411 #[test]
412 fn provision_report_defaults_omitted_lists() {
413 let report: ProvisionReport =
416 serde_json::from_value(serde_json::json!({"created_object_types": ["dcim.site"]}))
417 .unwrap();
418 assert!(report.created_fields.is_empty());
419 assert!(report.created_tags.is_empty());
420 assert_eq!(report.created_object_types, ["dcim.site"]);
421
422 assert!(serde_json::from_str::<ProvisionReport>("{}")
424 .unwrap()
425 .is_empty());
426 }
427
428 #[test]
429 fn op_helpers() {
430 let uid = Uid::from_u128(1);
431 let type_name = TypeName::new("test.type");
432 let op = Op::Delete {
433 uid,
434 type_name: type_name.clone(),
435 key: Key::default(),
436 backend_id: None,
437 };
438 assert_eq!(op.uid(), uid);
439 assert_eq!(op.type_name(), &type_name);
440 }
441}