Skip to main content

camel_component_validator/
compiled.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use camel_component_api::{Body, CamelError};
5use serde_yml::Value as YamlValue;
6
7use crate::config::{SchemaType, ValidatorConfig};
8use crate::error::ValidatorError;
9use crate::xsd_bridge::XsdBridge;
10
11pub(crate) enum CompiledValidator {
12    Xml {
13        /// Raw XSD bytes. `register` is called lazily on first validation so
14        /// bridge startup happens in an async context (avoiding runtime issues
15        /// caused by block_on's temporary runtime).
16        xsd_bytes: Vec<u8>,
17        backend: Arc<dyn XsdBridge>,
18    },
19    Json(Arc<jsonschema::Validator>),
20    Yaml(Arc<jsonschema::Validator>),
21}
22
23impl std::fmt::Debug for CompiledValidator {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("CompiledValidator").finish_non_exhaustive()
26    }
27}
28
29impl CompiledValidator {
30    pub fn compile(
31        config: &ValidatorConfig,
32        xsd_backend: Arc<dyn XsdBridge>,
33    ) -> Result<Self, CamelError> {
34        let path = &config.schema_path;
35
36        let content = std::fs::read(path).map_err(|e| {
37            CamelError::EndpointCreationFailed(format!(
38                "failed to read schema file '{}': {e}",
39                path.display()
40            ))
41        })?;
42
43        match config.schema_type {
44            SchemaType::Xml => Ok(Self::compile_xsd(&content, xsd_backend)),
45            SchemaType::Json => Self::compile_json(&content, path),
46            SchemaType::Yaml => Self::compile_yaml_schema(&content, path),
47            SchemaType::RelaxNg | SchemaType::Schematron => Err(ValidatorError::UnsupportedMode(
48                "RelaxNG/Schematron require a future xml-bridge update",
49            )
50            .to_endpoint_error()),
51        }
52    }
53
54    fn compile_xsd(content: &[u8], backend: Arc<dyn XsdBridge>) -> Self {
55        // Bridge startup and schema registration are deferred to the first validate()
56        // call, which runs in a proper async context. This avoids runtime issues
57        // caused by block_on's temporary runtime (channel I/O tasks would die with it).
58        CompiledValidator::Xml {
59            xsd_bytes: content.to_vec(),
60            backend,
61        }
62    }
63
64    fn compile_json(content: &[u8], path: &Path) -> Result<Self, CamelError> {
65        let schema_value: serde_json::Value = serde_json::from_slice(content).map_err(|e| {
66            CamelError::EndpointCreationFailed(format!(
67                "invalid JSON schema '{}': {e}",
68                path.display()
69            ))
70        })?;
71
72        let validator = jsonschema::validator_for(&schema_value).map_err(|e| {
73            CamelError::EndpointCreationFailed(format!(
74                "failed to compile JSON schema '{}': {e}",
75                path.display()
76            ))
77        })?;
78
79        Ok(CompiledValidator::Json(Arc::new(validator)))
80    }
81
82    fn compile_yaml_schema(content: &[u8], path: &Path) -> Result<Self, CamelError> {
83        let yaml_str = std::str::from_utf8(content).map_err(|e| {
84            CamelError::EndpointCreationFailed(format!(
85                "YAML schema '{}' is not valid UTF-8: {e}",
86                path.display()
87            ))
88        })?;
89
90        let yaml_value: YamlValue = serde_yml::from_str(yaml_str).map_err(|e| {
91            CamelError::EndpointCreationFailed(format!(
92                "invalid YAML schema '{}': {e}",
93                path.display()
94            ))
95        })?;
96
97        let schema_value: serde_json::Value = serde_json::to_value(&yaml_value).map_err(|e| {
98            CamelError::EndpointCreationFailed(format!(
99                "failed to convert YAML schema to JSON '{}': {e}",
100                path.display()
101            ))
102        })?;
103
104        let validator = jsonschema::validator_for(&schema_value).map_err(|e| {
105            CamelError::EndpointCreationFailed(format!(
106                "failed to compile YAML schema '{}': {e}",
107                path.display()
108            ))
109        })?;
110
111        Ok(CompiledValidator::Yaml(Arc::new(validator)))
112    }
113
114    pub async fn validate(&self, body: &Body) -> Result<(), CamelError> {
115        match self {
116            CompiledValidator::Xml { xsd_bytes, backend } => {
117                Self::validate_xml(xsd_bytes, backend, body).await
118            }
119            CompiledValidator::Json(validator) => Self::validate_json(validator, body),
120            CompiledValidator::Yaml(validator) => Self::validate_yaml(validator, body),
121        }
122    }
123
124    async fn validate_xml(
125        xsd_bytes: &[u8],
126        backend: &Arc<dyn XsdBridge>,
127        body: &Body,
128    ) -> Result<(), CamelError> {
129        // Register lazily (idempotent: no-op if already registered).
130        let schema_id = backend.register(xsd_bytes.to_vec()).await.map_err(|e| {
131            CamelError::EndpointCreationFailed(format!("XSD schema registration failed: {e}"))
132        })?;
133
134        let xml_bytes = match body {
135            Body::Xml(s) => s.as_bytes().to_vec(),
136            Body::Text(s) => s.as_bytes().to_vec(),
137            Body::Bytes(b) => b.to_vec(),
138            _ => {
139                return Err(CamelError::ProcessorError(
140                    "XSD validator requires Body::Xml, Body::Text, or Body::Bytes".to_string(),
141                ));
142            }
143        };
144
145        backend
146            .validate(&schema_id, xml_bytes)
147            .await
148            .map_err(|e| e.to_processor_error())
149    }
150
151    fn validate_json(validator: &jsonschema::Validator, body: &Body) -> Result<(), CamelError> {
152        let json_value = match body {
153            Body::Json(v) => v.clone(),
154            Body::Text(s) => serde_json::from_str(s)
155                .map_err(|e| CamelError::ProcessorError(format!("body is not valid JSON: {e}")))?,
156            Body::Bytes(b) => serde_json::from_slice(b).map_err(|e| {
157                CamelError::ProcessorError(format!("body bytes are not valid JSON: {e}"))
158            })?,
159            _ => {
160                return Err(CamelError::ProcessorError(
161                    "JSON Schema validator requires Body::Json, Body::Text, or Body::Bytes"
162                        .to_string(),
163                ));
164            }
165        };
166
167        let messages: Vec<String> = validator
168            .iter_errors(&json_value)
169            .map(|e| format!("{e} at {}", e.instance_path()))
170            .collect();
171
172        if messages.is_empty() {
173            Ok(())
174        } else {
175            Err(CamelError::ProcessorError(format!(
176                "JSON Schema validation failed:\n{}",
177                messages.join("\n")
178            )))
179        }
180    }
181
182    fn validate_yaml(validator: &jsonschema::Validator, body: &Body) -> Result<(), CamelError> {
183        let yaml_str = match body {
184            Body::Text(s) => s.as_str(),
185            _ => {
186                return Err(CamelError::ProcessorError(
187                    "YAML validator requires a text body (Body::Text)".to_string(),
188                ));
189            }
190        };
191
192        let yaml_value: serde_yml::Value = serde_yml::from_str(yaml_str)
193            .map_err(|e| CamelError::ProcessorError(format!("body is not valid YAML: {e}")))?;
194
195        let json_value: serde_json::Value = serde_json::to_value(&yaml_value)
196            .map_err(|e| CamelError::ProcessorError(format!("YAML→JSON conversion failed: {e}")))?;
197
198        let messages: Vec<String> = validator
199            .iter_errors(&json_value)
200            .map(|e| format!("{e} at {}", e.instance_path()))
201            .collect();
202
203        if messages.is_empty() {
204            Ok(())
205        } else {
206            Err(CamelError::ProcessorError(format!(
207                "YAML Schema validation failed:\n{}",
208                messages.join("\n")
209            )))
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::config::{SchemaType, ValidatorConfig};
218    use async_trait::async_trait;
219
220    #[derive(Debug, Clone)]
221    struct MockBridge {
222        register_err: Option<ValidatorError>,
223    }
224
225    #[async_trait]
226    impl XsdBridge for MockBridge {
227        async fn register(&self, _xsd_bytes: Vec<u8>) -> Result<String, ValidatorError> {
228            if let Some(err) = &self.register_err {
229                return Err(err.clone());
230            }
231            Ok("xsd-mock".to_string())
232        }
233
234        async fn validate(
235            &self,
236            _schema_id: &str,
237            _doc_bytes: Vec<u8>,
238        ) -> Result<(), ValidatorError> {
239            Ok(())
240        }
241    }
242
243    #[tokio::test]
244    async fn xsd_bridge_register_error_propagates_on_validate() {
245        let mut schema = tempfile::Builder::new().suffix(".xsd").tempfile().unwrap();
246        use std::io::Write;
247        schema.write_all(b"<xs:schema/>").unwrap();
248
249        let cfg = ValidatorConfig {
250            schema_path: schema.path().to_path_buf(),
251            schema_type: SchemaType::Xml,
252        };
253
254        let bridge = Arc::new(MockBridge {
255            register_err: Some(ValidatorError::CompilationFailed(
256                "COMPILATION_FAILED".to_string(),
257            )),
258        });
259
260        // compile() is now sync and always succeeds for XSD (deferred registration)
261        let compiled = CompiledValidator::compile(&cfg, bridge).expect("compile should succeed");
262
263        // The error surfaces on the first validate() call when register() is attempted
264        let err = compiled
265            .validate(&Body::Xml("<order/>".to_string()))
266            .await
267            .expect_err("expected validate to fail due to registration error");
268        assert!(err.to_string().contains("COMPILATION_FAILED"));
269    }
270}