1use alembic_core::{key_string, JsonMap, Key, Object, Schema, TypeName, Uid};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum BackendId {
13 Int(u64),
14 String(String),
15}
16
17impl fmt::Display for BackendId {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 BackendId::Int(id) => write!(f, "{}", id),
21 BackendId::String(id) => write!(f, "{}", id),
22 }
23 }
24}
25
26impl From<u64> for BackendId {
27 fn from(id: u64) -> Self {
28 BackendId::Int(id)
29 }
30}
31
32impl From<String> for BackendId {
33 fn from(id: String) -> Self {
34 BackendId::String(id)
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct FieldChange {
41 pub field: String,
43 pub from: serde_json::Value,
45 pub to: serde_json::Value,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(tag = "op", rename_all = "snake_case")]
52pub enum Op {
53 Create {
55 uid: Uid,
56 type_name: TypeName,
57 desired: Object,
58 },
59 Update {
61 uid: Uid,
62 type_name: TypeName,
63 desired: Object,
64 changes: Vec<FieldChange>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 backend_id: Option<BackendId>,
67 },
68 Delete {
70 uid: Uid,
71 type_name: TypeName,
72 key: Key,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 backend_id: Option<BackendId>,
75 },
76}
77
78impl Op {
79 pub fn uid(&self) -> Uid {
81 match self {
82 Op::Create { uid, .. } => *uid,
83 Op::Update { uid, .. } => *uid,
84 Op::Delete { uid, .. } => *uid,
85 }
86 }
87
88 pub fn type_name(&self) -> &TypeName {
90 match self {
91 Op::Create { type_name, .. } => type_name,
92 Op::Update { type_name, .. } => type_name,
93 Op::Delete { type_name, .. } => type_name,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Plan {
101 pub schema: Schema,
103 pub ops: Vec<Op>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub summary: Option<PlanSummary>,
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct PlanSummary {
113 pub create: usize,
115 pub update: usize,
117 pub delete: usize,
119}
120
121impl Plan {
122 pub fn summary(&self) -> PlanSummary {
124 let mut summary = PlanSummary::default();
125 for op in &self.ops {
126 match op {
127 Op::Create { .. } => summary.create += 1,
128 Op::Update { .. } => summary.update += 1,
129 Op::Delete { .. } => summary.delete += 1,
130 }
131 }
132 summary
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct ObservedObject {
139 pub type_name: TypeName,
141 pub key: Key,
143 pub attrs: JsonMap,
145 pub backend_id: Option<BackendId>,
147}
148
149#[derive(Debug, Default, Clone)]
151pub struct ObservedState {
152 pub by_backend_id: BTreeMap<(TypeName, BackendId), ObservedObject>,
154 pub by_key: BTreeMap<(TypeName, String), ObservedObject>,
156}
157
158impl ObservedState {
159 pub fn insert(&mut self, object: ObservedObject) {
161 if let Some(id) = &object.backend_id {
162 self.by_backend_id
163 .insert((object.type_name.clone(), id.clone()), object.clone());
164 }
165 self.by_key
166 .insert((object.type_name.clone(), key_string(&object.key)), object);
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AppliedOp {
173 pub uid: Uid,
175 pub type_name: TypeName,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub backend_id: Option<BackendId>,
180}
181
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184pub struct ApplyReport {
185 pub applied: Vec<AppliedOp>,
187 #[serde(default)]
189 pub provision: ProvisionReport,
190}
191
192#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct ProvisionReport {
195 pub created_fields: Vec<String>,
197 pub created_tags: Vec<String>,
199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 pub created_object_types: Vec<String>,
202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
204 pub created_object_fields: Vec<String>,
205 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub deprecated_object_types: Vec<String>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
210 pub deprecated_object_fields: Vec<String>,
211 #[serde(default, skip_serializing_if = "Vec::is_empty")]
213 pub deleted_object_types: Vec<String>,
214 #[serde(default, skip_serializing_if = "Vec::is_empty")]
216 pub deleted_object_fields: Vec<String>,
217}
218
219impl ProvisionReport {
220 pub fn is_empty(&self) -> bool {
221 self.created_fields.is_empty()
222 && self.created_tags.is_empty()
223 && self.created_object_types.is_empty()
224 && self.created_object_fields.is_empty()
225 && self.deprecated_object_types.is_empty()
226 && self.deprecated_object_fields.is_empty()
227 && self.deleted_object_types.is_empty()
228 && self.deleted_object_fields.is_empty()
229 }
230}
231
232impl fmt::Display for ProvisionReport {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 if self.is_empty() {
235 return write!(f, "no schema changes");
236 }
237
238 let mut first = true;
239 let sections: &[(&str, &[String])] = &[
240 ("fields created", &self.created_fields),
241 ("tags created", &self.created_tags),
242 ("object types created", &self.created_object_types),
243 ("object fields created", &self.created_object_fields),
244 ("object types deprecated", &self.deprecated_object_types),
245 ("object fields deprecated", &self.deprecated_object_fields),
246 ("object types deleted", &self.deleted_object_types),
247 ("object fields deleted", &self.deleted_object_fields),
248 ];
249
250 for (label, items) in sections {
251 if items.is_empty() {
252 continue;
253 }
254 if !first {
255 write!(f, ", ")?;
256 }
257 write!(f, "{} {label}", items.len())?;
258 first = false;
259 }
260
261 Ok(())
262 }
263}
264
265#[async_trait]
267pub trait Adapter: Send + Sync {
268 async fn read(
269 &self,
270 schema: &Schema,
271 types: &[TypeName],
272 state: &crate::state::StateStore,
273 ) -> anyhow::Result<ObservedState>;
274 async fn write(
275 &self,
276 schema: &Schema,
277 ops: &[Op],
278 state: &crate::state::StateStore,
279 ) -> anyhow::Result<ApplyReport>;
280 async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
281 Ok(ProvisionReport::default())
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use alembic_core::{Key, TypeName, Uid};
289
290 #[test]
291 fn backend_id_serialization() {
292 let int_id = BackendId::Int(123);
293 let json = serde_json::to_string(&int_id).unwrap();
294 assert_eq!(json, "123");
295 let back: BackendId = serde_json::from_str(&json).unwrap();
296 assert_eq!(back, int_id);
297
298 let str_id = BackendId::String("uuid".to_string());
299 let json = serde_json::to_string(&str_id).unwrap();
300 assert_eq!(json, "\"uuid\"");
301 let back: BackendId = serde_json::from_str(&json).unwrap();
302 assert_eq!(back, str_id);
303 }
304
305 #[test]
306 fn op_helpers() {
307 let uid = Uid::from_u128(1);
308 let type_name = TypeName::new("test.type");
309 let op = Op::Delete {
310 uid,
311 type_name: type_name.clone(),
312 key: Key::default(),
313 backend_id: None,
314 };
315 assert_eq!(op.uid(), uid);
316 assert_eq!(op.type_name(), &type_name);
317 }
318}