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;
9use std::hash::{DefaultHasher, Hash, Hasher};
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum BackendId {
15 Int(u64),
16 String(String),
17}
18
19impl fmt::Display for BackendId {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 BackendId::Int(id) => write!(f, "{}", id),
23 BackendId::String(id) => write!(f, "{}", id),
24 }
25 }
26}
27
28impl From<u64> for BackendId {
29 fn from(id: u64) -> Self {
30 BackendId::Int(id)
31 }
32}
33
34impl From<String> for BackendId {
35 fn from(id: String) -> Self {
36 BackendId::String(id)
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)]
42pub struct FieldChange {
43 pub field: String,
45 pub from: serde_json::Value,
47 pub to: serde_json::Value,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)]
53#[serde(tag = "op", rename_all = "snake_case")]
54pub enum Op {
55 Create {
57 uid: Uid,
58 type_name: TypeName,
59 desired: Object,
60 },
61 Update {
63 uid: Uid,
64 type_name: TypeName,
65 desired: Object,
66 changes: Vec<FieldChange>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 backend_id: Option<BackendId>,
69 },
70 Delete {
72 uid: Uid,
73 type_name: TypeName,
74 key: Key,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 backend_id: Option<BackendId>,
77 },
78}
79
80impl Op {
81 pub fn uid(&self) -> Uid {
83 match self {
84 Op::Create { uid, .. } => *uid,
85 Op::Update { uid, .. } => *uid,
86 Op::Delete { uid, .. } => *uid,
87 }
88 }
89
90 pub fn type_name(&self) -> &TypeName {
92 match self {
93 Op::Create { type_name, .. } => type_name,
94 Op::Update { type_name, .. } => type_name,
95 Op::Delete { type_name, .. } => type_name,
96 }
97 }
98
99 pub fn hashed(&self) -> u64 {
100 let mut hasher = DefaultHasher::new();
101 self.hash(&mut hasher);
102 hasher.finish()
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Plan {
109 pub schema: Schema,
111 pub ops: Vec<Op>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub summary: Option<PlanSummary>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub schema_preview: Option<ProvisionReport>,
120}
121
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct PlanSummary {
125 pub create: usize,
127 pub update: usize,
129 pub delete: usize,
131}
132
133impl Plan {
134 pub fn summary(&self) -> PlanSummary {
136 let mut summary = PlanSummary::default();
137 for op in &self.ops {
138 match op {
139 Op::Create { .. } => summary.create += 1,
140 Op::Update { .. } => summary.update += 1,
141 Op::Delete { .. } => summary.delete += 1,
142 }
143 }
144 summary
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct ObservedObject {
151 pub type_name: TypeName,
153 pub key: Key,
155 pub attrs: JsonMap,
157 pub backend_id: Option<BackendId>,
159}
160
161#[derive(Debug, Default, Clone)]
163pub struct ObservedState {
164 pub by_backend_id: BTreeMap<(TypeName, BackendId), ObservedObject>,
166 pub by_key: BTreeMap<(TypeName, String), ObservedObject>,
168}
169
170impl ObservedState {
171 pub fn insert(&mut self, object: ObservedObject) -> Result<()> {
174 if let Some(id) = &object.backend_id {
175 let key = (object.type_name.clone(), id.clone());
176 if self.by_backend_id.contains_key(&key) {
177 return Err(anyhow!(
178 "ObservedState already contains an object with backend id {} for type {}",
179 id,
180 object.type_name
181 ));
182 }
183 self.by_backend_id.insert(key, object.clone());
184 }
185
186 let key = (object.type_name.clone(), key_string(&object.key));
187 if self.by_key.contains_key(&key) {
188 return Err(anyhow!(
189 "ObservedState already contains an object with natural key {:?}",
190 key
191 ));
192 }
193 self.by_key.insert(key, object);
194
195 Ok(())
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AppliedOp {
202 pub uid: Uid,
204 pub type_name: TypeName,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub backend_id: Option<BackendId>,
209}
210
211#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct ApplyReport {
214 pub applied: Vec<AppliedOp>,
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub previously_applied_count: Option<usize>,
219 #[serde(default)]
221 pub provision: ProvisionReport,
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct ProvisionReport {
227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
229 pub created_fields: Vec<String>,
230 #[serde(default, skip_serializing_if = "Vec::is_empty")]
232 pub created_tags: Vec<String>,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
235 pub created_object_types: Vec<String>,
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
238 pub created_object_fields: Vec<String>,
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
241 pub deprecated_object_types: Vec<String>,
242 #[serde(default, skip_serializing_if = "Vec::is_empty")]
244 pub deprecated_object_fields: Vec<String>,
245 #[serde(default, skip_serializing_if = "Vec::is_empty")]
247 pub deleted_object_types: Vec<String>,
248 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub deleted_object_fields: Vec<String>,
251}
252
253impl ProvisionReport {
254 pub fn is_empty(&self) -> bool {
255 self.created_fields.is_empty()
256 && self.created_tags.is_empty()
257 && self.created_object_types.is_empty()
258 && self.created_object_fields.is_empty()
259 && self.deprecated_object_types.is_empty()
260 && self.deprecated_object_fields.is_empty()
261 && self.deleted_object_types.is_empty()
262 && self.deleted_object_fields.is_empty()
263 }
264}
265
266impl fmt::Display for ProvisionReport {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 if self.is_empty() {
269 return write!(f, "no schema changes");
270 }
271
272 let mut first = true;
273 let sections: &[(&str, &[String])] = &[
274 ("fields created", &self.created_fields),
275 ("tags created", &self.created_tags),
276 ("object types created", &self.created_object_types),
277 ("object fields created", &self.created_object_fields),
278 ("object types deprecated", &self.deprecated_object_types),
279 ("object fields deprecated", &self.deprecated_object_fields),
280 ("object types deleted", &self.deleted_object_types),
281 ("object fields deleted", &self.deleted_object_fields),
282 ];
283
284 for (label, items) in sections {
285 if items.is_empty() {
286 continue;
287 }
288 if !first {
289 write!(f, ", ")?;
290 }
291 write!(f, "{} {label}", items.len())?;
292 first = false;
293 }
294
295 Ok(())
296 }
297}
298
299#[async_trait]
301pub trait Observer: Send + Sync {
302 async fn read(
303 &self,
304 schema: &Schema,
305 types: &[TypeName],
306 state: &crate::state::StateStore,
307 ) -> anyhow::Result<ObservedState>;
308}
309
310#[async_trait]
312pub trait Emitter: Send + Sync {
313 async fn write(
314 &self,
315 schema: &Schema,
316 ops: &[Op],
317 state: &crate::state::StateStore,
318 ) -> anyhow::Result<ApplyReport>;
319}
320
321#[async_trait]
323pub trait Adapter: Observer + Emitter {
324 async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
325 Ok(ProvisionReport::default())
326 }
327
328 async fn preview_schema(&self, _schema: &Schema) -> anyhow::Result<Option<ProvisionReport>> {
333 Ok(None)
334 }
335}
336
337pub enum Backend {
339 Observer(Box<dyn Observer>),
341 Emitter(Box<dyn Emitter>),
343 Adapter(Box<dyn Adapter>),
345}
346
347impl Backend {
348 pub fn observer(&self) -> anyhow::Result<&dyn Observer> {
349 match self {
350 Backend::Observer(observer) => Ok(observer.as_ref()),
351 Backend::Adapter(adapter) => Ok(adapter.as_ref()),
352 Backend::Emitter(_) => Err(anyhow::anyhow!(
353 "backend is write-only; it cannot observe state"
354 )),
355 }
356 }
357
358 pub fn emitter(&self) -> anyhow::Result<&dyn Emitter> {
359 match self {
360 Backend::Emitter(emitter) => Ok(emitter.as_ref()),
361 Backend::Adapter(adapter) => Ok(adapter.as_ref()),
362 Backend::Observer(_) => Err(anyhow::anyhow!(
363 "backend is read-only; it cannot apply changes"
364 )),
365 }
366 }
367
368 pub fn adapter(&self) -> anyhow::Result<&dyn Adapter> {
369 match self {
370 Backend::Adapter(adapter) => Ok(adapter.as_ref()),
371 Backend::Observer(_) => Err(anyhow::anyhow!(
372 "backend is read-only; it cannot provision schema"
373 )),
374 Backend::Emitter(_) => Err(anyhow::anyhow!(
375 "backend is write-only; it cannot provision schema"
376 )),
377 }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use alembic_core::{Key, TypeName, Uid};
385
386 #[test]
387 fn backend_id_serialization() {
388 let int_id = BackendId::Int(123);
389 let json = serde_json::to_string(&int_id).unwrap();
390 assert_eq!(json, "123");
391 let back: BackendId = serde_json::from_str(&json).unwrap();
392 assert_eq!(back, int_id);
393
394 let str_id = BackendId::String("uuid".to_string());
395 let json = serde_json::to_string(&str_id).unwrap();
396 assert_eq!(json, "\"uuid\"");
397 let back: BackendId = serde_json::from_str(&json).unwrap();
398 assert_eq!(back, str_id);
399 }
400
401 #[test]
402 fn provision_report_defaults_omitted_lists() {
403 let report: ProvisionReport =
406 serde_json::from_value(serde_json::json!({"created_object_types": ["dcim.site"]}))
407 .unwrap();
408 assert!(report.created_fields.is_empty());
409 assert!(report.created_tags.is_empty());
410 assert_eq!(report.created_object_types, ["dcim.site"]);
411
412 assert!(serde_json::from_str::<ProvisionReport>("{}")
414 .unwrap()
415 .is_empty());
416 }
417
418 #[test]
419 fn op_helpers() {
420 let uid = Uid::from_u128(1);
421 let type_name = TypeName::new("test.type");
422 let op = Op::Delete {
423 uid,
424 type_name: type_name.clone(),
425 key: Key::default(),
426 backend_id: None,
427 };
428 assert_eq!(op.uid(), uid);
429 assert_eq!(op.type_name(), &type_name);
430 }
431}