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