Skip to main content

alembic_engine/
types.rs

1//! core engine types and adapter contract.
2
3use 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/// generic backend identifier (integer or string/uuid).
10#[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/// field-level change for an update op.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct FieldChange {
41    /// field name within attrs.
42    pub field: String,
43    /// previous value from observed state.
44    pub from: serde_json::Value,
45    /// desired value from the ir.
46    pub to: serde_json::Value,
47}
48
49/// plan operation.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(tag = "op", rename_all = "snake_case")]
52pub enum Op {
53    /// create a new backend object.
54    Create {
55        uid: Uid,
56        type_name: TypeName,
57        desired: Object,
58    },
59    /// update an existing backend object.
60    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 a backend object.
69    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    /// returns the ir uid for this operation.
80    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    /// returns the type name for this operation.
89    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/// full plan document.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Plan {
101    /// schema definitions required for apply.
102    pub schema: Schema,
103    /// ordered list of operations.
104    pub ops: Vec<Op>,
105    /// high-level summary of the plan.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub summary: Option<PlanSummary>,
108}
109
110/// high-level summary of plan operations.
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct PlanSummary {
113    /// number of objects to create.
114    pub create: usize,
115    /// number of objects to update.
116    pub update: usize,
117    /// number of objects to delete.
118    pub delete: usize,
119}
120
121impl Plan {
122    /// build a summary for the current plan.
123    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/// observed backend object representation.
137#[derive(Debug, Clone)]
138pub struct ObservedObject {
139    /// object type.
140    pub type_name: TypeName,
141    /// human key for matching.
142    pub key: Key,
143    /// observed attrs mapped to ir types.
144    pub attrs: JsonMap,
145    /// backend id when known.
146    pub backend_id: Option<BackendId>,
147}
148
149/// observed backend state indexed by id and key.
150#[derive(Debug, Default, Clone)]
151pub struct ObservedState {
152    /// observed objects keyed by backend id.
153    pub by_backend_id: BTreeMap<(TypeName, BackendId), ObservedObject>,
154    /// observed objects keyed by natural key.
155    pub by_key: BTreeMap<(TypeName, String), ObservedObject>,
156}
157
158impl ObservedState {
159    /// insert an observed object into both indexes.
160    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/// result for a single applied operation.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AppliedOp {
173    /// ir uid for the operation.
174    pub uid: Uid,
175    /// type for the operation.
176    pub type_name: TypeName,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    /// backend id returned by the adapter, if any.
179    pub backend_id: Option<BackendId>,
180}
181
182/// aggregated apply report.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ApplyReport {
185    /// list of operations applied by the adapter.
186    pub applied: Vec<AppliedOp>,
187}
188
189/// report from ensure_schema provisioning.
190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct ProvisionReport {
192    /// custom fields created on the backend.
193    pub created_fields: Vec<String>,
194    /// tags created on the backend.
195    pub created_tags: Vec<String>,
196    /// custom object types created on the backend.
197    #[serde(default, skip_serializing_if = "Vec::is_empty")]
198    pub created_object_types: Vec<String>,
199    /// custom object fields created on the backend.
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub created_object_fields: Vec<String>,
202    /// object types deprecated on the backend.
203    #[serde(default, skip_serializing_if = "Vec::is_empty")]
204    pub deprecated_object_types: Vec<String>,
205    /// object fields deprecated on the backend.
206    #[serde(default, skip_serializing_if = "Vec::is_empty")]
207    pub deprecated_object_fields: Vec<String>,
208    /// object types deleted on the backend.
209    #[serde(default, skip_serializing_if = "Vec::is_empty")]
210    pub deleted_object_types: Vec<String>,
211    /// object fields deleted on the backend.
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    pub deleted_object_fields: Vec<String>,
214}
215
216/// adapter contract for backend-specific io.
217#[async_trait]
218pub trait Adapter: Send + Sync {
219    async fn read(
220        &self,
221        schema: &Schema,
222        types: &[TypeName],
223        state: &crate::state::StateStore,
224    ) -> anyhow::Result<ObservedState>;
225    async fn write(
226        &self,
227        schema: &Schema,
228        ops: &[Op],
229        state: &crate::state::StateStore,
230    ) -> anyhow::Result<ApplyReport>;
231    async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
232        Ok(ProvisionReport::default())
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use alembic_core::{Key, TypeName, Uid};
240
241    #[test]
242    fn backend_id_serialization() {
243        let int_id = BackendId::Int(123);
244        let json = serde_json::to_string(&int_id).unwrap();
245        assert_eq!(json, "123");
246        let back: BackendId = serde_json::from_str(&json).unwrap();
247        assert_eq!(back, int_id);
248
249        let str_id = BackendId::String("uuid".to_string());
250        let json = serde_json::to_string(&str_id).unwrap();
251        assert_eq!(json, "\"uuid\"");
252        let back: BackendId = serde_json::from_str(&json).unwrap();
253        assert_eq!(back, str_id);
254    }
255
256    #[test]
257    fn op_helpers() {
258        let uid = Uid::from_u128(1);
259        let type_name = TypeName::new("test.type");
260        let op = Op::Delete {
261            uid,
262            type_name: type_name.clone(),
263            key: Key::default(),
264            backend_id: None,
265        };
266        assert_eq!(op.uid(), uid);
267        assert_eq!(op.type_name(), &type_name);
268    }
269}