1#![forbid(unsafe_code)]
2
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result, anyhow};
8use greentic_types::Flow;
9use greentic_types::error::ErrorCode;
10use greentic_types::flow_resolve::{
11 FlowResolveV1, read_flow_resolve, sidecar_path_for_flow, write_flow_resolve,
12};
13
14use crate::config::FlowConfig;
15
16#[derive(Clone, Debug)]
17pub struct FlowResolveSidecar {
18 pub flow_id: String,
19 pub flow_path: PathBuf,
20 pub sidecar_path: PathBuf,
21 pub document: Option<FlowResolveV1>,
22 pub warning: Option<String>,
23}
24
25pub fn discover_flow_resolves(pack_dir: &Path, flows: &[FlowConfig]) -> Vec<FlowResolveSidecar> {
29 flows
30 .iter()
31 .map(|flow| {
32 let flow_path = if flow.file.is_absolute() {
33 flow.file.clone()
34 } else {
35 pack_dir.join(&flow.file)
36 };
37 let sidecar_path = sidecar_path_for_flow(&flow_path);
38
39 let (document, warning) = match read_flow_resolve(&sidecar_path) {
40 Ok(doc) => (Some(doc), None),
41 Err(err) if err.code == ErrorCode::NotFound => (
42 None,
43 Some(format!(
44 "flow resolve sidecar missing for {} ({})",
45 flow.id,
46 sidecar_path.display()
47 )),
48 ),
49 Err(err) => (
50 None,
51 Some(format!(
52 "failed to read flow resolve sidecar for {}: {}",
53 flow.id, err
54 )),
55 ),
56 };
57
58 FlowResolveSidecar {
59 flow_id: flow.id.clone(),
60 flow_path,
61 sidecar_path,
62 document,
63 warning,
64 }
65 })
66 .collect()
67}
68
69pub fn ensure_sidecar_exists(
74 pack_dir: &Path,
75 flow: &FlowConfig,
76 compiled: &Flow,
77 strict: bool,
78) -> Result<()> {
79 let flow_path = if flow.file.is_absolute() {
80 flow.file.clone()
81 } else {
82 pack_dir.join(&flow.file)
83 };
84 let sidecar_path = sidecar_path_for_flow(&flow_path);
85
86 let doc = match read_flow_resolve(&sidecar_path) {
87 Ok(doc) => doc,
88 Err(err) if err.code == ErrorCode::NotFound => {
89 let doc = FlowResolveV1 {
90 schema_version: 1,
91 flow: flow.file.to_string_lossy().into_owned(),
92 nodes: BTreeMap::new(),
93 };
94 if let Some(parent) = sidecar_path.parent() {
95 fs::create_dir_all(parent)
96 .with_context(|| format!("failed to create {}", parent.display()))?;
97 }
98 write_flow_resolve(&sidecar_path, &doc)
99 .with_context(|| format!("failed to write {}", sidecar_path.display()))?;
100 doc
101 }
102 Err(err) => {
103 return Err(anyhow!(
104 "failed to read flow resolve sidecar for {}: {}",
105 flow.id,
106 err
107 ));
108 }
109 };
110
111 let missing = missing_node_mappings(compiled, &doc);
112 if !missing.is_empty() {
113 if strict {
114 anyhow::bail!(
115 "flow {} is missing resolve entries for nodes {} (sidecar {}). Add mappings to the sidecar, then rerun `greentic-pack resolve` followed by `greentic-pack build`.",
116 flow.id,
117 missing.join(", "),
118 sidecar_path.display()
119 );
120 } else {
121 eprintln!(
122 "warning: flow {} has no resolve entries for nodes {} ({}); add mappings to the sidecar and rerun `greentic-pack resolve`",
123 flow.id,
124 missing.join(", "),
125 sidecar_path.display()
126 );
127 }
128 }
129
130 Ok(())
131}
132
133pub fn enforce_sidecar_mappings(pack_dir: &Path, flow: &FlowConfig, compiled: &Flow) -> Result<()> {
135 let flow_path = if flow.file.is_absolute() {
136 flow.file.clone()
137 } else {
138 pack_dir.join(&flow.file)
139 };
140 let sidecar_path = sidecar_path_for_flow(&flow_path);
141 let doc = read_flow_resolve(&sidecar_path).map_err(|err| {
142 anyhow!(
143 "flow {} requires a resolve sidecar; expected {}: {}",
144 flow.id,
145 sidecar_path.display(),
146 err
147 )
148 })?;
149
150 let missing = missing_node_mappings(compiled, &doc);
151 if !missing.is_empty() {
152 anyhow::bail!(
153 "flow {} is missing resolve entries for nodes {} (sidecar {}). Add mappings to the sidecar, then rerun `greentic-pack resolve` followed by `greentic-pack build`.",
154 flow.id,
155 missing.join(", "),
156 sidecar_path.display()
157 );
158 }
159
160 Ok(())
161}
162
163pub fn missing_node_mappings(flow: &Flow, doc: &FlowResolveV1) -> Vec<String> {
165 flow.nodes
166 .keys()
167 .filter_map(|node| {
168 let id = node.to_string();
169 if doc.nodes.contains_key(id.as_str()) {
170 None
171 } else {
172 Some(id)
173 }
174 })
175 .collect()
176}