greentic_dev/dev_runner/
transcript.rs1use std::collections::HashSet;
2use std::error::Error;
3use std::fmt;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use serde::{Deserialize, Serialize};
9use serde_yaml_bw::{Mapping, Value as YamlValue};
10
11use super::runner::ValidatedNode;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct NodeTranscript {
15 pub node_name: String,
16 pub resolved_config: YamlValue,
17 pub schema_id: Option<String>,
18 pub run_log: Vec<String>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FlowTranscript {
23 pub flow_name: String,
24 pub flow_path: String,
25 pub generated_at: u64,
26 pub nodes: Vec<NodeTranscript>,
27}
28
29#[derive(Clone, Debug)]
30pub struct TranscriptStore {
31 root: PathBuf,
32}
33
34#[derive(Debug)]
35pub enum TranscriptError {
36 Io(std::io::Error),
37 Serialize(serde_yaml_bw::Error),
38}
39
40impl TranscriptStore {
41 pub fn with_root<P: Into<PathBuf>>(root: P) -> Self {
42 Self { root: root.into() }
43 }
44
45 pub fn write_transcript<P>(
46 &self,
47 flow_path: P,
48 transcript: &FlowTranscript,
49 ) -> Result<PathBuf, TranscriptError>
50 where
51 P: AsRef<Path>,
52 {
53 let flow_path = flow_path.as_ref();
54 let flow_stem = flow_path
55 .file_stem()
56 .and_then(|stem| stem.to_str())
57 .unwrap_or("flow");
58
59 let output_path = self
60 .root
61 .join(format!("{}-{}.yaml", flow_stem, transcript.generated_at));
62
63 if let Some(parent) = output_path.parent() {
64 fs::create_dir_all(parent)?;
65 }
66
67 let serialized = serde_yaml_bw::to_string(transcript)?;
68 fs::write(&output_path, serialized)?;
69
70 Ok(output_path)
71 }
72}
73
74impl Default for TranscriptStore {
75 fn default() -> Self {
76 Self::with_root(".greentic/transcripts")
77 }
78}
79
80impl FlowTranscript {
81 pub fn from_validated_nodes<P: AsRef<Path>>(flow_path: P, nodes: &[ValidatedNode]) -> Self {
82 let flow_path_ref = flow_path.as_ref();
83 let flow_name = flow_path_ref
84 .file_name()
85 .and_then(|name| name.to_str())
86 .unwrap_or("flow")
87 .to_string();
88
89 let generated_at = SystemTime::now()
90 .duration_since(UNIX_EPOCH)
91 .unwrap_or_default()
92 .as_secs();
93
94 let node_transcripts = nodes.iter().map(node_transcript_from_validated).collect();
95
96 Self {
97 flow_name,
98 flow_path: flow_path_ref.to_string_lossy().to_string(),
99 generated_at,
100 nodes: node_transcripts,
101 }
102 }
103}
104
105impl NodeTranscript {
106 pub fn merged_config(&self) -> &YamlValue {
107 &self.resolved_config
108 }
109}
110
111impl fmt::Display for TranscriptError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 TranscriptError::Io(error) => write!(f, "failed to write transcript: {error}"),
115 TranscriptError::Serialize(error) => {
116 write!(f, "failed to serialize transcript: {error}")
117 }
118 }
119 }
120}
121
122impl Error for TranscriptError {
123 fn source(&self) -> Option<&(dyn Error + 'static)> {
124 match self {
125 TranscriptError::Io(error) => Some(error),
126 TranscriptError::Serialize(error) => Some(error),
127 }
128 }
129}
130
131impl From<std::io::Error> for TranscriptError {
132 fn from(value: std::io::Error) -> Self {
133 TranscriptError::Io(value)
134 }
135}
136
137impl From<serde_yaml_bw::Error> for TranscriptError {
138 fn from(value: serde_yaml_bw::Error) -> Self {
139 TranscriptError::Serialize(value)
140 }
141}
142
143fn node_transcript_from_validated(node: &ValidatedNode) -> NodeTranscript {
144 let (resolved_config, run_log) = merge_with_defaults(node.defaults.as_ref(), &node.node_config);
145 let node_name = node_name(&node.node_config, &node.component);
146
147 NodeTranscript {
148 node_name,
149 resolved_config,
150 schema_id: node.schema_id.clone(),
151 run_log,
152 }
153}
154
155fn node_name(node_config: &YamlValue, fallback: &str) -> String {
156 node_config
157 .as_mapping()
158 .and_then(|mapping| mapping.get("id"))
159 .and_then(|value| value.as_str())
160 .unwrap_or(fallback)
161 .to_string()
162}
163
164fn merge_with_defaults(
165 defaults: Option<&YamlValue>,
166 overrides: &YamlValue,
167) -> (YamlValue, Vec<String>) {
168 let mut run_log = Vec::new();
169 let mut path = Vec::new();
170 let resolved = merge_node(defaults, overrides, &mut path, &mut run_log);
171
172 let mut seen = HashSet::new();
174 run_log.retain(|entry| seen.insert(entry.clone()));
175
176 (resolved, run_log)
177}
178
179fn merge_node(
180 defaults: Option<&YamlValue>,
181 overrides: &YamlValue,
182 path: &mut Vec<String>,
183 run_log: &mut Vec<String>,
184) -> YamlValue {
185 match (defaults, overrides) {
186 (Some(YamlValue::Mapping(default_map)), YamlValue::Mapping(override_map)) => {
187 let mut result = Mapping::new();
188
189 for (key, default_value) in default_map {
190 let key_str = key_to_segment(key);
191 path.push(key_str.clone());
192 if let Some(override_value) = override_map.get(key) {
193 let merged = merge_node(Some(default_value), override_value, path, run_log);
194 result.insert(key.clone(), merged);
195 } else {
196 log_default(path, run_log);
197 result.insert(key.clone(), default_value.clone());
198 }
199 path.pop();
200 }
201
202 for (key, override_value) in override_map {
203 if default_map.contains_key(key) {
204 continue;
205 }
206 let key_str = key_to_segment(key);
207 path.push(key_str.clone());
208 log_override(path, run_log);
209 let merged = merge_node(None, override_value, path, run_log);
210 result.insert(key.clone(), merged);
211 path.pop();
212 }
213
214 YamlValue::Mapping(result)
215 }
216 (Some(YamlValue::Sequence(default_seq)), YamlValue::Sequence(override_seq)) => {
217 if let Some(path_str) = path_string(path) {
218 if default_seq == override_seq {
219 run_log.push(format!("default: {path_str}"));
220 } else {
221 run_log.push(format!("override: {path_str}"));
222 }
223 }
224 YamlValue::Sequence(override_seq.clone())
225 }
226 (Some(default_value), override_value) => {
227 if let Some(path_str) = path_string(path) {
228 if default_value == override_value {
229 run_log.push(format!("default: {path_str}"));
230 } else {
231 run_log.push(format!("override: {path_str}"));
232 }
233 }
234 override_value.clone()
235 }
236 (None, override_value) => {
237 if let Some(path_str) = path_string(path) {
238 run_log.push(format!("override: {path_str}"));
239 }
240 override_value.clone()
241 }
242 }
243}
244
245fn log_default(path: &[String], run_log: &mut Vec<String>) {
246 if let Some(path_str) = path_string(path) {
247 run_log.push(format!("default: {path_str}"));
248 }
249}
250
251fn log_override(path: &[String], run_log: &mut Vec<String>) {
252 if let Some(path_str) = path_string(path) {
253 run_log.push(format!("override: {path_str}"));
254 }
255}
256
257fn path_string(path: &[String]) -> Option<String> {
258 if path.is_empty() {
259 None
260 } else {
261 Some(path.join("."))
262 }
263}
264
265fn key_to_segment(key: &YamlValue) -> String {
266 key.as_str()
267 .map(|s| s.to_string())
268 .or_else(|| key.as_u64().map(|n| n.to_string()))
269 .unwrap_or_else(|| "unknown".to_string())
270}