camel_component_validator/
compiled.rs1use 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 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 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 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 let compiled = CompiledValidator::compile(&cfg, bridge).expect("compile should succeed");
262
263 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}