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}
203
204/// adapter contract for backend-specific io.
205#[async_trait]
206pub trait Adapter: Send + Sync {
207    async fn read(
208        &self,
209        schema: &Schema,
210        types: &[TypeName],
211        state: &crate::state::StateStore,
212    ) -> anyhow::Result<ObservedState>;
213    async fn write(
214        &self,
215        schema: &Schema,
216        ops: &[Op],
217        state: &crate::state::StateStore,
218    ) -> anyhow::Result<ApplyReport>;
219    async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
220        Ok(ProvisionReport::default())
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use alembic_core::{Key, TypeName, Uid};
228
229    #[test]
230    fn backend_id_serialization() {
231        let int_id = BackendId::Int(123);
232        let json = serde_json::to_string(&int_id).unwrap();
233        assert_eq!(json, "123");
234        let back: BackendId = serde_json::from_str(&json).unwrap();
235        assert_eq!(back, int_id);
236
237        let str_id = BackendId::String("uuid".to_string());
238        let json = serde_json::to_string(&str_id).unwrap();
239        assert_eq!(json, "\"uuid\"");
240        let back: BackendId = serde_json::from_str(&json).unwrap();
241        assert_eq!(back, str_id);
242    }
243
244    #[test]
245    fn op_helpers() {
246        let uid = Uid::from_u128(1);
247        let type_name = TypeName::new("test.type");
248        let op = Op::Delete {
249            uid,
250            type_name: type_name.clone(),
251            key: Key::default(),
252            backend_id: None,
253        };
254        assert_eq!(op.uid(), uid);
255        assert_eq!(op.type_name(), &type_name);
256    }
257}