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