1use referencing::{Retrieve, Uri};
3use serde_json::Value;
4
5#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
6use crate::HttpOptions;
7
8#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
10#[derive(Debug)]
11pub enum HttpRetrieverError {
12 CertificateRead {
14 path: std::path::PathBuf,
15 source: std::io::Error,
16 },
17 CertificateParse {
19 path: std::path::PathBuf,
20 source: reqwest::Error,
21 },
22 ClientBuild(reqwest::Error),
24}
25
26#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
27impl std::fmt::Display for HttpRetrieverError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::CertificateRead { path, source } => {
31 write!(
32 f,
33 "Failed to read certificate file '{}': {source}",
34 path.display()
35 )
36 }
37 Self::CertificateParse { path, source } => {
38 write!(
39 f,
40 "Failed to parse certificate '{}': {source}",
41 path.display()
42 )
43 }
44 Self::ClientBuild(e) => write!(f, "Failed to build HTTP client: {e}"),
45 }
46 }
47}
48
49#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
50impl std::error::Error for HttpRetrieverError {
51 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52 match self {
53 Self::CertificateRead { source, .. } => Some(source),
54 Self::CertificateParse { source, .. } | Self::ClientBuild(source) => Some(source),
55 }
56 }
57}
58
59#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
61fn load_certificate(path: &std::path::Path) -> Result<reqwest::Certificate, HttpRetrieverError> {
62 let cert_data = std::fs::read(path).map_err(|e| HttpRetrieverError::CertificateRead {
63 path: path.to_path_buf(),
64 source: e,
65 })?;
66 reqwest::Certificate::from_pem(&cert_data).map_err(|e| HttpRetrieverError::CertificateParse {
67 path: path.to_path_buf(),
68 source: e,
69 })
70}
71
72#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
75macro_rules! configure_http_client {
76 ($builder:expr, $options:expr) => {{
77 let mut builder = $builder;
78 if let Some(connect_timeout) = $options.connect_timeout {
79 builder = builder.connect_timeout(connect_timeout);
80 }
81 if let Some(timeout) = $options.timeout {
82 builder = builder.timeout(timeout);
83 }
84 if $options.danger_accept_invalid_certs {
85 builder = builder.danger_accept_invalid_certs(true);
86 }
87 if let Some(ref cert_path) = &$options.root_certificate {
88 builder = builder.add_root_certificate(load_certificate(cert_path)?);
89 }
90 builder
91 }};
92}
93
94#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
95fn install_tls_provider() {
96 #[cfg(feature = "tls-aws-lc-rs")]
99 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
100 #[cfg(all(not(feature = "tls-aws-lc-rs"), feature = "tls-ring"))]
101 let _ = rustls::crypto::ring::default_provider().install_default();
102}
103
104pub(crate) struct DefaultRetriever;
105
106#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
124#[derive(Debug)]
125pub struct HttpRetriever {
126 client: reqwest::blocking::Client,
127}
128
129#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
130impl HttpRetriever {
131 pub fn new(options: &HttpOptions) -> Result<Self, HttpRetrieverError> {
140 install_tls_provider();
141
142 let builder = configure_http_client!(reqwest::blocking::Client::builder(), options);
143 Ok(Self {
144 client: builder.build().map_err(HttpRetrieverError::ClientBuild)?,
145 })
146 }
147}
148
149#[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
150impl Retrieve for HttpRetriever {
151 fn retrieve(
152 &self,
153 uri: &Uri<String>,
154 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
155 match uri.scheme().as_str() {
156 "http" | "https" => Ok(self.client.get(uri.as_str()).send()?.json()?),
157 "file" => {
158 #[cfg(feature = "resolve-file")]
159 {
160 let path = uri.path().as_str();
161 let path = {
162 #[cfg(windows)]
163 {
164 let path = path.trim_start_matches('/').replace('/', "\\");
165 std::path::PathBuf::from(path)
166 }
167 #[cfg(not(windows))]
168 {
169 std::path::PathBuf::from(path)
170 }
171 };
172 let file = std::fs::File::open(path)?;
173 Ok(serde_json::from_reader(file)?)
174 }
175 #[cfg(not(feature = "resolve-file"))]
176 {
177 Err("`resolve-file` feature or a custom resolver is required to resolve external schemas via files".into())
178 }
179 }
180 scheme => Err(format!("Unknown scheme {scheme}").into()),
181 }
182 }
183}
184
185#[cfg(all(
190 feature = "resolve-http",
191 feature = "resolve-async",
192 not(target_arch = "wasm32")
193))]
194#[derive(Debug)]
195pub struct AsyncHttpRetriever {
196 client: reqwest::Client,
197}
198
199#[cfg(all(
200 feature = "resolve-http",
201 feature = "resolve-async",
202 not(target_arch = "wasm32")
203))]
204impl AsyncHttpRetriever {
205 pub fn new(options: &HttpOptions) -> Result<Self, HttpRetrieverError> {
214 install_tls_provider();
215
216 let builder = configure_http_client!(reqwest::Client::builder(), options);
217 Ok(Self {
218 client: builder.build().map_err(HttpRetrieverError::ClientBuild)?,
219 })
220 }
221}
222
223#[cfg(all(
224 feature = "resolve-http",
225 feature = "resolve-async",
226 not(target_arch = "wasm32")
227))]
228#[async_trait::async_trait]
229impl referencing::AsyncRetrieve for AsyncHttpRetriever {
230 async fn retrieve(
231 &self,
232 uri: &Uri<String>,
233 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
234 match uri.scheme().as_str() {
235 "http" | "https" => Ok(self.client.get(uri.as_str()).send().await?.json().await?),
236 "file" => {
237 #[cfg(feature = "resolve-file")]
238 {
239 let path = uri.path().as_str().to_string();
240 let contents = tokio::task::spawn_blocking(
241 move || -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
242 let path = {
243 #[cfg(windows)]
244 {
245 let path = path.trim_start_matches('/').replace('/', "\\");
246 std::path::PathBuf::from(path)
247 }
248 #[cfg(not(windows))]
249 {
250 std::path::PathBuf::from(path)
251 }
252 };
253 let file = std::fs::File::open(path)?;
254 Ok(serde_json::from_reader(file)?)
255 },
256 )
257 .await??;
258 Ok(contents)
259 }
260 #[cfg(not(feature = "resolve-file"))]
261 {
262 Err("`resolve-file` feature or a custom resolver is required to resolve external schemas via files".into())
263 }
264 }
265 scheme => Err(format!("Unknown scheme {scheme}").into()),
266 }
267 }
268}
269
270impl Retrieve for DefaultRetriever {
271 #[allow(unused)]
272 fn retrieve(
273 &self,
274 uri: &Uri<String>,
275 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
276 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
277 {
278 Err("External references are not supported on wasm32-unknown-unknown".into())
279 }
280 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
281 match uri.scheme().as_str() {
282 "http" | "https" => {
283 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
284 {
285 install_tls_provider();
286
287 Ok(reqwest::blocking::get(uri.as_str())?.json()?)
288 }
289 #[cfg(all(feature = "resolve-http", target_arch = "wasm32"))]
290 {
291 Err("Synchronous HTTP retrieval is not supported on wasm32 targets. Use async_validator_for with the resolve-async feature instead".into())
292 }
293 #[cfg(not(feature = "resolve-http"))]
294 {
295 Err("`resolve-http` feature or a custom resolver is required to resolve external schemas via HTTP".into())
296 }
297 }
298 "file" => {
299 #[cfg(feature = "resolve-file")]
300 {
301 let path = uri.path().as_str();
302 let path = {
303 #[cfg(windows)]
304 {
305 let path = path.trim_start_matches('/').replace('/', "\\");
307 std::path::PathBuf::from(path)
308 }
309 #[cfg(not(windows))]
310 {
311 std::path::PathBuf::from(path)
312 }
313 };
314 let file = std::fs::File::open(path)?;
315 Ok(serde_json::from_reader(file)?)
316 }
317 #[cfg(not(feature = "resolve-file"))]
318 {
319 Err("`resolve-file` feature or a custom resolver is required to resolve external schemas via files".into())
320 }
321 }
322 scheme => Err(format!("Unknown scheme {scheme}").into()),
323 }
324 }
325}
326
327#[cfg(feature = "resolve-async")]
328#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
329#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
330impl referencing::AsyncRetrieve for DefaultRetriever {
331 async fn retrieve(
332 &self,
333 uri: &Uri<String>,
334 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
335 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
336 {
337 Err("External references are not supported on wasm32-unknown-unknown".into())
338 }
339 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
340 match uri.scheme().as_str() {
341 "http" | "https" => {
342 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
343 {
344 install_tls_provider();
345
346 Ok(reqwest::get(uri.as_str()).await?.json().await?)
347 }
348 #[cfg(all(feature = "resolve-http", target_arch = "wasm32"))]
349 {
350 Ok(reqwest::get(uri.as_str()).await?.json().await?)
351 }
352 #[cfg(not(feature = "resolve-http"))]
353 Err("`resolve-http` feature or a custom resolver is required to resolve external schemas via HTTP".into())
354 }
355 "file" => {
356 #[cfg(feature = "resolve-file")]
357 {
358 let path = uri.path().as_str().to_string();
360 let contents = tokio::task::spawn_blocking(
361 move || -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
362 let path = {
363 #[cfg(windows)]
364 {
365 let path = path.trim_start_matches('/').replace('/', "\\");
366 std::path::PathBuf::from(path)
367 }
368 #[cfg(not(windows))]
369 {
370 std::path::PathBuf::from(path)
371 }
372 };
373 let file = std::fs::File::open(path)?;
374 Ok(serde_json::from_reader(file)?)
375 },
376 )
377 .await??;
378 Ok(contents)
379 }
380 #[cfg(not(feature = "resolve-file"))]
381 {
382 Err("`resolve-file` feature or a custom resolver is required to resolve external schemas via files".into())
383 }
384 }
385 scheme => Err(format!("Unknown scheme {scheme}").into()),
386 }
387 }
388}
389
390#[cfg(all(test, not(target_arch = "wasm32")))]
391use percent_encoding::{AsciiSet, CONTROLS};
392
393#[cfg(all(test, not(target_arch = "wasm32")))]
394const URI_SEGMENT: &AsciiSet = &CONTROLS
395 .add(b' ')
396 .add(b'"')
397 .add(b'<')
398 .add(b'>')
399 .add(b'`')
400 .add(b'#')
401 .add(b'?')
402 .add(b'{')
403 .add(b'}')
404 .add(b'/')
405 .add(b'%');
406
407#[cfg(all(test, not(target_arch = "wasm32"), not(target_os = "windows")))]
408const UNIX_URI_SEGMENT: &AsciiSet = &URI_SEGMENT.add(b'\\');
409
410#[cfg(all(test, not(target_arch = "wasm32")))]
411pub(crate) fn path_to_uri(path: &std::path::Path) -> String {
412 use percent_encoding::percent_encode;
413
414 let mut result = "file://".to_owned();
415
416 #[cfg(not(target_os = "windows"))]
417 {
418 use std::os::unix::ffi::OsStrExt;
419
420 for component in path.components().skip(1) {
421 result.push('/');
422 result.extend(percent_encode(
423 component.as_os_str().as_bytes(),
424 UNIX_URI_SEGMENT,
425 ));
426 }
427 }
428 #[cfg(target_os = "windows")]
429 {
430 use std::path::{Component, Prefix};
431 let mut components = path.components();
432
433 match components.next() {
434 Some(Component::Prefix(ref p)) => match p.kind() {
435 Prefix::Disk(letter) | Prefix::VerbatimDisk(letter) => {
436 result.push('/');
437 result.push(letter as char);
438 result.push(':');
439 }
440 _ => panic!("Unexpected path"),
441 },
442 _ => panic!("Unexpected path"),
443 }
444
445 for component in components {
446 if component == Component::RootDir {
447 continue;
448 }
449
450 let component = component.as_os_str().to_str().expect("Unexpected path");
451
452 result.push('/');
453 result.extend(percent_encode(component.as_bytes(), URI_SEGMENT));
454 }
455 }
456 result
457}
458
459#[cfg(test)]
460mod tests {
461 #[cfg(not(target_arch = "wasm32"))]
462 use super::path_to_uri;
463 #[cfg(all(
464 feature = "resolve-http",
465 feature = "resolve-async",
466 not(target_arch = "wasm32")
467 ))]
468 use crate::AsyncHttpRetriever;
469 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
470 use crate::{HttpOptions, HttpRetriever, HttpRetrieverError};
471 use serde_json::json;
472 #[cfg(not(target_arch = "wasm32"))]
473 use std::io::Write;
474
475 #[test]
476 #[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
477 fn test_retrieve_from_file() {
478 let mut temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
479 let external_schema = json!({
480 "type": "object",
481 "properties": {
482 "name": { "type": "string" }
483 },
484 "required": ["name"]
485 });
486 write!(temp_file, "{external_schema}").expect("Failed to write to temp file");
487
488 let uri = path_to_uri(temp_file.path());
489
490 let schema = json!({
491 "type": "object",
492 "properties": {
493 "user": { "$ref": uri }
494 }
495 });
496
497 let validator = crate::validator_for(&schema).expect("Schema compilation failed");
498
499 let valid = json!({"user": {"name": "John Doe"}});
500 assert!(validator.is_valid(&valid));
501
502 let invalid = json!({"user": {}});
503 assert!(!validator.is_valid(&invalid));
504 }
505
506 #[test]
507 fn test_unknown_scheme() {
508 let schema = json!({
509 "type": "object",
510 "properties": {
511 "test": { "$ref": "unknown-schema://test" }
512 }
513 });
514
515 let result = crate::validator_for(&schema);
516
517 assert!(result.is_err());
518 let error = result.unwrap_err().to_string();
519 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
520 assert!(error.contains("Unknown scheme"));
521 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
522 assert!(error.contains("External references are not supported on wasm32-unknown-unknown"));
523 }
524
525 #[cfg(not(target_arch = "wasm32"))]
526 fn create_temp_file(dir: &tempfile::TempDir, name: &str, content: &str) -> String {
527 let file_path = dir.path().join(name);
528 std::fs::write(&file_path, content).unwrap();
529 file_path.to_str().unwrap().to_string()
530 }
531
532 #[test]
533 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
534 fn test_http_retriever_with_default_options() {
535 let options = HttpOptions::new();
536 let retriever = HttpRetriever::new(&options);
537 assert!(retriever.is_ok());
538 }
539
540 #[test]
541 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
542 fn test_http_retriever_nonexistent_cert() {
543 let options = HttpOptions::new().add_root_certificate("/nonexistent/cert.pem");
544 let result = HttpRetriever::new(&options);
545 assert!(result.is_err());
546 let err = result.unwrap_err();
547 assert!(matches!(err, HttpRetrieverError::CertificateRead { .. }));
548 assert!(err.to_string().contains("/nonexistent/cert.pem"));
549 }
550
551 #[test]
552 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
553 fn test_http_retriever_error_source() {
554 use std::error::Error;
555
556 let options = HttpOptions::new().add_root_certificate("/nonexistent/cert.pem");
557 let err = HttpRetriever::new(&options).unwrap_err();
558 assert!(err.source().is_some());
559 }
560
561 #[test]
562 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
563 fn test_http_retriever_with_valid_certificate() {
564 let cert_path =
567 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/test_cert.pem");
568 let options = HttpOptions::new().add_root_certificate(&cert_path);
569 let retriever = HttpRetriever::new(&options);
570 assert!(retriever.is_ok(), "Failed: {:?}", retriever.err());
571 }
572
573 #[test]
574 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
575 fn test_with_http_options_default() {
576 let http_options = HttpOptions::new();
577 let schema = json!({"type": "string"});
578 let result = crate::options().with_http_options(&http_options);
579 assert!(result.is_ok());
580 let validator = result.unwrap().build(&schema);
581 assert!(validator.is_ok());
582 assert!(validator.unwrap().is_valid(&json!("test")));
583 }
584
585 #[test]
586 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
587 fn test_with_http_options_with_timeouts() {
588 use std::time::Duration;
589
590 let http_options = HttpOptions::new()
591 .connect_timeout(Duration::from_secs(10))
592 .timeout(Duration::from_secs(30));
593 let schema = json!({"type": "integer"});
594 let result = crate::options().with_http_options(&http_options);
595 assert!(result.is_ok());
596 let validator = result.unwrap().build(&schema).unwrap();
597 assert!(validator.is_valid(&json!(42)));
598 assert!(!validator.is_valid(&json!("not an integer")));
599 }
600
601 #[test]
602 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
603 fn test_with_http_options_invalid_cert() {
604 let http_options = HttpOptions::new().add_root_certificate("/nonexistent/cert.pem");
605 let result = crate::options().with_http_options(&http_options);
606 assert!(result.is_err());
607 let err = result.unwrap_err();
608 assert!(err.to_string().contains("/nonexistent/cert.pem"));
609 }
610
611 #[test]
612 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
613 fn test_with_http_options_danger_accept_invalid_certs() {
614 let http_options = HttpOptions::new().danger_accept_invalid_certs(true);
615 let schema = json!({"type": "boolean"});
616 let result = crate::options().with_http_options(&http_options);
617 assert!(result.is_ok());
618 let validator = result.unwrap().build(&schema).unwrap();
619 assert!(validator.is_valid(&json!(true)));
620 }
621
622 #[test]
623 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
624 fn test_http_retriever_retrieve_trait() {
625 use referencing::Retrieve;
626 use std::time::Duration;
627
628 let options = HttpOptions::new().timeout(Duration::from_secs(30));
629 let retriever = HttpRetriever::new(&options).unwrap();
630 let uri =
631 referencing::uri::from_str("https://json-schema.org/draft/2020-12/schema").unwrap();
632 let result = retriever.retrieve(&uri);
633 assert!(result.is_ok());
634 let schema = result.unwrap();
635 assert!(schema.is_object());
637 assert!(schema.get("$schema").is_some());
638 }
639
640 #[test]
641 #[cfg(all(
642 feature = "resolve-http",
643 feature = "resolve-file",
644 not(target_arch = "wasm32")
645 ))]
646 fn test_http_retriever_file_scheme() {
647 use referencing::Retrieve;
648
649 let dir = tempfile::tempdir().unwrap();
650 let schema_content = r#"{"type": "string"}"#;
651 let schema_path = dir.path().join("schema.json");
652 std::fs::write(&schema_path, schema_content).unwrap();
653
654 let options = HttpOptions::new();
655 let retriever = HttpRetriever::new(&options).unwrap();
656 let uri = referencing::uri::from_str(&path_to_uri(&schema_path)).unwrap();
657 let result = retriever.retrieve(&uri);
658 assert!(result.is_ok());
659 let schema = result.unwrap();
660 assert_eq!(schema, json!({"type": "string"}));
661 }
662
663 #[test]
664 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
665 fn test_http_retriever_unknown_scheme() {
666 use referencing::Retrieve;
667
668 let options = HttpOptions::new();
669 let retriever = HttpRetriever::new(&options).unwrap();
670 let uri = referencing::uri::from_str("ftp://example.com/schema.json").unwrap();
671 let result = retriever.retrieve(&uri);
672 assert!(result.is_err());
673 assert!(result.unwrap_err().to_string().contains("Unknown scheme"));
674 }
675
676 #[test]
677 #[cfg(all(feature = "resolve-http", not(target_arch = "wasm32")))]
678 fn test_http_retriever_error_invalid_certificate() {
679 use std::io::Write;
680
681 let mut temp = tempfile::NamedTempFile::new().unwrap();
682 temp.write_all(b"-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----")
683 .unwrap();
684 temp.flush().unwrap();
685
686 let options = HttpOptions::new().add_root_certificate(temp.path());
687 let result = HttpRetriever::new(&options);
688 assert!(result.is_err());
689 let err = result.unwrap_err();
690 assert!(matches!(err, HttpRetrieverError::ClientBuild(_)));
691 assert!(err.to_string().contains("Failed to build HTTP client"));
692 }
693
694 #[tokio::test]
695 #[cfg(all(
696 feature = "resolve-http",
697 feature = "resolve-async",
698 not(target_arch = "wasm32")
699 ))]
700 async fn test_async_http_retriever_retrieve_trait() {
701 use referencing::AsyncRetrieve;
702 use serde_json::Value;
703 use std::time::Duration;
704
705 let options = HttpOptions::new().timeout(Duration::from_secs(30));
706 let retriever = AsyncHttpRetriever::new(&options).unwrap();
707 let uri =
708 referencing::uri::from_str("https://json-schema.org/draft/2020-12/schema").unwrap();
709 let result: Result<Value, _> = retriever.retrieve(&uri).await;
710 assert!(result.is_ok());
711 let schema = result.unwrap();
712 assert!(schema.is_object());
714 assert!(schema.get("$schema").is_some());
715 }
716
717 #[tokio::test]
718 #[cfg(all(
719 feature = "resolve-http",
720 feature = "resolve-async",
721 feature = "resolve-file",
722 not(target_arch = "wasm32")
723 ))]
724 async fn test_async_http_retriever_file_scheme() {
725 use referencing::AsyncRetrieve;
726 use serde_json::Value;
727
728 let dir = tempfile::tempdir().unwrap();
729 let schema_content = r#"{"type": "integer"}"#;
730 let schema_path = dir.path().join("schema.json");
731 std::fs::write(&schema_path, schema_content).unwrap();
732
733 let options = HttpOptions::new();
734 let retriever = AsyncHttpRetriever::new(&options).unwrap();
735 let uri = referencing::uri::from_str(&path_to_uri(&schema_path)).unwrap();
736 let result: Result<Value, _> = retriever.retrieve(&uri).await;
737 assert!(result.is_ok());
738 let schema = result.unwrap();
739 assert_eq!(schema, json!({"type": "integer"}));
740 }
741
742 #[tokio::test]
743 #[cfg(all(
744 feature = "resolve-http",
745 feature = "resolve-async",
746 not(target_arch = "wasm32")
747 ))]
748 async fn test_async_http_retriever_unknown_scheme() {
749 use referencing::AsyncRetrieve;
750 use serde_json::Value;
751
752 let options = HttpOptions::new();
753 let retriever = AsyncHttpRetriever::new(&options).unwrap();
754 let uri = referencing::uri::from_str("ftp://example.com/schema.json").unwrap();
755 let result: Result<Value, _> = retriever.retrieve(&uri).await;
756 assert!(result.is_err());
757 assert!(result.unwrap_err().to_string().contains("Unknown scheme"));
758 }
759
760 #[test]
761 #[cfg(all(not(target_arch = "wasm32"), feature = "resolve-file"))]
762 fn test_with_base_uri_resolution() {
763 let dir = tempfile::tempdir().unwrap();
764
765 let b_schema = r#"
766 {
767 "type": "object",
768 "properties": {
769 "age": { "type": "number" }
770 },
771 "required": ["age"]
772 }
773 "#;
774 let _b_path = create_temp_file(&dir, "b.json", b_schema);
775
776 let a_schema = r#"
777 {
778 "$schema": "https://json-schema.org/draft/2020-12/schema",
779 "$ref": "./b.json",
780 "type": "object"
781 }
782 "#;
783 let a_path = create_temp_file(&dir, "a.json", a_schema);
784
785 let valid_instance = serde_json::json!({ "age": 30 });
786
787 let schema_str = std::fs::read_to_string(&a_path).unwrap();
788 let schema_json: serde_json::Value = serde_json::from_str(&schema_str).unwrap();
789
790 let base_uri = path_to_uri(dir.path());
791 let validator = crate::options()
792 .with_base_uri(format!("{base_uri}/"))
793 .build(&schema_json)
794 .expect("Schema compilation failed");
795
796 assert!(validator.is_valid(&valid_instance));
797
798 let invalid_instance = serde_json::json!({ "age": "thirty" });
799 assert!(!validator.is_valid(&invalid_instance));
800 }
801}
802
803#[cfg(all(test, feature = "resolve-async", not(target_arch = "wasm32")))]
804mod async_tests {
805 use super::*;
806 use crate::Registry;
807 use serde_json::json;
808 use std::io::Write;
809
810 #[tokio::test]
811 #[cfg(feature = "resolve-file")]
812 async fn test_async_retrieve_from_file() {
813 let mut temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
814 let external_schema = json!({
815 "type": "object",
816 "properties": {
817 "name": { "type": "string" }
818 },
819 "required": ["name"]
820 });
821 write!(temp_file, "{external_schema}").expect("Failed to write to temp file");
822
823 let uri = path_to_uri(temp_file.path());
824
825 let schema = json!({
826 "type": "object",
827 "properties": {
828 "user": { "$ref": uri }
829 }
830 });
831
832 let validator = crate::async_options()
833 .with_base_uri("http://example.com/schema")
834 .with_retriever(DefaultRetriever)
835 .build(&schema)
836 .await
837 .expect("Invalid schema");
838
839 let valid = json!({"user": {"name": "John Doe"}});
840 assert!(validator.is_valid(&valid));
841
842 let invalid = json!({"user": {}});
843 assert!(!validator.is_valid(&invalid));
844 }
845
846 #[tokio::test]
847 async fn test_async_unknown_scheme() {
848 let schema = json!({
849 "type": "object",
850 "properties": {
851 "test": { "$ref": "unknown-schema://test" }
852 }
853 });
854
855 let result = Registry::new()
856 .async_retriever(DefaultRetriever)
857 .add("http://example.com/schema", schema)
858 .expect("Resource should be accepted")
859 .async_prepare()
860 .await;
861
862 assert!(result.is_err());
863 let error = result.unwrap_err().to_string();
864 assert!(error.contains("Unknown scheme"));
865 }
866
867 #[tokio::test]
868 #[cfg(feature = "resolve-file")]
869 async fn test_async_concurrent_retrievals() {
870 let mut temp_files = vec![];
871 let mut uris = vec![];
872
873 for i in 0..3 {
875 let mut temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file");
876 let schema = json!({
877 "type": "object",
878 "properties": {
879 "field": { "type": "string", "minLength": i }
880 }
881 });
882 write!(temp_file, "{schema}").expect("Failed to write to temp file");
883 uris.push(path_to_uri(temp_file.path()));
884 temp_files.push(temp_file);
885 }
886
887 let schema = json!({
889 "type": "object",
890 "properties": {
891 "obj1": { "$ref": uris[0] },
892 "obj2": { "$ref": uris[1] },
893 "obj3": { "$ref": uris[2] }
894 }
895 });
896
897 let validator = crate::async_options()
898 .with_base_uri("http://example.com/schema")
899 .with_retriever(DefaultRetriever)
900 .build(&schema)
901 .await
902 .expect("Invalid schema");
903
904 let valid = json!({
905 "obj1": { "field": "" }, "obj2": { "field": "a" }, "obj3": { "field": "ab" } });
909 assert!(validator.is_valid(&valid));
910
911 let invalid = json!({
913 "obj1": { "field": "" },
914 "obj2": { "field": "" }, "obj3": { "field": "a" } });
917 assert!(!validator.is_valid(&invalid));
918 }
919}