greentic_flow/
flow_meta.rs1use anyhow::{Result, anyhow};
2use serde_json::Value;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5pub const META_NAMESPACE: &str = "greentic";
6
7pub fn now_epoch_seconds() -> u64 {
8 SystemTime::now()
9 .duration_since(UNIX_EPOCH)
10 .map(|d| d.as_secs())
11 .unwrap_or(0)
12}
13
14fn ensure_object(value: &mut Option<Value>) -> &mut serde_json::Map<String, Value> {
15 if !matches!(value, Some(Value::Object(_))) {
16 *value = Some(Value::Object(serde_json::Map::new()));
17 }
18 match value.as_mut().unwrap() {
19 Value::Object(map) => map,
20 _ => unreachable!(),
21 }
22}
23
24fn ensure_child_map<'a>(
25 parent: &'a mut serde_json::Map<String, Value>,
26 key: &str,
27) -> &'a mut serde_json::Map<String, Value> {
28 let entry = parent
29 .entry(key.to_string())
30 .or_insert_with(|| Value::Object(serde_json::Map::new()));
31 match entry {
32 Value::Object(map) => map,
33 _ => {
34 *entry = Value::Object(serde_json::Map::new());
35 match entry {
36 Value::Object(map) => map,
37 _ => unreachable!(),
38 }
39 }
40 }
41}
42
43pub fn ensure_greentic_meta(meta: &mut Option<Value>) -> &mut serde_json::Map<String, Value> {
44 let root = ensure_object(meta);
45 ensure_child_map(root, META_NAMESPACE)
46}
47
48pub fn set_component_entry(
49 meta: &mut Option<Value>,
50 node_id: &str,
51 component_id: &str,
52 abi_version: &str,
53 digest: Option<&str>,
54 exported_ops: &[String],
55 contract: Option<&ComponentContractMeta>,
56) {
57 let greentic = ensure_greentic_meta(meta);
58 let components = ensure_child_map(greentic, "components");
59 let mut added_at = now_epoch_seconds();
60 if let Some(Value::Object(existing)) = components.get(node_id)
61 && let Some(Value::Number(number)) = existing.get("added_at")
62 && let Some(value) = number.as_u64()
63 {
64 added_at = value;
65 }
66 let mut entry = serde_json::Map::new();
67 entry.insert(
68 "component_id".to_string(),
69 Value::String(component_id.to_string()),
70 );
71 entry.insert(
72 "abi_version".to_string(),
73 Value::String(abi_version.to_string()),
74 );
75 if let Some(contract) = contract {
76 entry.insert(
77 "describe_hash".to_string(),
78 Value::String(contract.describe_hash.clone()),
79 );
80 entry.insert(
81 "operation_id".to_string(),
82 Value::String(contract.operation_id.clone()),
83 );
84 entry.insert(
85 "schema_hash".to_string(),
86 Value::String(contract.schema_hash.clone()),
87 );
88 if let Some(version) = &contract.component_version {
89 entry.insert(
90 "component_version".to_string(),
91 Value::String(version.clone()),
92 );
93 }
94 if let Some(world) = &contract.world {
95 entry.insert("world".to_string(), Value::String(world.clone()));
96 }
97 if let Some(config_schema_cbor) = &contract.config_schema_cbor {
98 entry.insert(
99 "config_schema_cbor".to_string(),
100 Value::String(config_schema_cbor.clone()),
101 );
102 }
103 }
104 if let Some(d) = digest {
105 entry.insert("resolved_digest".to_string(), Value::String(d.to_string()));
106 }
107 entry.insert(
108 "exported_ops_seen".to_string(),
109 Value::Array(
110 exported_ops
111 .iter()
112 .map(|s| Value::String(s.clone()))
113 .collect(),
114 ),
115 );
116 entry.insert(
117 "added_at".to_string(),
118 Value::Number(serde_json::Number::from(added_at)),
119 );
120 entry.insert(
121 "updated_at".to_string(),
122 Value::Number(serde_json::Number::from(now_epoch_seconds())),
123 );
124 components.insert(node_id.to_string(), Value::Object(entry));
125}
126
127pub struct ComponentContractMeta {
128 pub describe_hash: String,
129 pub operation_id: String,
130 pub schema_hash: String,
131 pub component_version: Option<String>,
132 pub world: Option<String>,
133 pub config_schema_cbor: Option<String>,
134}
135
136pub fn clear_component_entry(meta: &mut Option<Value>, node_id: &str) {
137 let Some(Value::Object(root)) = meta else {
138 return;
139 };
140 let Some(Value::Object(greentic)) = root.get_mut(META_NAMESPACE) else {
141 return;
142 };
143 if let Some(Value::Object(components)) = greentic.get_mut("components") {
144 components.remove(node_id);
145 }
146 if let Some(Value::Object(secrets)) = greentic.get_mut("secrets_hints") {
147 secrets.remove(node_id);
148 }
149 if let Some(Value::Object(bindings)) = greentic.get_mut("bindings_hints") {
150 bindings.remove(node_id);
151 }
152}
153
154pub fn ensure_hints_empty(meta: &mut Option<Value>, node_id: &str) {
155 let greentic = ensure_greentic_meta(meta);
156 {
157 let secrets = ensure_child_map(greentic, "secrets_hints");
158 secrets
159 .entry(node_id.to_string())
160 .or_insert_with(|| Value::Array(Vec::new()));
161 }
162 {
163 let bindings = ensure_child_map(greentic, "bindings_hints");
164 bindings
165 .entry(node_id.to_string())
166 .or_insert_with(|| Value::Array(Vec::new()));
167 }
168}
169
170pub fn find_node_for_component(meta: &Option<Value>, component_id: &str) -> Result<String> {
171 let Some(Value::Object(root)) = meta else {
172 return Err(anyhow!("flow metadata missing; provide --step"));
173 };
174 let Some(Value::Object(greentic)) = root.get(META_NAMESPACE) else {
175 return Err(anyhow!("flow metadata missing; provide --step"));
176 };
177 let Some(Value::Object(components)) = greentic.get("components") else {
178 return Err(anyhow!("flow metadata missing; provide --step"));
179 };
180 let mut matches = Vec::new();
181 for (node_id, entry) in components {
182 if let Value::Object(obj) = entry
183 && obj
184 .get("component_id")
185 .and_then(Value::as_str)
186 .is_some_and(|id| id == component_id)
187 {
188 matches.push(node_id.clone());
189 }
190 }
191 match matches.len() {
192 0 => Err(anyhow!(
193 "no node found for component id '{component_id}'; provide --step"
194 )),
195 1 => Ok(matches.remove(0)),
196 _ => Err(anyhow!(
197 "multiple nodes found for component id '{component_id}'; provide --step"
198 )),
199 }
200}