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