alembic_engine/
extract.rs1use crate::state::StateStore;
4use crate::types::Adapter;
5use alembic_core::{key_string, uid_v5, Inventory, Object, Schema, TypeName};
6use anyhow::Result;
7
8#[derive(Debug)]
9pub struct ImportReport {
10 pub inventory: Inventory,
11}
12
13pub async fn import_inventory(
14 adapter: &(dyn Adapter + '_),
15 schema: &Schema,
16 types: &[TypeName],
17 state: &StateStore,
18) -> Result<ImportReport> {
19 let observed = adapter.read(schema, types, state).await?;
20
21 let mut objects: Vec<_> = observed.by_key.values().cloned().collect();
22 objects.sort_by(|a, b| {
23 (a.type_name.as_str().to_string(), key_string(&a.key))
24 .cmp(&(b.type_name.as_str().to_string(), key_string(&b.key)))
25 });
26
27 let mut inventory_objects = Vec::new();
28 for object in objects {
29 let uid = uid_v5(object.type_name.as_str(), &key_string(&object.key));
30 inventory_objects.push(Object {
31 uid,
32 type_name: object.type_name,
33 key: object.key,
34 attrs: object.attrs,
35 source: None,
36 });
37 }
38
39 Ok(ImportReport {
40 inventory: Inventory {
41 schema: schema.clone(),
42 objects: inventory_objects,
43 },
44 })
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::types::{BackendId, ObservedState};
51 use crate::Adapter;
52 use alembic_core::{
53 key_string, FieldSchema, FieldType, JsonMap, Key, Schema, TypeName, TypeSchema,
54 };
55 use async_trait::async_trait;
56 use futures::executor::block_on;
57 use serde_json::json;
58 use std::collections::BTreeMap;
59
60 struct MockAdapter {
61 observed: ObservedState,
62 }
63
64 #[async_trait]
65 impl Adapter for MockAdapter {
66 async fn read(
67 &self,
68 _schema: &Schema,
69 _types: &[TypeName],
70 _state: &crate::state::StateStore,
71 ) -> anyhow::Result<ObservedState> {
72 Ok(self.observed.clone())
73 }
74
75 async fn write(
76 &self,
77 _schema: &Schema,
78 _ops: &[crate::Op],
79 _state: &crate::state::StateStore,
80 ) -> anyhow::Result<crate::ApplyReport> {
81 unimplemented!("not used in import tests")
82 }
83 }
84
85 fn observed_state() -> ObservedState {
86 let mut state = ObservedState::default();
87 state.insert(crate::ObservedObject {
88 type_name: TypeName::new("dcim.site"),
89 key: key_str("site=fra1"),
90 attrs: attrs_map(json!({
91 "name": "FRA1",
92 "slug": "fra1",
93 "status": "active"
94 })),
95 backend_id: Some(BackendId::Int(1)),
96 });
97 state
98 }
99
100 fn key_str(raw: &str) -> Key {
101 let mut map = BTreeMap::new();
102 for segment in raw.split('/') {
103 let (field, value) = segment
104 .split_once('=')
105 .unwrap_or_else(|| panic!("invalid key segment: {segment}"));
106 map.insert(
107 field.to_string(),
108 serde_json::Value::String(value.to_string()),
109 );
110 }
111 Key::from(map)
112 }
113
114 fn attrs_map(value: serde_json::Value) -> JsonMap {
115 let serde_json::Value::Object(map) = value else {
116 panic!("attrs must be a json object");
117 };
118 map.into_iter().collect::<BTreeMap<_, _>>().into()
119 }
120
121 fn schema_for_observed(state: &ObservedState) -> Schema {
122 let mut types: BTreeMap<String, TypeSchema> = BTreeMap::new();
123 for object in state.by_key.values() {
124 let entry = types
125 .entry(object.type_name.as_str().to_string())
126 .or_insert_with(|| TypeSchema {
127 key: BTreeMap::new(),
128 fields: BTreeMap::new(),
129 });
130 for field in object.key.keys() {
131 entry.key.entry(field.clone()).or_insert(FieldSchema {
132 r#type: FieldType::Json,
133 required: true,
134 nullable: false,
135 description: None,
136 format: None,
137 pattern: None,
138 });
139 }
140 for field in object.attrs.keys() {
141 entry.fields.entry(field.clone()).or_insert(FieldSchema {
142 r#type: FieldType::Json,
143 required: false,
144 nullable: true,
145 description: None,
146 format: None,
147 pattern: None,
148 });
149 }
150 }
151 Schema { types }
152 }
153
154 #[test]
155 fn import_inventory_uses_stable_uid() {
156 let adapter = MockAdapter {
157 observed: observed_state(),
158 };
159 let schema = schema_for_observed(&adapter.observed);
160 let state = crate::state::StateStore::new(None, crate::state::StateData::default());
161 let report = block_on(import_inventory(&adapter, &schema, &[], &state)).unwrap();
162 assert_eq!(report.inventory.objects.len(), 1);
163 let object = &report.inventory.objects[0];
164 let key = key_str("site=fra1");
165 assert_eq!(object.key, key);
166 assert_eq!(object.uid, uid_v5("dcim.site", &key_string(&key)));
167 }
168}