referencing/
registry.rs

1use std::{
2    collections::{hash_map::Entry, HashSet, VecDeque},
3    hash::{Hash, Hasher},
4    pin::Pin,
5    sync::Arc,
6};
7
8use ahash::{AHashMap, AHashSet, AHasher};
9use fluent_uri::Uri;
10use once_cell::sync::Lazy;
11use serde_json::Value;
12
13use crate::{
14    anchors::{AnchorKey, AnchorKeyRef},
15    cache::{SharedUriCache, UriCache},
16    hasher::BuildNoHashHasher,
17    list::List,
18    meta::{self, metas_for_draft},
19    resource::{unescape_segment, InnerResourcePtr, JsonSchemaResource},
20    uri,
21    vocabularies::{self, VocabularySet},
22    Anchor, DefaultRetriever, Draft, Error, Resolver, Resource, ResourceRef, Retrieve,
23};
24
25/// An owned-or-refstatic wrapper for JSON `Value`.
26#[derive(Debug)]
27pub(crate) enum ValueWrapper {
28    Owned(Value),
29    StaticRef(&'static Value),
30}
31
32impl AsRef<Value> for ValueWrapper {
33    fn as_ref(&self) -> &Value {
34        match self {
35            ValueWrapper::Owned(value) => value,
36            ValueWrapper::StaticRef(value) => value,
37        }
38    }
39}
40
41// SAFETY: `Pin` guarantees stable memory locations for resource pointers,
42// while `Arc` enables cheap sharing between multiple registries
43type DocumentStore = AHashMap<Arc<Uri<String>>, Pin<Arc<ValueWrapper>>>;
44type ResourceMap = AHashMap<Arc<Uri<String>>, InnerResourcePtr>;
45
46/// Pre-loaded registry containing all JSON Schema meta-schemas and their vocabularies
47pub static SPECIFICATIONS: Lazy<Registry> =
48    Lazy::new(|| Registry::build_from_meta_schemas(meta::META_SCHEMAS_ALL.as_slice()));
49
50/// A registry of JSON Schema resources, each identified by their canonical URIs.
51///
52/// Registries store a collection of in-memory resources and their anchors.
53/// They eagerly process all added resources, including their subresources and anchors.
54/// This means that subresources contained within any added resources are immediately
55/// discoverable and retrievable via their own IDs.
56///
57/// # Resource Retrieval
58///
59/// Registry supports both blocking and non-blocking retrieval of external resources.
60///
61/// ## Blocking Retrieval
62///
63/// ```rust
64/// use referencing::{Registry, Resource, Retrieve, Uri};
65/// use serde_json::{json, Value};
66///
67/// struct ExampleRetriever;
68///
69/// impl Retrieve for ExampleRetriever {
70///     fn retrieve(
71///         &self,
72///         uri: &Uri<String>
73///     ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
74///         // Always return the same value for brevity
75///         Ok(json!({"type": "string"}))
76///     }
77/// }
78///
79/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
80/// let registry = Registry::options()
81///     .retriever(ExampleRetriever)
82///     .build([
83///         // Initial schema that might reference external schemas
84///         (
85///             "https://example.com/user.json",
86///             Resource::from_contents(json!({
87///                 "type": "object",
88///                 "properties": {
89///                     // Should be retrieved by `ExampleRetriever`
90///                     "role": {"$ref": "https://example.com/role.json"}
91///                 }
92///             }))?
93///         )
94///     ])?;
95/// # Ok(())
96/// # }
97/// ```
98///
99/// ## Non-blocking Retrieval
100///
101/// ```rust
102/// # #[cfg(feature = "retrieve-async")]
103/// # mod example {
104/// use referencing::{Registry, Resource, AsyncRetrieve, Uri};
105/// use serde_json::{json, Value};
106///
107/// struct ExampleRetriever;
108///
109/// #[async_trait::async_trait]
110/// impl AsyncRetrieve for ExampleRetriever {
111///     async fn retrieve(
112///         &self,
113///         uri: &Uri<String>
114///     ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
115///         // Always return the same value for brevity
116///         Ok(json!({"type": "string"}))
117///     }
118/// }
119///
120///  # async fn example() -> Result<(), Box<dyn std::error::Error>> {
121/// let registry = Registry::options()
122///     .async_retriever(ExampleRetriever)
123///     .build([
124///         (
125///             "https://example.com/user.json",
126///             Resource::from_contents(json!({
127///                 // Should be retrieved by `ExampleRetriever`
128///                 "$ref": "https://example.com/common/user.json"
129///             }))?
130///         )
131///     ])
132///     .await?;
133/// # Ok(())
134/// # }
135/// # }
136/// ```
137///
138/// The registry will automatically:
139///
140/// - Resolve external references
141/// - Cache retrieved schemas
142/// - Handle nested references
143/// - Process JSON Schema anchors
144///
145#[derive(Debug)]
146pub struct Registry {
147    documents: DocumentStore,
148    pub(crate) resources: ResourceMap,
149    anchors: AHashMap<AnchorKey, Anchor>,
150    resolution_cache: SharedUriCache,
151}
152
153impl Clone for Registry {
154    fn clone(&self) -> Self {
155        Self {
156            documents: self.documents.clone(),
157            resources: self.resources.clone(),
158            anchors: self.anchors.clone(),
159            resolution_cache: self.resolution_cache.clone(),
160        }
161    }
162}
163
164/// Configuration options for creating a [`Registry`].
165pub struct RegistryOptions<R> {
166    retriever: R,
167    draft: Draft,
168}
169
170impl<R> RegistryOptions<R> {
171    /// Set specification version under which the resources should be interpreted under.
172    #[must_use]
173    pub fn draft(mut self, draft: Draft) -> Self {
174        self.draft = draft;
175        self
176    }
177}
178
179impl RegistryOptions<Arc<dyn Retrieve>> {
180    /// Create a new [`RegistryOptions`] with default settings.
181    #[must_use]
182    pub fn new() -> Self {
183        Self {
184            retriever: Arc::new(DefaultRetriever),
185            draft: Draft::default(),
186        }
187    }
188    /// Set a custom retriever for the [`Registry`].
189    #[must_use]
190    pub fn retriever(mut self, retriever: impl IntoRetriever) -> Self {
191        self.retriever = retriever.into_retriever();
192        self
193    }
194    /// Set a custom async retriever for the [`Registry`].
195    #[cfg(feature = "retrieve-async")]
196    #[must_use]
197    pub fn async_retriever(
198        self,
199        retriever: impl IntoAsyncRetriever,
200    ) -> RegistryOptions<Arc<dyn crate::AsyncRetrieve>> {
201        RegistryOptions {
202            retriever: retriever.into_retriever(),
203            draft: self.draft,
204        }
205    }
206    /// Create a [`Registry`] from multiple resources using these options.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error if:
211    /// - Any URI is invalid
212    /// - Any referenced resources cannot be retrieved
213    pub fn build(
214        self,
215        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
216    ) -> Result<Registry, Error> {
217        Registry::try_from_resources_impl(pairs, &*self.retriever, self.draft)
218    }
219}
220
221#[cfg(feature = "retrieve-async")]
222impl RegistryOptions<Arc<dyn crate::AsyncRetrieve>> {
223    /// Create a [`Registry`] from multiple resources using these options with async retrieval.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if:
228    /// - Any URI is invalid
229    /// - Any referenced resources cannot be retrieved
230    pub async fn build(
231        self,
232        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
233    ) -> Result<Registry, Error> {
234        Registry::try_from_resources_async_impl(pairs, &*self.retriever, self.draft).await
235    }
236}
237
238pub trait IntoRetriever {
239    fn into_retriever(self) -> Arc<dyn Retrieve>;
240}
241
242impl<T: Retrieve + 'static> IntoRetriever for T {
243    fn into_retriever(self) -> Arc<dyn Retrieve> {
244        Arc::new(self)
245    }
246}
247
248impl IntoRetriever for Arc<dyn Retrieve> {
249    fn into_retriever(self) -> Arc<dyn Retrieve> {
250        self
251    }
252}
253
254#[cfg(feature = "retrieve-async")]
255pub trait IntoAsyncRetriever {
256    fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve>;
257}
258
259#[cfg(feature = "retrieve-async")]
260impl<T: crate::AsyncRetrieve + 'static> IntoAsyncRetriever for T {
261    fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve> {
262        Arc::new(self)
263    }
264}
265
266#[cfg(feature = "retrieve-async")]
267impl IntoAsyncRetriever for Arc<dyn crate::AsyncRetrieve> {
268    fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve> {
269        self
270    }
271}
272
273impl Default for RegistryOptions<Arc<dyn Retrieve>> {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279impl Registry {
280    /// Get [`RegistryOptions`] for configuring a new [`Registry`].
281    #[must_use]
282    pub fn options() -> RegistryOptions<Arc<dyn Retrieve>> {
283        RegistryOptions::new()
284    }
285    /// Create a new [`Registry`] with a single resource.
286    ///
287    /// # Arguments
288    ///
289    /// * `uri` - The URI of the resource.
290    /// * `resource` - The resource to add.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if the URI is invalid or if there's an issue processing the resource.
295    pub fn try_new(uri: impl AsRef<str>, resource: Resource) -> Result<Self, Error> {
296        Self::try_new_impl(uri, resource, &DefaultRetriever, Draft::default())
297    }
298    /// Create a new [`Registry`] from an iterator of (URI, Resource) pairs.
299    ///
300    /// # Arguments
301    ///
302    /// * `pairs` - An iterator of (URI, Resource) pairs.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error if any URI is invalid or if there's an issue processing the resources.
307    pub fn try_from_resources(
308        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
309    ) -> Result<Self, Error> {
310        Self::try_from_resources_impl(pairs, &DefaultRetriever, Draft::default())
311    }
312    fn try_new_impl(
313        uri: impl AsRef<str>,
314        resource: Resource,
315        retriever: &dyn Retrieve,
316        draft: Draft,
317    ) -> Result<Self, Error> {
318        Self::try_from_resources_impl([(uri, resource)], retriever, draft)
319    }
320    fn try_from_resources_impl(
321        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
322        retriever: &dyn Retrieve,
323        draft: Draft,
324    ) -> Result<Self, Error> {
325        let mut documents = AHashMap::new();
326        let mut resources = ResourceMap::new();
327        let mut anchors = AHashMap::new();
328        let mut resolution_cache = UriCache::new();
329        process_resources(
330            pairs,
331            retriever,
332            &mut documents,
333            &mut resources,
334            &mut anchors,
335            &mut resolution_cache,
336            draft,
337        )?;
338        Ok(Registry {
339            documents,
340            resources,
341            anchors,
342            resolution_cache: resolution_cache.into_shared(),
343        })
344    }
345    /// Create a new [`Registry`] from an iterator of (URI, Resource) pairs using an async retriever.
346    ///
347    /// # Arguments
348    ///
349    /// * `pairs` - An iterator of (URI, Resource) pairs.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if any URI is invalid or if there's an issue processing the resources.
354    #[cfg(feature = "retrieve-async")]
355    async fn try_from_resources_async_impl(
356        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
357        retriever: &dyn crate::AsyncRetrieve,
358        draft: Draft,
359    ) -> Result<Self, Error> {
360        let mut documents = AHashMap::new();
361        let mut resources = ResourceMap::new();
362        let mut anchors = AHashMap::new();
363        let mut resolution_cache = UriCache::new();
364
365        process_resources_async(
366            pairs,
367            retriever,
368            &mut documents,
369            &mut resources,
370            &mut anchors,
371            &mut resolution_cache,
372            draft,
373        )
374        .await?;
375
376        Ok(Registry {
377            documents,
378            resources,
379            anchors,
380            resolution_cache: resolution_cache.into_shared(),
381        })
382    }
383    /// Create a new registry with a new resource.
384    ///
385    /// # Errors
386    ///
387    /// Returns an error if the URI is invalid or if there's an issue processing the resource.
388    pub fn try_with_resource(
389        self,
390        uri: impl AsRef<str>,
391        resource: Resource,
392    ) -> Result<Registry, Error> {
393        let draft = resource.draft();
394        self.try_with_resources([(uri, resource)], draft)
395    }
396    /// Create a new registry with new resources.
397    ///
398    /// # Errors
399    ///
400    /// Returns an error if any URI is invalid or if there's an issue processing the resources.
401    pub fn try_with_resources(
402        self,
403        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
404        draft: Draft,
405    ) -> Result<Registry, Error> {
406        self.try_with_resources_and_retriever(pairs, &DefaultRetriever, draft)
407    }
408    /// Create a new registry with new resources and using the given retriever.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error if any URI is invalid or if there's an issue processing the resources.
413    pub fn try_with_resources_and_retriever(
414        self,
415        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
416        retriever: &dyn Retrieve,
417        draft: Draft,
418    ) -> Result<Registry, Error> {
419        let mut documents = self.documents;
420        let mut resources = self.resources;
421        let mut anchors = self.anchors;
422        let mut resolution_cache = self.resolution_cache.into_local();
423        process_resources(
424            pairs,
425            retriever,
426            &mut documents,
427            &mut resources,
428            &mut anchors,
429            &mut resolution_cache,
430            draft,
431        )?;
432        Ok(Registry {
433            documents,
434            resources,
435            anchors,
436            resolution_cache: resolution_cache.into_shared(),
437        })
438    }
439    /// Create a new registry with new resources and using the given non-blocking retriever.
440    ///
441    /// # Errors
442    ///
443    /// Returns an error if any URI is invalid or if there's an issue processing the resources.
444    #[cfg(feature = "retrieve-async")]
445    pub async fn try_with_resources_and_retriever_async(
446        self,
447        pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
448        retriever: &dyn crate::AsyncRetrieve,
449        draft: Draft,
450    ) -> Result<Registry, Error> {
451        let mut documents = self.documents;
452        let mut resources = self.resources;
453        let mut anchors = self.anchors;
454        let mut resolution_cache = self.resolution_cache.into_local();
455        process_resources_async(
456            pairs,
457            retriever,
458            &mut documents,
459            &mut resources,
460            &mut anchors,
461            &mut resolution_cache,
462            draft,
463        )
464        .await?;
465        Ok(Registry {
466            documents,
467            resources,
468            anchors,
469            resolution_cache: resolution_cache.into_shared(),
470        })
471    }
472    /// Create a new [`Resolver`] for this registry with the given base URI.
473    ///
474    /// # Errors
475    ///
476    /// Returns an error if the base URI is invalid.
477    pub fn try_resolver(&self, base_uri: &str) -> Result<Resolver<'_>, Error> {
478        let base = uri::from_str(base_uri)?;
479        Ok(self.resolver(base))
480    }
481    /// Create a new [`Resolver`] for this registry with a known valid base URI.
482    #[must_use]
483    pub fn resolver(&self, base_uri: Uri<String>) -> Resolver<'_> {
484        Resolver::new(self, Arc::new(base_uri))
485    }
486    #[must_use]
487    pub fn resolver_from_raw_parts(
488        &self,
489        base_uri: Arc<Uri<String>>,
490        scopes: List<Uri<String>>,
491    ) -> Resolver<'_> {
492        Resolver::from_parts(self, base_uri, scopes)
493    }
494    pub(crate) fn anchor<'a>(&self, uri: &'a Uri<String>, name: &'a str) -> Result<&Anchor, Error> {
495        let key = AnchorKeyRef::new(uri, name);
496        if let Some(value) = self.anchors.get(key.borrow_dyn()) {
497            return Ok(value);
498        }
499        let resource = &self.resources[uri];
500        if let Some(id) = resource.id() {
501            let uri = uri::from_str(id)?;
502            let key = AnchorKeyRef::new(&uri, name);
503            if let Some(value) = self.anchors.get(key.borrow_dyn()) {
504                return Ok(value);
505            }
506        }
507        if name.contains('/') {
508            Err(Error::invalid_anchor(name.to_string()))
509        } else {
510            Err(Error::no_such_anchor(name.to_string()))
511        }
512    }
513    /// Resolves a reference URI against a base URI using registry's cache.
514    ///
515    /// # Errors
516    ///
517    /// Returns an error if base has not schema or there is a fragment.
518    pub fn resolve_against(&self, base: &Uri<&str>, uri: &str) -> Result<Arc<Uri<String>>, Error> {
519        self.resolution_cache.resolve_against(base, uri)
520    }
521    /// Returns vocabulary set configured for given draft and contents.
522    #[must_use]
523    pub fn find_vocabularies(&self, draft: Draft, contents: &Value) -> VocabularySet {
524        match draft.detect(contents) {
525            Ok(draft) => draft.default_vocabularies(),
526            Err(Error::UnknownSpecification { specification }) => {
527                // Try to lookup the specification and find enabled vocabularies
528                if let Ok(Some(resource)) =
529                    uri::from_str(&specification).map(|uri| self.resources.get(&uri))
530                {
531                    if let Ok(Some(vocabularies)) = vocabularies::find(resource.contents()) {
532                        return vocabularies;
533                    }
534                }
535                draft.default_vocabularies()
536            }
537            _ => unreachable!(),
538        }
539    }
540
541    /// Build a registry with all the given meta-schemas from specs.
542    pub(crate) fn build_from_meta_schemas(schemas: &[(&'static str, &'static Value)]) -> Self {
543        let schemas_count = schemas.len();
544        let pairs = schemas.iter().map(|(uri, schema)| {
545            (
546                uri,
547                ResourceRef::from_contents(schema).expect("Invalid resource"),
548            )
549        });
550
551        let mut documents = DocumentStore::with_capacity(schemas_count);
552        let mut resources = ResourceMap::with_capacity(schemas_count);
553
554        // The actual number of anchors and cache-entries varies across
555        // drafts. We overshoot here to avoid reallocations, using the sum
556        // over all specifications.
557        let mut anchors = AHashMap::with_capacity(8);
558        let mut resolution_cache = UriCache::with_capacity(35);
559
560        process_meta_schemas(
561            pairs,
562            &mut documents,
563            &mut resources,
564            &mut anchors,
565            &mut resolution_cache,
566        )
567        .expect("Failed to process meta schemas");
568
569        Self {
570            documents,
571            resources,
572            anchors,
573            resolution_cache: resolution_cache.into_shared(),
574        }
575    }
576}
577
578fn process_meta_schemas(
579    pairs: impl IntoIterator<Item = (impl AsRef<str>, ResourceRef<'static>)>,
580    documents: &mut DocumentStore,
581    resources: &mut ResourceMap,
582    anchors: &mut AHashMap<AnchorKey, Anchor>,
583    resolution_cache: &mut UriCache,
584) -> Result<(), Error> {
585    let mut queue = VecDeque::with_capacity(32);
586
587    for (uri, resource) in pairs {
588        let uri = uri::from_str(uri.as_ref().trim_end_matches('#'))?;
589        let key = Arc::new(uri);
590        let contents: &'static Value = resource.contents();
591        let wrapped_value = Arc::pin(ValueWrapper::StaticRef(contents));
592        let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), resource.draft());
593        documents.insert(Arc::clone(&key), wrapped_value);
594        resources.insert(Arc::clone(&key), resource.clone());
595        queue.push_back((key, resource));
596    }
597
598    // Process current queue and collect references to external resources
599    while let Some((mut base, resource)) = queue.pop_front() {
600        if let Some(id) = resource.id() {
601            base = resolution_cache.resolve_against(&base.borrow(), id)?;
602            resources.insert(base.clone(), resource.clone());
603        }
604
605        // Look for anchors
606        for anchor in resource.anchors() {
607            anchors.insert(AnchorKey::new(base.clone(), anchor.name()), anchor);
608        }
609
610        // Process subresources
611        for contents in resource.draft().subresources_of(resource.contents()) {
612            let subresource = InnerResourcePtr::new(contents, resource.draft());
613            queue.push_back((base.clone(), subresource));
614        }
615    }
616    Ok(())
617}
618
619struct ProcessingState {
620    queue: VecDeque<(Arc<Uri<String>>, InnerResourcePtr)>,
621    seen: HashSet<u64, BuildNoHashHasher>,
622    external: AHashSet<(String, Uri<String>)>,
623    scratch: String,
624    refers_metaschemas: bool,
625}
626
627impl ProcessingState {
628    fn new() -> Self {
629        Self {
630            queue: VecDeque::with_capacity(32),
631            seen: HashSet::with_hasher(BuildNoHashHasher::default()),
632            external: AHashSet::new(),
633            scratch: String::new(),
634            refers_metaschemas: false,
635        }
636    }
637}
638
639fn process_input_resources(
640    pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
641    documents: &mut DocumentStore,
642    resources: &mut ResourceMap,
643    state: &mut ProcessingState,
644) -> Result<(), Error> {
645    for (uri, resource) in pairs {
646        let uri = uri::from_str(uri.as_ref().trim_end_matches('#'))?;
647        let key = Arc::new(uri);
648        match documents.entry(Arc::clone(&key)) {
649            Entry::Occupied(_) => {}
650            Entry::Vacant(entry) => {
651                let (draft, contents) = resource.into_inner();
652                let wrapped_value = Arc::pin(ValueWrapper::Owned(contents));
653                let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), draft);
654                resources.insert(Arc::clone(&key), resource.clone());
655                state.queue.push_back((key, resource));
656                entry.insert(wrapped_value);
657            }
658        }
659    }
660    Ok(())
661}
662
663fn process_queue(
664    state: &mut ProcessingState,
665    resources: &mut ResourceMap,
666    anchors: &mut AHashMap<AnchorKey, Anchor>,
667    resolution_cache: &mut UriCache,
668) -> Result<(), Error> {
669    while let Some((mut base, resource)) = state.queue.pop_front() {
670        if let Some(id) = resource.id() {
671            base = resolution_cache.resolve_against(&base.borrow(), id)?;
672            resources.insert(base.clone(), resource.clone());
673        }
674
675        for anchor in resource.anchors() {
676            anchors.insert(AnchorKey::new(base.clone(), anchor.name()), anchor);
677        }
678
679        collect_external_resources(
680            &base,
681            resource.contents(),
682            &mut state.external,
683            &mut state.seen,
684            resolution_cache,
685            &mut state.scratch,
686            &mut state.refers_metaschemas,
687        )?;
688
689        for contents in resource.draft().subresources_of(resource.contents()) {
690            let subresource = InnerResourcePtr::new(contents, resource.draft());
691            state.queue.push_back((base.clone(), subresource));
692        }
693    }
694    Ok(())
695}
696
697fn handle_fragment(
698    uri: &Uri<String>,
699    resource: &InnerResourcePtr,
700    key: &Arc<Uri<String>>,
701    default_draft: Draft,
702    queue: &mut VecDeque<(Arc<Uri<String>>, InnerResourcePtr)>,
703) -> Result<(), Error> {
704    if let Some(fragment) = uri.fragment() {
705        if let Some(resolved) = pointer(resource.contents(), fragment.as_str()) {
706            let draft = default_draft.detect(resolved)?;
707            let contents = std::ptr::addr_of!(*resolved);
708            let resource = InnerResourcePtr::new(contents, draft);
709            queue.push_back((Arc::clone(key), resource));
710        }
711    }
712    Ok(())
713}
714
715fn handle_metaschemas(
716    refers_metaschemas: bool,
717    resources: &mut ResourceMap,
718    anchors: &mut AHashMap<AnchorKey, Anchor>,
719    draft_version: Draft,
720) {
721    if refers_metaschemas {
722        let schemas = metas_for_draft(draft_version);
723        let draft_registry = Registry::build_from_meta_schemas(schemas);
724        resources.reserve(draft_registry.resources.len());
725        for (key, resource) in draft_registry.resources {
726            resources.insert(key, resource.clone());
727        }
728        anchors.reserve(draft_registry.anchors.len());
729        for (key, anchor) in draft_registry.anchors {
730            anchors.insert(key, anchor);
731        }
732    }
733}
734
735fn create_resource(
736    retrieved: Value,
737    fragmentless: Uri<String>,
738    default_draft: Draft,
739    documents: &mut DocumentStore,
740    resources: &mut ResourceMap,
741) -> Result<(Arc<Uri<String>>, InnerResourcePtr), Error> {
742    let draft = default_draft.detect(&retrieved)?;
743    let wrapped_value = Arc::pin(ValueWrapper::Owned(retrieved));
744    let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), draft);
745    let key = Arc::new(fragmentless);
746    documents.insert(Arc::clone(&key), wrapped_value);
747    resources.insert(Arc::clone(&key), resource.clone());
748    Ok((key, resource))
749}
750
751fn process_resources(
752    pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
753    retriever: &dyn Retrieve,
754    documents: &mut DocumentStore,
755    resources: &mut ResourceMap,
756    anchors: &mut AHashMap<AnchorKey, Anchor>,
757    resolution_cache: &mut UriCache,
758    default_draft: Draft,
759) -> Result<(), Error> {
760    let mut state = ProcessingState::new();
761    process_input_resources(pairs, documents, resources, &mut state)?;
762
763    loop {
764        if state.queue.is_empty() && state.external.is_empty() {
765            break;
766        }
767
768        process_queue(&mut state, resources, anchors, resolution_cache)?;
769
770        // Retrieve external resources
771        for (original, uri) in state.external.drain() {
772            let mut fragmentless = uri.clone();
773            fragmentless.set_fragment(None);
774            if !resources.contains_key(&fragmentless) {
775                let retrieved = match retriever.retrieve(&fragmentless) {
776                    Ok(retrieved) => retrieved,
777                    Err(error) => {
778                        return if uri.scheme().as_str() == "json-schema" {
779                            Err(Error::unretrievable(
780                                original,
781                                "No base URI is available".into(),
782                            ))
783                        } else {
784                            Err(Error::unretrievable(fragmentless.as_str(), error))
785                        }
786                    }
787                };
788
789                let (key, resource) =
790                    create_resource(retrieved, fragmentless, default_draft, documents, resources)?;
791
792                handle_fragment(&uri, &resource, &key, default_draft, &mut state.queue)?;
793
794                state.queue.push_back((key, resource));
795            }
796        }
797    }
798
799    handle_metaschemas(state.refers_metaschemas, resources, anchors, default_draft);
800
801    Ok(())
802}
803
804#[cfg(feature = "retrieve-async")]
805async fn process_resources_async(
806    pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
807    retriever: &dyn crate::AsyncRetrieve,
808    documents: &mut DocumentStore,
809    resources: &mut ResourceMap,
810    anchors: &mut AHashMap<AnchorKey, Anchor>,
811    resolution_cache: &mut UriCache,
812    default_draft: Draft,
813) -> Result<(), Error> {
814    let mut state = ProcessingState::new();
815    process_input_resources(pairs, documents, resources, &mut state)?;
816
817    loop {
818        if state.queue.is_empty() && state.external.is_empty() {
819            break;
820        }
821
822        process_queue(&mut state, resources, anchors, resolution_cache)?;
823
824        if !state.external.is_empty() {
825            let data = state
826                .external
827                .drain()
828                .filter_map(|(original, uri)| {
829                    let mut fragmentless = uri.clone();
830                    fragmentless.set_fragment(None);
831                    if resources.contains_key(&fragmentless) {
832                        None
833                    } else {
834                        Some((original, uri, fragmentless))
835                    }
836                })
837                .collect::<Vec<_>>();
838
839            let results = {
840                let futures = data
841                    .iter()
842                    .map(|(_, _, fragmentless)| retriever.retrieve(fragmentless));
843                futures::future::join_all(futures).await
844            };
845
846            for ((original, uri, fragmentless), result) in data.iter().zip(results) {
847                let retrieved = match result {
848                    Ok(retrieved) => retrieved,
849                    Err(error) => {
850                        return if uri.scheme().as_str() == "json-schema" {
851                            Err(Error::unretrievable(
852                                original,
853                                "No base URI is available".into(),
854                            ))
855                        } else {
856                            Err(Error::unretrievable(fragmentless.as_str(), error))
857                        }
858                    }
859                };
860
861                let (key, resource) = create_resource(
862                    retrieved,
863                    fragmentless.clone(),
864                    default_draft,
865                    documents,
866                    resources,
867                )?;
868
869                handle_fragment(uri, &resource, &key, default_draft, &mut state.queue)?;
870
871                state.queue.push_back((key, resource));
872            }
873        }
874    }
875
876    handle_metaschemas(state.refers_metaschemas, resources, anchors, default_draft);
877
878    Ok(())
879}
880
881fn collect_external_resources(
882    base: &Uri<String>,
883    contents: &Value,
884    collected: &mut AHashSet<(String, Uri<String>)>,
885    seen: &mut HashSet<u64, BuildNoHashHasher>,
886    resolution_cache: &mut UriCache,
887    scratch: &mut String,
888    refers_metaschemas: &mut bool,
889) -> Result<(), Error> {
890    // URN schemes are not supported for external resolution
891    if base.scheme().as_str() == "urn" {
892        return Ok(());
893    }
894
895    macro_rules! on_reference {
896        ($reference:expr, $key:literal) => {
897            // Skip well-known schema references
898            if $reference.starts_with("https://json-schema.org/draft/")
899                || $reference.starts_with("http://json-schema.org/draft-")
900                || base.as_str().starts_with("https://json-schema.org/draft/")
901            {
902                if $key == "$ref" {
903                    *refers_metaschemas = true;
904                }
905            } else if $reference != "#" {
906                let mut hasher = AHasher::default();
907                (base.as_str(), $reference).hash(&mut hasher);
908                let hash = hasher.finish();
909                if seen.insert(hash) {
910                    // Handle local references separately as they may have nested references to external resources
911                    if $reference.starts_with('#') {
912                        if let Some(referenced) =
913                            pointer(contents, $reference.trim_start_matches('#'))
914                        {
915                            collect_external_resources(
916                                base,
917                                referenced,
918                                collected,
919                                seen,
920                                resolution_cache,
921                                scratch,
922                                refers_metaschemas,
923                            )?;
924                        }
925                    } else {
926                        let resolved = if base.has_fragment() {
927                            let mut base_without_fragment = base.clone();
928                            base_without_fragment.set_fragment(None);
929
930                            let (path, fragment) = match $reference.split_once('#') {
931                                Some((path, fragment)) => (path, Some(fragment)),
932                                None => ($reference, None),
933                            };
934
935                            let mut resolved = (*resolution_cache
936                                .resolve_against(&base_without_fragment.borrow(), path)?)
937                            .clone();
938                            // Add the fragment back if present
939                            if let Some(fragment) = fragment {
940                                // It is cheaper to check if it is properly encoded than allocate given that
941                                // the majority of inputs do not need to be additionally encoded
942                                if let Some(encoded) = uri::EncodedString::new(fragment) {
943                                    resolved = resolved.with_fragment(Some(encoded));
944                                } else {
945                                    uri::encode_to(fragment, scratch);
946                                    resolved = resolved.with_fragment(Some(
947                                        uri::EncodedString::new_or_panic(scratch),
948                                    ));
949                                    scratch.clear();
950                                }
951                            }
952                            resolved
953                        } else {
954                            (*resolution_cache.resolve_against(&base.borrow(), $reference)?).clone()
955                        };
956
957                        collected.insert(($reference.to_string(), resolved));
958                    }
959                }
960            }
961        };
962    }
963
964    if let Some(object) = contents.as_object() {
965        if object.len() < 3 {
966            for (key, value) in object {
967                if key == "$ref" {
968                    if let Some(reference) = value.as_str() {
969                        on_reference!(reference, "$ref");
970                    }
971                } else if key == "$schema" {
972                    if let Some(reference) = value.as_str() {
973                        on_reference!(reference, "$schema");
974                    }
975                }
976            }
977        } else {
978            if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
979                on_reference!(reference, "$ref");
980            }
981            if let Some(reference) = object.get("$schema").and_then(Value::as_str) {
982                on_reference!(reference, "$schema");
983            }
984        }
985    }
986    Ok(())
987}
988
989/// Look up a value by a JSON Pointer.
990///
991/// **NOTE**: A slightly faster version of pointer resolution based on `Value::pointer` from `serde_json`.
992pub fn pointer<'a>(document: &'a Value, pointer: &str) -> Option<&'a Value> {
993    if pointer.is_empty() {
994        return Some(document);
995    }
996    if !pointer.starts_with('/') {
997        return None;
998    }
999    pointer.split('/').skip(1).map(unescape_segment).try_fold(
1000        document,
1001        |target, token| match target {
1002            Value::Object(map) => map.get(&*token),
1003            Value::Array(list) => parse_index(&token).and_then(|x| list.get(x)),
1004            _ => None,
1005        },
1006    )
1007}
1008
1009// Taken from `serde_json`.
1010#[must_use]
1011pub fn parse_index(s: &str) -> Option<usize> {
1012    if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) {
1013        return None;
1014    }
1015    s.parse().ok()
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use std::error::Error as _;
1021
1022    use ahash::AHashMap;
1023    use fluent_uri::Uri;
1024    use serde_json::{json, Value};
1025    use test_case::test_case;
1026
1027    use crate::{uri::from_str, Draft, Registry, Resource, Retrieve};
1028
1029    use super::{pointer, RegistryOptions, SPECIFICATIONS};
1030
1031    #[test]
1032    fn test_empty_pointer() {
1033        let document = json!({});
1034        assert_eq!(pointer(&document, ""), Some(&document));
1035    }
1036
1037    #[test]
1038    fn test_invalid_uri_on_registry_creation() {
1039        let schema = Draft::Draft202012.create_resource(json!({}));
1040        let result = Registry::try_new(":/example.com", schema);
1041        let error = result.expect_err("Should fail");
1042
1043        assert_eq!(
1044            error.to_string(),
1045            "Invalid URI reference ':/example.com': unexpected character at index 0"
1046        );
1047        let source_error = error.source().expect("Should have a source");
1048        let inner_source = source_error.source().expect("Should have a source");
1049        assert_eq!(inner_source.to_string(), "unexpected character at index 0");
1050    }
1051
1052    #[test]
1053    fn test_lookup_unresolvable_url() {
1054        // Create a registry with a single resource
1055        let schema = Draft::Draft202012.create_resource(json!({
1056            "type": "object",
1057            "properties": {
1058                "foo": { "type": "string" }
1059            }
1060        }));
1061        let registry =
1062            Registry::try_new("http://example.com/schema1", schema).expect("Invalid resources");
1063
1064        // Attempt to create a resolver for a URL not in the registry
1065        let resolver = registry
1066            .try_resolver("http://example.com/non_existent_schema")
1067            .expect("Invalid base URI");
1068
1069        let result = resolver.lookup("");
1070
1071        assert_eq!(
1072            result.unwrap_err().to_string(),
1073            "Resource 'http://example.com/non_existent_schema' is not present in a registry and retrieving it failed: Retrieving external resources is not supported once the registry is populated"
1074        );
1075    }
1076
1077    #[test]
1078    fn test_relative_uri_without_base() {
1079        let schema = Draft::Draft202012.create_resource(json!({"$ref": "./virtualNetwork.json"}));
1080        let error = Registry::try_new("json-schema:///", schema).expect_err("Should fail");
1081        assert_eq!(error.to_string(), "Resource './virtualNetwork.json' is not present in a registry and retrieving it failed: No base URI is available");
1082    }
1083
1084    struct TestRetriever {
1085        schemas: AHashMap<String, Value>,
1086    }
1087
1088    impl TestRetriever {
1089        fn new(schemas: AHashMap<String, Value>) -> Self {
1090            TestRetriever { schemas }
1091        }
1092    }
1093
1094    impl Retrieve for TestRetriever {
1095        fn retrieve(
1096            &self,
1097            uri: &Uri<String>,
1098        ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1099            if let Some(value) = self.schemas.get(uri.as_str()) {
1100                Ok(value.clone())
1101            } else {
1102                Err(format!("Failed to find {uri}").into())
1103            }
1104        }
1105    }
1106
1107    fn create_test_retriever(schemas: &[(&str, Value)]) -> TestRetriever {
1108        TestRetriever::new(
1109            schemas
1110                .iter()
1111                .map(|&(k, ref v)| (k.to_string(), v.clone()))
1112                .collect(),
1113        )
1114    }
1115
1116    struct TestCase {
1117        input_resources: Vec<(&'static str, Value)>,
1118        remote_resources: Vec<(&'static str, Value)>,
1119        expected_resolved_uris: Vec<&'static str>,
1120    }
1121
1122    #[test_case(
1123        TestCase {
1124            input_resources: vec![
1125                ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1126            ],
1127            remote_resources: vec![
1128                ("http://example.com/schema2", json!({"type": "object"})),
1129            ],
1130            expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1131        }
1132    ;"External ref at top")]
1133    #[test_case(
1134        TestCase {
1135            input_resources: vec![
1136                ("http://example.com/schema1", json!({
1137                    "$defs": {
1138                        "subschema": {"type": "string"}
1139                    },
1140                    "$ref": "#/$defs/subschema"
1141                })),
1142            ],
1143            remote_resources: vec![],
1144            expected_resolved_uris: vec!["http://example.com/schema1"],
1145        }
1146    ;"Internal ref at top")]
1147    #[test_case(
1148        TestCase {
1149            input_resources: vec![
1150                ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1151                ("http://example.com/schema2", json!({"type": "object"})),
1152            ],
1153            remote_resources: vec![],
1154            expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1155        }
1156    ;"Ref to later resource")]
1157    #[test_case(
1158    TestCase {
1159            input_resources: vec![
1160                ("http://example.com/schema1", json!({
1161                    "type": "object",
1162                    "properties": {
1163                        "prop1": {"$ref": "http://example.com/schema2"}
1164                    }
1165                })),
1166            ],
1167            remote_resources: vec![
1168                ("http://example.com/schema2", json!({"type": "string"})),
1169            ],
1170            expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1171        }
1172    ;"External ref in subresource")]
1173    #[test_case(
1174        TestCase {
1175            input_resources: vec![
1176                ("http://example.com/schema1", json!({
1177                    "type": "object",
1178                    "properties": {
1179                        "prop1": {"$ref": "#/$defs/subschema"}
1180                    },
1181                    "$defs": {
1182                        "subschema": {"type": "string"}
1183                    }
1184                })),
1185            ],
1186            remote_resources: vec![],
1187            expected_resolved_uris: vec!["http://example.com/schema1"],
1188        }
1189    ;"Internal ref in subresource")]
1190    #[test_case(
1191        TestCase {
1192            input_resources: vec![
1193                ("file:///schemas/main.json", json!({"$ref": "file:///schemas/external.json"})),
1194            ],
1195            remote_resources: vec![
1196                ("file:///schemas/external.json", json!({"type": "object"})),
1197            ],
1198            expected_resolved_uris: vec!["file:///schemas/main.json", "file:///schemas/external.json"],
1199        }
1200    ;"File scheme: external ref at top")]
1201    #[test_case(
1202        TestCase {
1203            input_resources: vec![
1204                ("file:///schemas/main.json", json!({"$ref": "subfolder/schema.json"})),
1205            ],
1206            remote_resources: vec![
1207                ("file:///schemas/subfolder/schema.json", json!({"type": "string"})),
1208            ],
1209            expected_resolved_uris: vec!["file:///schemas/main.json", "file:///schemas/subfolder/schema.json"],
1210        }
1211    ;"File scheme: relative path ref")]
1212    #[test_case(
1213        TestCase {
1214            input_resources: vec![
1215                ("file:///schemas/main.json", json!({
1216                    "type": "object",
1217                    "properties": {
1218                        "local": {"$ref": "local.json"},
1219                        "remote": {"$ref": "http://example.com/schema"}
1220                    }
1221                })),
1222            ],
1223            remote_resources: vec![
1224                ("file:///schemas/local.json", json!({"type": "string"})),
1225                ("http://example.com/schema", json!({"type": "number"})),
1226            ],
1227            expected_resolved_uris: vec![
1228                "file:///schemas/main.json",
1229                "file:///schemas/local.json",
1230                "http://example.com/schema"
1231            ],
1232        }
1233    ;"File scheme: mixing with http scheme")]
1234    #[test_case(
1235        TestCase {
1236            input_resources: vec![
1237                ("file:///C:/schemas/main.json", json!({"$ref": "/D:/other_schemas/schema.json"})),
1238            ],
1239            remote_resources: vec![
1240                ("file:///D:/other_schemas/schema.json", json!({"type": "boolean"})),
1241            ],
1242            expected_resolved_uris: vec![
1243                "file:///C:/schemas/main.json",
1244                "file:///D:/other_schemas/schema.json"
1245            ],
1246        }
1247    ;"File scheme: absolute path in Windows style")]
1248    #[test_case(
1249        TestCase {
1250            input_resources: vec![
1251                ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1252            ],
1253            remote_resources: vec![
1254                ("http://example.com/schema2", json!({"$ref": "http://example.com/schema3"})),
1255                ("http://example.com/schema3", json!({"$ref": "http://example.com/schema4"})),
1256                ("http://example.com/schema4", json!({"$ref": "http://example.com/schema5"})),
1257                ("http://example.com/schema5", json!({"type": "object"})),
1258            ],
1259            expected_resolved_uris: vec![
1260                "http://example.com/schema1",
1261                "http://example.com/schema2",
1262                "http://example.com/schema3",
1263                "http://example.com/schema4",
1264                "http://example.com/schema5",
1265            ],
1266        }
1267    ;"Four levels of external references")]
1268    #[test_case(
1269        TestCase {
1270            input_resources: vec![
1271                ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1272            ],
1273            remote_resources: vec![
1274                ("http://example.com/schema2", json!({"$ref": "http://example.com/schema3"})),
1275                ("http://example.com/schema3", json!({"$ref": "http://example.com/schema4"})),
1276                ("http://example.com/schema4", json!({"$ref": "http://example.com/schema5"})),
1277                ("http://example.com/schema5", json!({"$ref": "http://example.com/schema6"})),
1278                ("http://example.com/schema6", json!({"$ref": "http://example.com/schema1"})),
1279            ],
1280            expected_resolved_uris: vec![
1281                "http://example.com/schema1",
1282                "http://example.com/schema2",
1283                "http://example.com/schema3",
1284                "http://example.com/schema4",
1285                "http://example.com/schema5",
1286                "http://example.com/schema6",
1287            ],
1288        }
1289    ;"Five levels of external references with circular reference")]
1290    fn test_references_processing(test_case: TestCase) {
1291        let retriever = create_test_retriever(&test_case.remote_resources);
1292
1293        let input_pairs = test_case
1294            .input_resources
1295            .clone()
1296            .into_iter()
1297            .map(|(uri, value)| {
1298                (
1299                    uri,
1300                    Resource::from_contents(value).expect("Invalid resource"),
1301                )
1302            });
1303
1304        let registry = Registry::options()
1305            .retriever(retriever)
1306            .build(input_pairs)
1307            .expect("Invalid resources");
1308        // Verify that all expected URIs are resolved and present in resources
1309        for uri in test_case.expected_resolved_uris {
1310            let resolver = registry.try_resolver("").expect("Invalid base URI");
1311            assert!(resolver.lookup(uri).is_ok());
1312        }
1313    }
1314
1315    #[test]
1316    fn test_default_retriever_with_remote_refs() {
1317        let result = Registry::try_from_resources([(
1318            "http://example.com/schema1",
1319            Resource::from_contents(json!({"$ref": "http://example.com/schema2"}))
1320                .expect("Invalid resource"),
1321        )]);
1322        let error = result.expect_err("Should fail");
1323        assert_eq!(error.to_string(), "Resource 'http://example.com/schema2' is not present in a registry and retrieving it failed: Default retriever does not fetch resources");
1324        assert!(error.source().is_some());
1325    }
1326
1327    #[test]
1328    fn test_options() {
1329        let _registry = RegistryOptions::default()
1330            .build([(
1331                "",
1332                Resource::from_contents(json!({})).expect("Invalid resource"),
1333            )])
1334            .expect("Invalid resources");
1335    }
1336
1337    #[test]
1338    fn test_registry_with_duplicate_input_uris() {
1339        let input_resources = vec![
1340            (
1341                "http://example.com/schema",
1342                json!({
1343                    "type": "object",
1344                    "properties": {
1345                        "foo": { "type": "string" }
1346                    }
1347                }),
1348            ),
1349            (
1350                "http://example.com/schema",
1351                json!({
1352                    "type": "object",
1353                    "properties": {
1354                        "bar": { "type": "number" }
1355                    }
1356                }),
1357            ),
1358        ];
1359
1360        let result = Registry::try_from_resources(
1361            input_resources
1362                .into_iter()
1363                .map(|(uri, value)| (uri, Draft::Draft202012.create_resource(value))),
1364        );
1365
1366        assert!(
1367            result.is_ok(),
1368            "Failed to create registry with duplicate input URIs"
1369        );
1370        let registry = result.unwrap();
1371
1372        let resource = registry
1373            .resources
1374            .get(&from_str("http://example.com/schema").expect("Invalid URI"))
1375            .unwrap();
1376        let properties = resource
1377            .contents()
1378            .get("properties")
1379            .and_then(|v| v.as_object())
1380            .unwrap();
1381
1382        assert!(
1383            !properties.contains_key("bar"),
1384            "Registry should contain the earliest added schema"
1385        );
1386        assert!(
1387            properties.contains_key("foo"),
1388            "Registry should contain the overwritten schema"
1389        );
1390    }
1391
1392    #[test]
1393    fn test_resolver_debug() {
1394        let registry = SPECIFICATIONS
1395            .clone()
1396            .try_with_resource(
1397                "http://example.com",
1398                Resource::from_contents(json!({})).expect("Invalid resource"),
1399            )
1400            .expect("Invalid resource");
1401        let resolver = registry
1402            .try_resolver("http://127.0.0.1/schema")
1403            .expect("Invalid base URI");
1404        assert_eq!(
1405            format!("{resolver:?}"),
1406            "Resolver { base_uri: \"http://127.0.0.1/schema\", scopes: \"[]\" }"
1407        );
1408    }
1409
1410    #[test]
1411    fn test_try_with_resource() {
1412        let registry = SPECIFICATIONS
1413            .clone()
1414            .try_with_resource(
1415                "http://example.com",
1416                Resource::from_contents(json!({})).expect("Invalid resource"),
1417            )
1418            .expect("Invalid resource");
1419        let resolver = registry.try_resolver("").expect("Invalid base URI");
1420        let resolved = resolver
1421            .lookup("http://json-schema.org/draft-06/schema#/definitions/schemaArray")
1422            .expect("Lookup failed");
1423        assert_eq!(
1424            resolved.contents(),
1425            &json!({
1426                "type": "array",
1427                "minItems": 1,
1428                "items": { "$ref": "#" }
1429            })
1430        );
1431    }
1432
1433    #[test]
1434    fn test_invalid_reference() {
1435        // Found via fuzzing
1436        let resource = Draft::Draft202012.create_resource(json!({"$schema": "$##"}));
1437        let _ = Registry::try_new("http://#/", resource);
1438    }
1439}
1440
1441#[cfg(all(test, feature = "retrieve-async"))]
1442mod async_tests {
1443    use crate::{uri, DefaultRetriever, Draft, Registry, Resource, Uri};
1444    use ahash::AHashMap;
1445    use serde_json::{json, Value};
1446    use std::error::Error;
1447
1448    struct TestAsyncRetriever {
1449        schemas: AHashMap<String, Value>,
1450    }
1451
1452    impl TestAsyncRetriever {
1453        fn with_schema(uri: impl Into<String>, schema: Value) -> Self {
1454            TestAsyncRetriever {
1455                schemas: { AHashMap::from_iter([(uri.into(), schema)]) },
1456            }
1457        }
1458    }
1459
1460    #[async_trait::async_trait]
1461    impl crate::AsyncRetrieve for TestAsyncRetriever {
1462        async fn retrieve(
1463            &self,
1464            uri: &Uri<String>,
1465        ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1466            self.schemas
1467                .get(uri.as_str())
1468                .cloned()
1469                .ok_or_else(|| "Schema not found".into())
1470        }
1471    }
1472
1473    #[tokio::test]
1474    async fn test_default_async_retriever_with_remote_refs() {
1475        let result = Registry::options()
1476            .async_retriever(DefaultRetriever)
1477            .build([(
1478                "http://example.com/schema1",
1479                Resource::from_contents(json!({"$ref": "http://example.com/schema2"}))
1480                    .expect("Invalid resource"),
1481            )])
1482            .await;
1483
1484        let error = result.expect_err("Should fail");
1485        assert_eq!(error.to_string(), "Resource 'http://example.com/schema2' is not present in a registry and retrieving it failed: Default retriever does not fetch resources");
1486        assert!(error.source().is_some());
1487    }
1488
1489    #[tokio::test]
1490    async fn test_async_options() {
1491        let _registry = Registry::options()
1492            .async_retriever(DefaultRetriever)
1493            .build([("", Draft::default().create_resource(json!({})))])
1494            .await
1495            .expect("Invalid resources");
1496    }
1497
1498    #[tokio::test]
1499    async fn test_async_registry_with_duplicate_input_uris() {
1500        let input_resources = vec![
1501            (
1502                "http://example.com/schema",
1503                json!({
1504                    "type": "object",
1505                    "properties": {
1506                        "foo": { "type": "string" }
1507                    }
1508                }),
1509            ),
1510            (
1511                "http://example.com/schema",
1512                json!({
1513                    "type": "object",
1514                    "properties": {
1515                        "bar": { "type": "number" }
1516                    }
1517                }),
1518            ),
1519        ];
1520
1521        let result = Registry::options()
1522            .async_retriever(DefaultRetriever)
1523            .build(
1524                input_resources
1525                    .into_iter()
1526                    .map(|(uri, value)| (uri, Draft::Draft202012.create_resource(value))),
1527            )
1528            .await;
1529
1530        assert!(
1531            result.is_ok(),
1532            "Failed to create registry with duplicate input URIs"
1533        );
1534        let registry = result.unwrap();
1535
1536        let resource = registry
1537            .resources
1538            .get(&uri::from_str("http://example.com/schema").expect("Invalid URI"))
1539            .unwrap();
1540        let properties = resource
1541            .contents()
1542            .get("properties")
1543            .and_then(|v| v.as_object())
1544            .unwrap();
1545
1546        assert!(
1547            !properties.contains_key("bar"),
1548            "Registry should contain the earliest added schema"
1549        );
1550        assert!(
1551            properties.contains_key("foo"),
1552            "Registry should contain the overwritten schema"
1553        );
1554    }
1555
1556    #[tokio::test]
1557    async fn test_async_try_with_resource() {
1558        let retriever = TestAsyncRetriever::with_schema(
1559            "http://example.com/schema2",
1560            json!({"type": "object"}),
1561        );
1562
1563        let registry = Registry::options()
1564            .async_retriever(retriever)
1565            .build([(
1566                "http://example.com",
1567                Resource::from_contents(json!({"$ref": "http://example.com/schema2"}))
1568                    .expect("Invalid resource"),
1569            )])
1570            .await
1571            .expect("Invalid resource");
1572
1573        let resolver = registry.try_resolver("").expect("Invalid base URI");
1574        let resolved = resolver
1575            .lookup("http://example.com/schema2")
1576            .expect("Lookup failed");
1577        assert_eq!(resolved.contents(), &json!({"type": "object"}));
1578    }
1579
1580    #[tokio::test]
1581    async fn test_async_registry_with_multiple_refs() {
1582        let retriever = TestAsyncRetriever {
1583            schemas: AHashMap::from_iter([
1584                (
1585                    "http://example.com/schema2".to_string(),
1586                    json!({"type": "object"}),
1587                ),
1588                (
1589                    "http://example.com/schema3".to_string(),
1590                    json!({"type": "string"}),
1591                ),
1592            ]),
1593        };
1594
1595        let registry = Registry::options()
1596            .async_retriever(retriever)
1597            .build([(
1598                "http://example.com/schema1",
1599                Resource::from_contents(json!({
1600                    "type": "object",
1601                    "properties": {
1602                        "obj": {"$ref": "http://example.com/schema2"},
1603                        "str": {"$ref": "http://example.com/schema3"}
1604                    }
1605                }))
1606                .expect("Invalid resource"),
1607            )])
1608            .await
1609            .expect("Invalid resource");
1610
1611        let resolver = registry.try_resolver("").expect("Invalid base URI");
1612
1613        // Check both references are resolved correctly
1614        let resolved2 = resolver
1615            .lookup("http://example.com/schema2")
1616            .expect("Lookup failed");
1617        assert_eq!(resolved2.contents(), &json!({"type": "object"}));
1618
1619        let resolved3 = resolver
1620            .lookup("http://example.com/schema3")
1621            .expect("Lookup failed");
1622        assert_eq!(resolved3.contents(), &json!({"type": "string"}));
1623    }
1624
1625    #[tokio::test]
1626    async fn test_async_registry_with_nested_refs() {
1627        let retriever = TestAsyncRetriever {
1628            schemas: AHashMap::from_iter([
1629                (
1630                    "http://example.com/address".to_string(),
1631                    json!({
1632                        "type": "object",
1633                        "properties": {
1634                            "street": {"type": "string"},
1635                            "city": {"$ref": "http://example.com/city"}
1636                        }
1637                    }),
1638                ),
1639                (
1640                    "http://example.com/city".to_string(),
1641                    json!({
1642                        "type": "string",
1643                        "minLength": 1
1644                    }),
1645                ),
1646            ]),
1647        };
1648
1649        let registry = Registry::options()
1650            .async_retriever(retriever)
1651            .build([(
1652                "http://example.com/person",
1653                Resource::from_contents(json!({
1654                    "type": "object",
1655                    "properties": {
1656                        "name": {"type": "string"},
1657                        "address": {"$ref": "http://example.com/address"}
1658                    }
1659                }))
1660                .expect("Invalid resource"),
1661            )])
1662            .await
1663            .expect("Invalid resource");
1664
1665        let resolver = registry.try_resolver("").expect("Invalid base URI");
1666
1667        // Verify nested reference resolution
1668        let resolved = resolver
1669            .lookup("http://example.com/city")
1670            .expect("Lookup failed");
1671        assert_eq!(
1672            resolved.contents(),
1673            &json!({"type": "string", "minLength": 1})
1674        );
1675    }
1676}