camel_component_validator/
compiled.rs1use std::path::Path;
2use std::sync::Arc;
3
4use camel_component_api::{Body, CamelError};
5use noyalib::compat::serde_yaml as serde_yml;
8use serde_yml::Value as YamlValue;
9
10use crate::config::{SchemaType, ValidatorConfig};
11use crate::error::ValidatorError;
12use crate::resolver::{FilesystemResolver, ResourceResolver};
13use crate::xsd_bridge::XsdBridge;
14
15pub(crate) fn approx_body_byte_len(body: &Body) -> Option<usize> {
20 match body {
21 Body::Empty => Some(0),
22 Body::Bytes(b) => Some(b.len()),
23 Body::Text(s) => Some(s.len()),
24 Body::Xml(s) => Some(s.len()),
25 Body::Json(v) => serde_json::to_vec(v).ok().map(|v| v.len()),
26 Body::Stream(_) => None,
27 }
28}
29
30pub(crate) enum CompiledValidator {
31 Xml {
32 xsd_bytes: Vec<u8>,
36 backend: Arc<dyn XsdBridge>,
37 max_payload_bytes: Option<usize>,
38 },
39 Json {
40 validator: Arc<jsonschema::Validator>,
41 max_payload_bytes: Option<usize>,
42 },
43 Yaml {
44 validator: Arc<jsonschema::Validator>,
45 max_payload_bytes: Option<usize>,
46 },
47}
48
49impl std::fmt::Debug for CompiledValidator {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("CompiledValidator").finish_non_exhaustive()
52 }
53}
54
55impl CompiledValidator {
56 pub fn compile(
57 config: &ValidatorConfig,
58 xsd_backend: Arc<dyn XsdBridge>,
59 ) -> Result<Self, CamelError> {
60 let path = &config.schema_path;
61
62 let resolver = FilesystemResolver;
63 let path_str = path.to_str().ok_or_else(|| {
64 CamelError::EndpointCreationFailed("schema path contains non-UTF-8 characters".into())
65 })?;
66 let content = resolver.resolve(path_str)?;
67
68 match config.schema_type {
69 SchemaType::Xml => Ok(Self::compile_xsd(
70 &content,
71 xsd_backend,
72 config.max_payload_bytes,
73 )),
74 SchemaType::Json => Self::compile_json(&content, path, config.max_payload_bytes),
75 SchemaType::Yaml => Self::compile_yaml_schema(&content, path, config.max_payload_bytes),
76 SchemaType::RelaxNg | SchemaType::Schematron => Err(CamelError::Config(format!(
77 "schema type {:?} is not yet supported; supported: Xml, Json, Yaml",
78 config.schema_type
79 ))),
80 }
81 }
82
83 fn compile_xsd(
84 content: &[u8],
85 backend: Arc<dyn XsdBridge>,
86 max_payload_bytes: Option<usize>,
87 ) -> Self {
88 CompiledValidator::Xml {
92 xsd_bytes: content.to_vec(),
93 backend,
94 max_payload_bytes,
95 }
96 }
97
98 fn compile_json(
99 content: &[u8],
100 path: &Path,
101 max_payload_bytes: Option<usize>,
102 ) -> Result<Self, CamelError> {
103 let schema_value: serde_json::Value = serde_json::from_slice(content).map_err(|e| {
104 CamelError::EndpointCreationFailed(format!(
105 "invalid JSON schema '{}': {e}",
106 path.display()
107 ))
108 })?;
109
110 let validator = jsonschema::validator_for(&schema_value).map_err(|e| {
111 CamelError::EndpointCreationFailed(format!(
112 "failed to compile JSON schema '{}': {e}",
113 path.display()
114 ))
115 })?;
116
117 Ok(CompiledValidator::Json {
118 validator: Arc::new(validator),
119 max_payload_bytes,
120 })
121 }
122
123 fn compile_yaml_schema(
124 content: &[u8],
125 path: &Path,
126 max_payload_bytes: Option<usize>,
127 ) -> Result<Self, CamelError> {
128 let yaml_str = std::str::from_utf8(content).map_err(|e| {
129 CamelError::EndpointCreationFailed(format!(
130 "YAML schema '{}' is not valid UTF-8: {e}",
131 path.display()
132 ))
133 })?;
134
135 let yaml_value: YamlValue = serde_yml::from_str(yaml_str).map_err(|e| {
136 CamelError::EndpointCreationFailed(format!(
137 "invalid YAML schema '{}': {e}",
138 path.display()
139 ))
140 })?;
141
142 let schema_value: serde_json::Value = serde_json::to_value(&yaml_value).map_err(|e| {
143 CamelError::EndpointCreationFailed(format!(
144 "failed to convert YAML schema to JSON '{}': {e}",
145 path.display()
146 ))
147 })?;
148
149 let validator = jsonschema::validator_for(&schema_value).map_err(|e| {
150 CamelError::EndpointCreationFailed(format!(
151 "failed to compile YAML schema '{}': {e}",
152 path.display()
153 ))
154 })?;
155
156 Ok(CompiledValidator::Yaml {
157 validator: Arc::new(validator),
158 max_payload_bytes,
159 })
160 }
161
162 pub async fn validate(&self, body: &Body) -> Result<(), CamelError> {
163 if let Some(limit) = self.max_payload_bytes()
164 && let Some(actual) = approx_body_byte_len(body)
165 && actual > limit
166 {
167 return Err(ValidatorError::PayloadTooLarge { actual, limit }.to_processor_error());
168 }
169
170 match self {
171 CompiledValidator::Xml {
172 xsd_bytes, backend, ..
173 } => Self::validate_xml(xsd_bytes, backend, body).await,
174 CompiledValidator::Json { validator, .. } => Self::validate_json(validator, body),
175 CompiledValidator::Yaml { validator, .. } => Self::validate_yaml(validator, body),
176 }
177 }
178
179 fn max_payload_bytes(&self) -> Option<usize> {
180 match self {
181 CompiledValidator::Xml {
182 max_payload_bytes, ..
183 } => *max_payload_bytes,
184 CompiledValidator::Json {
185 max_payload_bytes, ..
186 } => *max_payload_bytes,
187 CompiledValidator::Yaml {
188 max_payload_bytes, ..
189 } => *max_payload_bytes,
190 }
191 }
192
193 async fn validate_xml(
194 xsd_bytes: &[u8],
195 backend: &Arc<dyn XsdBridge>,
196 body: &Body,
197 ) -> Result<(), CamelError> {
198 let schema_id = backend
200 .register(xsd_bytes.to_vec())
201 .await
202 .map_err(|e| CamelError::Config(format!("XSD registration failed: {e}")))?;
203
204 let xml_bytes = match body {
205 Body::Xml(s) => s.as_bytes().to_vec(),
206 Body::Text(s) => s.as_bytes().to_vec(),
207 Body::Bytes(b) => b.to_vec(),
208 _ => {
209 return Err(CamelError::ProcessorError(
210 "XSD validator requires Body::Xml, Body::Text, or Body::Bytes".to_string(),
211 ));
212 }
213 };
214
215 backend
216 .validate(&schema_id, xml_bytes)
217 .await
218 .map_err(|e| e.to_processor_error())
219 }
220
221 fn validate_json(validator: &jsonschema::Validator, body: &Body) -> Result<(), CamelError> {
222 let json_value = match body {
223 Body::Json(v) => v.clone(),
224 Body::Text(s) => serde_json::from_str(s)
225 .map_err(|e| CamelError::ProcessorError(format!("body is not valid JSON: {e}")))?,
226 Body::Bytes(b) => serde_json::from_slice(b).map_err(|e| {
227 CamelError::ProcessorError(format!("body bytes are not valid JSON: {e}"))
228 })?,
229 _ => {
230 return Err(CamelError::ProcessorError(
231 "JSON Schema validator requires Body::Json, Body::Text, or Body::Bytes"
232 .to_string(),
233 ));
234 }
235 };
236
237 let messages: Vec<String> = validator
238 .iter_errors(&json_value)
239 .map(|e| format!("{e} at {}", e.instance_path()))
240 .collect();
241
242 if messages.is_empty() {
243 Ok(())
244 } else {
245 Err(CamelError::ProcessorError(format!(
246 "JSON Schema validation failed:\n{}",
247 messages.join("\n")
248 )))
249 }
250 }
251
252 fn validate_yaml(validator: &jsonschema::Validator, body: &Body) -> Result<(), CamelError> {
253 let yaml_str = match body {
254 Body::Text(s) => s.as_str(),
255 _ => {
256 return Err(CamelError::ProcessorError(
257 "YAML validator requires a text body (Body::Text)".to_string(),
258 ));
259 }
260 };
261
262 let yaml_value: serde_yml::Value = serde_yml::from_str(yaml_str)
263 .map_err(|e| CamelError::ProcessorError(format!("body is not valid YAML: {e}")))?;
264
265 let json_value: serde_json::Value = serde_json::to_value(&yaml_value)
266 .map_err(|e| CamelError::ProcessorError(format!("YAML→JSON conversion failed: {e}")))?;
267
268 let messages: Vec<String> = validator
269 .iter_errors(&json_value)
270 .map(|e| format!("{e} at {}", e.instance_path()))
271 .collect();
272
273 if messages.is_empty() {
274 Ok(())
275 } else {
276 Err(CamelError::ProcessorError(format!(
277 "YAML Schema validation failed:\n{}",
278 messages.join("\n")
279 )))
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::config::{DEFAULT_SCHEMA_CACHE_MAX_ENTRIES, SchemaType, ValidatorConfig};
288 use async_trait::async_trait;
289
290 #[derive(Debug, Clone)]
291 struct MockBridge {
292 register_err: Option<ValidatorError>,
293 }
294
295 #[async_trait]
296 impl XsdBridge for MockBridge {
297 async fn register(&self, _xsd_bytes: Vec<u8>) -> Result<String, ValidatorError> {
298 if let Some(err) = &self.register_err {
299 return Err(err.clone());
300 }
301 Ok("xsd-mock".to_string())
302 }
303
304 async fn validate(
305 &self,
306 _schema_id: &str,
307 _doc_bytes: Vec<u8>,
308 ) -> Result<(), ValidatorError> {
309 Ok(())
310 }
311 }
312
313 #[tokio::test]
314 async fn xsd_bridge_register_error_propagates_on_validate() {
315 let mut schema = tempfile::Builder::new().suffix(".xsd").tempfile().unwrap();
316 use std::io::Write;
317 schema.write_all(b"<xs:schema/>").unwrap();
318
319 let cfg = ValidatorConfig {
320 schema_path: schema.path().to_path_buf(),
321 schema_type: SchemaType::Xml,
322 max_payload_bytes: None,
323 schema_cache_max_entries: DEFAULT_SCHEMA_CACHE_MAX_ENTRIES,
324 fail_on_null_body: true,
325 header_name: None,
326 fail_on_null_header: true,
327 };
328
329 let bridge = Arc::new(MockBridge {
330 register_err: Some(ValidatorError::CompilationFailed {
331 message: "COMPILATION_FAILED".to_string(),
332 source: None,
333 }),
334 });
335
336 let compiled = CompiledValidator::compile(&cfg, bridge).expect("compile should succeed");
338
339 let err = compiled
341 .validate(&Body::Xml("<order/>".to_string()))
342 .await
343 .expect_err("expected validate to fail due to registration error");
344 assert!(matches!(err, CamelError::Config(_)));
345 assert!(err.to_string().contains("COMPILATION_FAILED"));
346 }
347
348 #[test]
349 fn approx_body_byte_len_variants() {
350 assert_eq!(approx_body_byte_len(&Body::Empty), Some(0));
351 assert_eq!(
352 approx_body_byte_len(&Body::Text("hello".to_string())),
353 Some(5)
354 );
355 assert_eq!(
356 approx_body_byte_len(&Body::Xml("<a/>".to_string())),
357 Some(4)
358 );
359 let json_len = approx_body_byte_len(&Body::Json(serde_json::json!({"id": 1})));
361 assert!(json_len.is_some());
362 assert!(json_len.unwrap() > 0);
363 }
364}