greentic_dev/dev_runner/
runner.rs

1use 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}