alembic_engine/
plan_view.rs1use crate::types::{Op, Plan};
10use alembic_core::key_string;
11use std::fmt::Write;
12
13const MAX_LISTED: usize = 50;
15
16pub fn render_plan(plan: &Plan) -> String {
21 let mut create = Vec::new();
22 let mut update = Vec::new();
23 let mut delete = Vec::new();
24 for op in &plan.ops {
25 match op {
26 Op::Create {
27 type_name, desired, ..
28 } => create.push(format!(" {} {}", type_name, key_string(&desired.key))),
29 Op::Update {
30 type_name,
31 desired,
32 changes,
33 ..
34 } => {
35 let mut line = format!(" {} {}", type_name, key_string(&desired.key));
36 for change in changes {
37 let _ = write!(
38 line,
39 "\n {}: {} -> {}",
40 change.field, change.from, change.to
41 );
42 }
43 update.push(line);
44 }
45 Op::Delete { type_name, key, .. } => {
46 delete.push(format!(" {} {}", type_name, key_string(key)))
47 }
48 }
49 }
50
51 let mut out = format!(
52 "plan: {} to create, {} to update, {} to delete",
53 create.len(),
54 update.len(),
55 delete.len()
56 );
57 for (label, lines) in [
58 ("create", &create),
59 ("update", &update),
60 ("delete", &delete),
61 ] {
62 if lines.is_empty() {
63 continue;
64 }
65 let _ = write!(out, "\n\n{label}:");
66 for line in lines.iter().take(MAX_LISTED) {
67 let _ = write!(out, "\n{line}");
68 }
69 if lines.len() > MAX_LISTED {
70 let _ = write!(out, "\n ... and {} more", lines.len() - MAX_LISTED);
71 }
72 }
73 out
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::types::{FieldChange, Op, Plan};
80 use alembic_core::{Key, Object, Schema, TypeName, Uid};
81 use std::collections::BTreeMap;
82
83 fn key(v: &str) -> Key {
84 Key::from(BTreeMap::from([(
85 "name".to_string(),
86 serde_json::Value::String(v.to_string()),
87 )]))
88 }
89
90 fn object(uid: u128, type_name: &str, k: &str) -> Object {
91 Object::new(
92 Uid::from_u128(uid),
93 TypeName::new(type_name),
94 key(k),
95 Default::default(),
96 )
97 .unwrap()
98 }
99
100 fn plan_of(ops: Vec<Op>) -> Plan {
101 Plan {
102 schema: Schema {
103 types: BTreeMap::new(),
104 },
105 ops,
106 summary: None,
107 schema_preview: None,
108 }
109 }
110
111 #[test]
112 fn renders_each_category_with_keys_and_field_changes() {
113 let plan = plan_of(vec![
114 Op::Create {
115 uid: Uid::from_u128(1),
116 type_name: TypeName::new("dcim.device"),
117 desired: object(1, "dcim.device", "leaf01"),
118 },
119 Op::Update {
120 uid: Uid::from_u128(2),
121 type_name: TypeName::new("dcim.interface"),
122 desired: object(2, "dcim.interface", "eth0"),
123 changes: vec![FieldChange {
124 field: "type".to_string(),
125 from: serde_json::json!("access"),
126 to: serde_json::json!("trunk"),
127 }],
128 backend_id: None,
129 },
130 ]);
131 let out = render_plan(&plan);
132 assert!(
133 out.starts_with("plan: 1 to create, 1 to update, 0 to delete"),
134 "{out}"
135 );
136 assert!(out.contains("create:"));
137 assert!(out.contains("dcim.device"));
138 assert!(out.contains("update:"));
139 assert!(out.contains(r#"type: "access" -> "trunk""#), "{out}");
140 assert!(!out.contains("delete:"));
141 }
142
143 #[test]
144 fn truncates_a_long_category() {
145 let ops = (0..MAX_LISTED as u128 + 5)
146 .map(|i| Op::Create {
147 uid: Uid::from_u128(i),
148 type_name: TypeName::new("dcim.device"),
149 desired: object(i, "dcim.device", &format!("d{i}")),
150 })
151 .collect();
152 let out = render_plan(&plan_of(ops));
153 assert!(out.contains("... and 5 more"), "{out}");
154 }
155}