1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::{Context, Poll};
5
6use tower::Service;
7use tracing::debug;
8
9use crate::compiled::CompiledValidator;
10use crate::config::ValidatorConfig;
11use crate::xsd_bridge::{XsdBridge, XsdBridgeBackend};
12use camel_component_api::ComponentContext;
13use camel_component_api::{
14 BoxProcessor, CamelError, Component, ComponentMetadata, Consumer, Endpoint, Exchange,
15 ProducerContext, RuntimeObservability,
16};
17
18use crate::metadata::ValidatorMetadataDescriptor;
19
20pub struct ValidatorComponent {
21 xsd_bridge: Arc<dyn XsdBridge>,
22 xsd_backend: Option<Arc<XsdBridgeBackend>>,
23}
24
25impl ValidatorComponent {
26 pub fn new() -> Self {
27 let xsd_backend = Arc::new(XsdBridgeBackend::new());
28 Self {
29 xsd_bridge: Arc::clone(&xsd_backend) as Arc<dyn XsdBridge>,
30 xsd_backend: Some(xsd_backend),
31 }
32 }
33
34 pub fn xsd_bridge_backend(&self) -> Option<Arc<XsdBridgeBackend>> {
35 self.xsd_backend.as_ref().map(Arc::clone)
36 }
37
38 #[cfg(test)]
39 fn with_xsd_bridge(xsd_bridge: Arc<dyn XsdBridge>) -> Self {
40 Self {
41 xsd_bridge,
42 xsd_backend: None,
43 }
44 }
45}
46
47impl Default for ValidatorComponent {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl Component for ValidatorComponent {
54 fn scheme(&self) -> &str {
55 "validator"
56 }
57
58 fn metadata(&self) -> ComponentMetadata {
59 ValidatorMetadataDescriptor::metadata()
60 }
61
62 fn create_endpoint(
63 &self,
64 uri: &str,
65 _ctx: &dyn ComponentContext,
66 ) -> Result<Box<dyn Endpoint>, CamelError> {
67 let config = ValidatorConfig::from_uri(uri)?;
68 let xsd_bridge = Arc::clone(&self.xsd_bridge);
69 let compiled = CompiledValidator::compile(&config, xsd_bridge)?;
70 Ok(Box::new(ValidatorEndpoint {
71 uri: uri.to_string(),
72 config,
73 compiled: Arc::new(compiled),
74 xsd_backend: self.xsd_backend.as_ref().map(Arc::clone),
75 }))
76 }
77}
78
79struct ValidatorEndpoint {
85 uri: String,
86 config: ValidatorConfig,
87 compiled: Arc<CompiledValidator>,
88 xsd_backend: Option<Arc<XsdBridgeBackend>>,
89}
90
91impl ValidatorEndpoint {
92 #[allow(dead_code)]
94 pub fn schema_info(&self) -> String {
95 format!(
96 "{:?} schema: {}",
97 self.config.schema_type,
98 self.config.schema_path.display()
99 )
100 }
101}
102
103impl Endpoint for ValidatorEndpoint {
104 fn uri(&self) -> &str {
105 &self.uri
106 }
107
108 fn create_consumer(
109 &self,
110 _rt: Arc<dyn RuntimeObservability>,
111 ) -> Result<Box<dyn Consumer>, CamelError> {
112 Err(CamelError::EndpointCreationFailed(
113 "validator endpoint does not support consumers".to_string(),
114 ))
115 }
116
117 fn create_producer(
118 &self,
119 rt: Arc<dyn RuntimeObservability>,
120 ctx: &ProducerContext,
121 ) -> Result<BoxProcessor, CamelError> {
122 if let Some(ref backend) = self.xsd_backend {
123 backend.set_observability(
124 rt.clone(),
125 ctx.route_id().unwrap_or("validator-bridge").to_string(),
126 );
127 }
128 Ok(BoxProcessor::new(ValidatorProducer {
129 uri: self.uri.clone(),
130 config: self.config.clone(),
131 compiled: Arc::clone(&self.compiled),
132 }))
133 }
134}
135
136#[derive(Clone)]
137struct ValidatorProducer {
138 uri: String,
139 config: ValidatorConfig,
140 compiled: Arc<CompiledValidator>,
141}
142
143impl Service<Exchange> for ValidatorProducer {
144 type Response = Exchange;
145 type Error = CamelError;
146 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
147
148 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
149 Poll::Ready(Ok(()))
150 }
151
152 fn call(&mut self, exchange: Exchange) -> Self::Future {
153 let compiled = Arc::clone(&self.compiled);
154 let uri = self.uri.clone();
155 let config = self.config.clone();
156 Box::pin(async move {
157 debug!(
158 uri = %camel_api::redact::redact_url_fail_closed(&uri),
159 "validating exchange body"
160 );
161
162 if let Some(ref header_name) = config.header_name {
164 match exchange.input.header(header_name) {
165 Some(value) => {
166 let header_str = match value.as_str() {
168 Some(s) => s.to_string(),
169 None => value.to_string(),
170 };
171 let header_body = camel_component_api::Body::Text(header_str);
172 compiled.validate(&header_body).await?;
173 }
174 None => {
175 if config.fail_on_null_header {
177 return Err(CamelError::ProcessorError(format!(
178 "header '{header_name}' is missing and failOnNullHeader is true"
179 )));
180 }
181 }
183 }
184 return Ok(exchange);
185 }
186
187 if exchange.input.body.is_empty() {
189 if config.fail_on_null_body {
190 return Err(CamelError::ProcessorError(
191 "body is empty and failOnNullBody is true".to_string(),
192 ));
193 }
194 return Ok(exchange);
196 }
197
198 compiled.validate(&exchange.input.body).await?;
199 Ok(exchange)
200 })
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use camel_component_api::test_support::PanicRuntimeObservability;
207 fn rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
208 std::sync::Arc::new(PanicRuntimeObservability)
209 }
210
211 use super::*;
212 use crate::error::ValidatorError;
213 use async_trait::async_trait;
214 use camel_component_api::{Message, NoOpComponentContext};
215 use std::io::Write;
216 use std::sync::atomic::{AtomicUsize, Ordering};
217 use tower::ServiceExt;
218
219 fn json_schema_file() -> tempfile::NamedTempFile {
220 let mut f = tempfile::Builder::new().suffix(".json").tempfile().unwrap();
221 f.write_all(br#"{"type":"object","required":["id"]}"#)
222 .unwrap();
223 f
224 }
225
226 fn xsd_file() -> tempfile::NamedTempFile {
227 let mut f = tempfile::Builder::new().suffix(".xsd").tempfile().unwrap();
228 f.write_all(
229 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:element name="order" type="xs:string"/></xs:schema>"#,
230 ).unwrap();
231 f
232 }
233
234 #[derive(Debug)]
235 struct MockXsdBridge {
236 register_calls: AtomicUsize,
237 validate_calls: AtomicUsize,
238 register_error: Option<ValidatorError>,
239 validate_error: Option<ValidatorError>,
240 }
241
242 #[async_trait]
243 impl XsdBridge for MockXsdBridge {
244 async fn register(
245 &self,
246 _schema_id: &str,
247 _set: &crate::schema_set::SchemaSet,
248 ) -> Result<String, ValidatorError> {
249 self.register_calls.fetch_add(1, Ordering::SeqCst);
250 if let Some(err) = &self.register_error {
251 return Err(err.clone());
252 }
253 Ok("xsd-mock-id".to_string())
254 }
255
256 async fn validate(
257 &self,
258 _schema_id: &str,
259 _doc_bytes: Vec<u8>,
260 ) -> Result<(), ValidatorError> {
261 self.validate_calls.fetch_add(1, Ordering::SeqCst);
262 if let Some(err) = &self.validate_error {
263 return Err(err.clone());
264 }
265 Ok(())
266 }
267 }
268
269 #[test]
270 fn scheme_is_validator() {
271 assert_eq!(ValidatorComponent::new().scheme(), "validator");
272 }
273
274 #[test]
275 fn consumer_not_supported() {
276 let f = json_schema_file();
277 let uri = format!("validator:{}", f.path().display());
278 let ep = ValidatorComponent::new()
279 .create_endpoint(&uri, &NoOpComponentContext)
280 .unwrap();
281 assert!(ep.create_consumer(rt()).is_err());
282 }
283
284 #[test]
285 fn endpoint_creation_fails_for_missing_schema() {
286 let result = ValidatorComponent::new()
287 .create_endpoint("validator:/nonexistent/schema.json", &NoOpComponentContext);
288 assert!(result.is_err());
289 }
290
291 #[tokio::test]
292 async fn valid_json_body_passes_through() {
293 let f = json_schema_file();
294 let uri = format!("validator:{}", f.path().display());
295 let ep = ValidatorComponent::new()
296 .create_endpoint(&uri, &NoOpComponentContext)
297 .unwrap();
298 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
299 let exchange = Exchange::new(Message::new(camel_component_api::Body::Json(
300 serde_json::json!({"id": "1"}),
301 )));
302 let result = producer.oneshot(exchange).await;
303 assert!(result.is_ok());
304 }
305
306 #[tokio::test]
307 async fn invalid_json_body_returns_err() {
308 let f = json_schema_file();
309 let uri = format!("validator:{}", f.path().display());
310 let ep = ValidatorComponent::new()
311 .create_endpoint(&uri, &NoOpComponentContext)
312 .unwrap();
313 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
314 let exchange = Exchange::new(Message::new(camel_component_api::Body::Json(
315 serde_json::json!({"name": "x"}),
316 )));
317 let result = producer.oneshot(exchange).await;
318 assert!(result.is_err());
319 let msg = result.unwrap_err().to_string();
320 assert!(msg.contains("validation failed"), "got: {msg}");
321 }
322
323 #[tokio::test]
324 async fn valid_xml_body_passes() {
325 let backend = Arc::new(MockXsdBridge {
326 register_calls: AtomicUsize::new(0),
327 validate_calls: AtomicUsize::new(0),
328 register_error: None,
329 validate_error: None,
330 });
331 let f = xsd_file();
332 let uri = format!("validator:{}", f.path().display());
333 let ep = ValidatorComponent::with_xsd_bridge(backend)
334 .create_endpoint(&uri, &NoOpComponentContext)
335 .unwrap();
336 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
337 let exchange = Exchange::new(Message::new(camel_component_api::Body::Xml(
338 "<order>hello</order>".to_string(),
339 )));
340 assert!(producer.oneshot(exchange).await.is_ok());
341 }
342
343 #[tokio::test]
344 async fn xsd_bridge_register_and_validate_mock() {
345 let backend = Arc::new(MockXsdBridge {
346 register_calls: AtomicUsize::new(0),
347 validate_calls: AtomicUsize::new(0),
348 register_error: None,
349 validate_error: None,
350 });
351
352 let f = xsd_file();
353 let uri = format!("validator:{}", f.path().display());
354 let ep = ValidatorComponent::with_xsd_bridge(Arc::clone(&backend) as Arc<dyn XsdBridge>)
355 .create_endpoint(&uri, &NoOpComponentContext)
356 .unwrap();
357
358 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
359 let exchange = Exchange::new(Message::new(camel_component_api::Body::Xml(
360 "<order>ok</order>".to_string(),
361 )));
362 assert!(producer.oneshot(exchange).await.is_ok());
363 assert_eq!(backend.register_calls.load(Ordering::SeqCst), 1);
364 assert_eq!(backend.validate_calls.load(Ordering::SeqCst), 1);
365 }
366
367 #[tokio::test]
368 async fn xsd_bridge_register_error_propagates_on_validate() {
369 let backend = Arc::new(MockXsdBridge {
370 register_calls: AtomicUsize::new(0),
371 validate_calls: AtomicUsize::new(0),
372 register_error: Some(ValidatorError::CompilationFailed {
373 message: "COMPILATION_FAILED".to_string(),
374 source: None,
375 }),
376 validate_error: None,
377 });
378 let f = xsd_file();
379 let uri = format!("validator:{}", f.path().display());
380 let ep = ValidatorComponent::with_xsd_bridge(backend)
382 .create_endpoint(&uri, &NoOpComponentContext)
383 .expect("endpoint creation should succeed");
384 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
386 let exchange = Exchange::new(Message::new(camel_component_api::Body::Xml(
387 "<order/>".to_string(),
388 )));
389 let err = producer
390 .oneshot(exchange)
391 .await
392 .expect_err("expected validate to fail due to registration error");
393 assert!(err.to_string().contains("COMPILATION_FAILED"));
394 }
395
396 #[tokio::test]
397 async fn test_validator_rejects_oversized_payload() {
398 let f = json_schema_file();
400 let uri = format!("validator:{}?maxPayloadBytes=100", f.path().display());
401 let ep = ValidatorComponent::new()
402 .create_endpoint(&uri, &NoOpComponentContext)
403 .unwrap();
404 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
405 let big_body: String = "x".repeat(200);
407 let exchange = Exchange::new(Message::new(camel_component_api::Body::Text(big_body)));
408 let result = producer.oneshot(exchange).await;
409 assert!(result.is_err(), "expected oversized payload to be rejected");
410 let msg = result.unwrap_err().to_string();
411 assert!(
412 msg.contains("payload too large"),
413 "expected 'payload too large' in error, got: {msg}"
414 );
415 }
416
417 #[tokio::test]
418 async fn test_validator_allows_payload_under_limit() {
419 let f = json_schema_file();
420 let uri = format!("validator:{}?maxPayloadBytes=1024", f.path().display());
421 let ep = ValidatorComponent::new()
422 .create_endpoint(&uri, &NoOpComponentContext)
423 .unwrap();
424 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
425 let exchange = Exchange::new(Message::new(camel_component_api::Body::Json(
426 serde_json::json!({"id": "1"}),
427 )));
428 let result = producer.oneshot(exchange).await;
429 assert!(result.is_ok(), "expected valid payload under limit to pass");
430 }
431
432 #[tokio::test]
433 async fn test_validator_no_limit_allows_any_size() {
434 let f = json_schema_file();
435 let uri = format!("validator:{}", f.path().display());
436 let ep = ValidatorComponent::new()
437 .create_endpoint(&uri, &NoOpComponentContext)
438 .unwrap();
439 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
440 let exchange = Exchange::new(Message::new(camel_component_api::Body::Json(
442 serde_json::json!({"id": "this is a longer value that would exceed small limits"}),
443 )));
444 let result = producer.oneshot(exchange).await;
445 assert!(
446 result.is_ok(),
447 "expected no-limit validator to pass any valid payload"
448 );
449 }
450
451 #[tokio::test]
452 async fn test_fail_on_null_body_default_rejects_empty() {
453 let f = json_schema_file();
454 let uri = format!("validator:{}", f.path().display());
455 let ep = ValidatorComponent::new()
456 .create_endpoint(&uri, &NoOpComponentContext)
457 .unwrap();
458 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
459 let exchange = Exchange::new(Message::new(camel_component_api::Body::Empty));
460 let result = producer.oneshot(exchange).await;
461 assert!(result.is_err());
462 let msg = result.unwrap_err().to_string();
463 assert!(
464 msg.contains("failOnNullBody"),
465 "expected failOnNullBody in error, got: {msg}"
466 );
467 }
468
469 #[tokio::test]
470 async fn test_fail_on_null_body_false_passes_empty() {
471 let f = json_schema_file();
472 let uri = format!("validator:{}?failOnNullBody=false", f.path().display());
473 let ep = ValidatorComponent::new()
474 .create_endpoint(&uri, &NoOpComponentContext)
475 .unwrap();
476 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
477 let exchange = Exchange::new(Message::new(camel_component_api::Body::Empty));
478 let result = producer.oneshot(exchange).await;
479 assert!(
480 result.is_ok(),
481 "expected empty body to pass with failOnNullBody=false"
482 );
483 }
484
485 #[tokio::test]
486 async fn test_header_name_validation_uses_header_value() {
487 let f = json_schema_file();
488 let uri = format!("validator:{}?headerName=X-Data", f.path().display());
489 let ep = ValidatorComponent::new()
490 .create_endpoint(&uri, &NoOpComponentContext)
491 .unwrap();
492 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
493 let mut msg = Message::new(camel_component_api::Body::Empty);
494 msg.set_header("X-Data", serde_json::json!({"id": "1"}).to_string());
495 let exchange = Exchange::new(msg);
496 let result = producer.oneshot(exchange).await;
497 assert!(
499 result.is_ok(),
500 "expected valid header to pass: {:?}",
501 result
502 );
503 }
504
505 #[tokio::test]
506 async fn test_header_name_missing_header_fails_by_default() {
507 let f = json_schema_file();
508 let uri = format!("validator:{}?headerName=X-Missing", f.path().display());
509 let ep = ValidatorComponent::new()
510 .create_endpoint(&uri, &NoOpComponentContext)
511 .unwrap();
512 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
513 let exchange = Exchange::new(Message::new(camel_component_api::Body::Empty));
514 let result = producer.oneshot(exchange).await;
515 assert!(result.is_err());
516 let msg = result.unwrap_err().to_string();
517 assert!(
518 msg.contains("X-Missing"),
519 "expected header name in error, got: {msg}"
520 );
521 }
522
523 #[tokio::test]
524 async fn test_fail_on_null_header_false_passes_missing_header() {
525 let f = json_schema_file();
526 let uri = format!(
527 "validator:{}?headerName=X-Missing&failOnNullHeader=false",
528 f.path().display()
529 );
530 let ep = ValidatorComponent::new()
531 .create_endpoint(&uri, &NoOpComponentContext)
532 .unwrap();
533 let producer = ep.create_producer(rt(), &ProducerContext::new()).unwrap();
534 let exchange = Exchange::new(Message::new(camel_component_api::Body::Empty));
535 let result = producer.oneshot(exchange).await;
536 assert!(
537 result.is_ok(),
538 "expected missing header to pass with failOnNullHeader=false"
539 );
540 }
541
542 #[test]
543 fn test_relaxng_schema_type_rejected_at_creation() {
544 let mut f = tempfile::Builder::new().suffix(".rng").tempfile().unwrap();
545 use std::io::Write;
546 f.write_all(b"<grammar/>").unwrap();
547 let uri = format!("validator:{}", f.path().display());
548 let result = ValidatorComponent::new().create_endpoint(&uri, &NoOpComponentContext);
549 let err = result.err().expect("expected endpoint creation to fail");
550 let msg = err.to_string();
551 assert!(
552 msg.contains("not yet supported"),
553 "expected 'not yet supported' in error, got: {msg}"
554 );
555 }
556
557 #[test]
558 fn test_schematron_schema_type_rejected_at_creation() {
559 let mut f = tempfile::Builder::new().suffix(".sch").tempfile().unwrap();
560 use std::io::Write;
561 f.write_all(b"<schema/>").unwrap();
562 let uri = format!("validator:{}", f.path().display());
563 let result = ValidatorComponent::new().create_endpoint(&uri, &NoOpComponentContext);
564 let err = result.err().expect("expected endpoint creation to fail");
565 let msg = err.to_string();
566 assert!(
567 msg.contains("not yet supported"),
568 "expected 'not yet supported' in error, got: {msg}"
569 );
570 }
571
572 #[test]
575 fn xsd_mock_compiles_against_new_trait_signature() {
576 fn assert_xsd_bridge<T: XsdBridge>() {}
577 assert_xsd_bridge::<MockXsdBridge>();
578 }
579
580 #[test]
581 fn create_endpoint_rejects_malformed_alias_uri() {
582 let f = xsd_file();
586 let uri = format!("validator:{}?schemaAliases=%7Bbad", f.path().display());
587 let err = ValidatorComponent::new()
588 .create_endpoint(&uri, &NoOpComponentContext)
589 .err()
590 .expect("malformed schemaAliases must reject endpoint creation");
591 match err {
592 CamelError::InvalidUri(m) => assert!(
593 m.contains("schemaAliases"),
594 "error must name the schemaAliases option, got: {m}"
595 ),
596 other => panic!("expected InvalidUri, got: {other}"),
597 }
598 }
599
600 #[test]
601 fn create_endpoint_rejects_alias_cap() {
602 let over_cap = crate::schema_set::MAX_SCHEMA_ALIASES + 1;
603
604 let mut obj = serde_json::Map::new();
607 for i in 0..over_cap {
608 obj.insert(
609 format!("urn:a{i}"),
610 serde_json::Value::String("a/parent.xsd".to_string()),
611 );
612 }
613 let json = serde_json::Value::Object(obj).to_string();
614 let encoded =
615 percent_encoding::utf8_percent_encode(&json, percent_encoding::NON_ALPHANUMERIC)
616 .to_string();
617 let uri = format!("validator:/nonexistent/root.xsd?schemaAliases={encoded}");
618 let err = ValidatorComponent::new()
619 .create_endpoint(&uri, &NoOpComponentContext)
620 .err()
621 .expect("an over-cap schemaAliases URI must reject endpoint creation");
622 match err {
623 CamelError::InvalidUri(m) => assert!(
624 m.contains("128"),
625 "parse error must name the 128-entry cap, got: {m}"
626 ),
627 other => panic!("expected InvalidUri for the URI surface, got: {other}"),
628 }
629
630 let mut aliases = std::collections::BTreeMap::new();
637 for i in 0..over_cap {
638 aliases.insert(format!("urn:a{i}"), "a/parent.xsd".to_string());
639 }
640 let cfg = ValidatorConfig {
641 schema_path: std::path::PathBuf::from("/nonexistent/root.xsd"),
642 schema_type: crate::config::SchemaType::Xml,
643 max_payload_bytes: None,
644 schema_cache_max_entries: crate::config::DEFAULT_SCHEMA_CACHE_MAX_ENTRIES,
645 fail_on_null_body: true,
646 header_name: None,
647 fail_on_null_header: true,
648 schema_aliases: aliases,
649 };
650 let backend = Arc::new(MockXsdBridge {
651 register_calls: AtomicUsize::new(0),
652 validate_calls: AtomicUsize::new(0),
653 register_error: None,
654 validate_error: None,
655 });
656 let err = CompiledValidator::compile(&cfg, backend)
657 .expect_err("an over-cap programmatic config must fail to compile");
658 match err {
659 CamelError::Config(m) => assert!(
660 m.contains("128"),
661 "config guard must name the 128-entry cap, got: {m}"
662 ),
663 other => panic!(
664 "expected the config-level cap guard (CamelError::Config); the \
665 guard is unwired and this surfaced as: {other}"
666 ),
667 }
668 }
669
670 #[test]
671 fn create_endpoint_accepts_alias_uri() {
672 let dir = tempfile::tempdir().unwrap();
673 std::fs::write(
674 dir.path().join("root.xsd"),
675 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>"#,
676 )
677 .unwrap();
678 std::fs::create_dir_all(dir.path().join("a")).unwrap();
679 std::fs::write(
680 dir.path().join("a/parent.xsd"),
681 br#"<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>"#,
682 )
683 .unwrap();
684
685 let uri = format!(
686 "validator:{}/root.xsd?schemaAliases=%7B%22urn%3Aalias%22%3A%22a%2Fparent.xsd%22%7D",
687 dir.path().display()
688 );
689 let result = ValidatorComponent::new().create_endpoint(&uri, &NoOpComponentContext);
690 assert!(
691 result.is_ok(),
692 "a valid root with the aliased schema present must create an endpoint: {:?}",
693 result.err().map(|e| e.to_string())
694 );
695 }
696
697 #[test]
698 fn test_schema_info_returns_description() {
699 let f = json_schema_file();
700 let uri = format!("validator:{}", f.path().display());
701 let ep = ValidatorComponent::new()
702 .create_endpoint(&uri, &NoOpComponentContext)
703 .unwrap();
704 assert_eq!(ep.uri(), uri);
707 }
708}