greentic_dev/dev_runner/
runner.rs1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use serde_yaml_bw::Value as YamlValue;
6
7use super::registry::DescribeRegistry;
8use super::schema::{schema_id_from_json, validate_yaml_against_schema};
9use crate::path_safety::normalize_under_root;
10
11#[derive(Clone, Debug, Default)]
12pub struct ComponentSchema {
13 pub node_schema: Option<String>,
14}
15
16pub trait ComponentDescriber {
17 fn describe(&self, component: &str) -> Result<ComponentSchema, String>;
18}
19
20#[derive(Debug, Clone)]
21pub struct StaticComponentDescriber {
22 schemas: HashMap<String, ComponentSchema>,
23 fallback: ComponentSchema,
24}
25
26impl StaticComponentDescriber {
27 pub fn new() -> Self {
28 Self {
29 schemas: HashMap::new(),
30 fallback: ComponentSchema::default(),
31 }
32 }
33
34 pub fn with_fallback(mut self, fallback_schema: ComponentSchema) -> Self {
35 self.fallback = fallback_schema;
36 self
37 }
38
39 pub fn register_schema<S: Into<String>>(
40 &mut self,
41 component: S,
42 schema: ComponentSchema,
43 ) -> &mut Self {
44 self.schemas.insert(component.into(), schema);
45 self
46 }
47}
48
49impl ComponentDescriber for StaticComponentDescriber {
50 fn describe(&self, component: &str) -> Result<ComponentSchema, String> {
51 if let Some(schema) = self.schemas.get(component) {
52 Ok(schema.clone())
53 } else {
54 Ok(self.fallback.clone())
55 }
56 }
57}
58
59impl Default for StaticComponentDescriber {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65pub struct FlowValidator<D> {
66 describer: D,
67 registry: DescribeRegistry,
68}
69
70#[derive(Clone, Debug)]
71pub struct ValidatedNode {
72 pub component: String,
73 pub node_config: YamlValue,
74 pub schema_json: Option<String>,
75 pub schema_id: Option<String>,
76 pub defaults: Option<YamlValue>,
77}
78
79impl<D> FlowValidator<D>
80where
81 D: ComponentDescriber,
82{
83 pub fn new(describer: D, registry: DescribeRegistry) -> Self {
84 Self {
85 describer,
86 registry,
87 }
88 }
89
90 pub fn validate_file<P>(&self, path: P) -> Result<Vec<ValidatedNode>, FlowValidationError>
91 where
92 P: AsRef<Path>,
93 {
94 let path_ref = path.as_ref();
95 let root = std::env::current_dir()
96 .map_err(|error| FlowValidationError::Io {
97 path: path_ref.to_path_buf(),
98 error,
99 })?
100 .canonicalize()
101 .map_err(|error| FlowValidationError::Io {
102 path: path_ref.to_path_buf(),
103 error,
104 })?;
105 let safe =
106 normalize_under_root(&root, path_ref).map_err(|error| FlowValidationError::Io {
107 path: path_ref.to_path_buf(),
108 error: std::io::Error::other(error.to_string()),
109 })?;
110 let source = fs::read_to_string(&safe)
111 .map_err(|error| FlowValidationError::Io { path: safe, error })?;
112 self.validate_str(&source)
113 }
114
115 pub fn validate_str(
116 &self,
117 yaml_source: &str,
118 ) -> Result<Vec<ValidatedNode>, FlowValidationError> {
119 let document: YamlValue = serde_yaml_bw::from_str(yaml_source).map_err(|error| {
120 FlowValidationError::YamlParse {
121 error: error.to_string(),
122 }
123 })?;
124 self.validate_document(&document)
125 }
126
127 pub fn validate_document(
128 &self,
129 document: &YamlValue,
130 ) -> Result<Vec<ValidatedNode>, FlowValidationError> {
131 let nodes = match nodes_from_document(document) {
132 Some(nodes) => nodes,
133 None => {
134 return Err(FlowValidationError::MissingNodes);
135 }
136 };
137
138 let mut validated_nodes = Vec::with_capacity(nodes.len());
139
140 for (index, node) in nodes.iter().enumerate() {
141 let node_mapping = match node.as_mapping() {
142 Some(mapping) => mapping,
143 None => {
144 return Err(FlowValidationError::NodeNotMapping { index });
145 }
146 };
147
148 let component = component_name(node_mapping)
149 .ok_or(FlowValidationError::MissingComponent { index })?;
150
151 let schema = self.describer.describe(component).map_err(|error| {
152 FlowValidationError::DescribeFailed {
153 component: component.to_owned(),
154 error,
155 }
156 })?;
157
158 let schema_json = self
159 .registry
160 .get_schema(component)
161 .map(|schema| schema.to_owned())
162 .or_else(|| schema.node_schema.clone());
163
164 let schema_id = schema_json.as_deref().and_then(schema_id_from_json);
165
166 if let Some(schema_json) = schema_json.as_deref() {
167 validate_yaml_against_schema(node, schema_json).map_err(|message| {
168 FlowValidationError::SchemaValidation {
169 component: component.to_owned(),
170 index,
171 message,
172 }
173 })?;
174 }
175
176 let defaults = self.registry.get_defaults(component).cloned();
177
178 validated_nodes.push(ValidatedNode {
179 component: component.to_owned(),
180 node_config: node.clone(),
181 schema_json,
182 schema_id,
183 defaults,
184 });
185 }
186
187 Ok(validated_nodes)
188 }
189}
190
191fn nodes_from_document(document: &YamlValue) -> Option<&Vec<YamlValue>> {
192 if let Some(sequence) = document.as_sequence() {
193 return Some(&**sequence);
194 }
195
196 let mapping = document.as_mapping()?;
197 mapping
198 .get("nodes")
199 .and_then(|value| value.as_sequence().map(|sequence| &**sequence))
200}
201
202fn component_name(mapping: &serde_yaml_bw::Mapping) -> Option<&str> {
203 mapping
204 .get("component")
205 .and_then(|value| value.as_str())
206 .or_else(|| mapping.get("type").and_then(|value| value.as_str()))
207}
208
209#[derive(Debug)]
210pub enum FlowValidationError {
211 Io {
212 path: PathBuf,
213 error: std::io::Error,
214 },
215 YamlParse {
216 error: String,
217 },
218 MissingNodes,
219 NodeNotMapping {
220 index: usize,
221 },
222 MissingComponent {
223 index: usize,
224 },
225 DescribeFailed {
226 component: String,
227 error: String,
228 },
229 SchemaValidation {
230 component: String,
231 index: usize,
232 message: String,
233 },
234}