jsonschema_valid/
resolver.rs

1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::context::Context;
6use crate::error::ValidationError;
7use crate::schemas::{self, Draft};
8// TODO: Make the choice of resolver dynamic
9
10const DOCUMENT_PROTOCOL: &str = "document:///";
11
12fn id_of(draft: Draft, schema: &Value) -> Option<&str> {
13    if let Value::Object(object) = schema {
14        if draft == Draft::Draft4 {
15            object.get("$id").or_else(|| object.get("id"))
16        } else {
17            object.get("$id")
18        }
19        .and_then(Value::as_str)
20    } else {
21        None
22    }
23}
24
25pub struct Resolver<'a> {
26    base_url: String,
27    id_mapping: HashMap<String, &'a Value>,
28}
29
30/// Iterate through all of the document fragments with an assigned id, calling a
31/// callback at each location.
32fn find_ids<'a, F>(
33    draft: Draft,
34    schema: &'a Value,
35    base_url: &url::Url,
36    visitor: &mut F,
37) -> Result<Option<&'a Value>, ValidationError>
38where
39    F: FnMut(String, &'a Value) -> Option<&'a Value>,
40{
41    match schema {
42        Value::Object(object) => {
43            if let Some(url) = id_of(draft, schema) {
44                let new_url = base_url.join(url)?;
45                if let Some(x) = visitor(new_url.to_string(), schema) {
46                    return Ok(Some(x));
47                }
48                for (_k, v) in object {
49                    let result = find_ids(draft, v, &new_url, visitor)?;
50                    if result.is_some() {
51                        return Ok(result);
52                    }
53                }
54            } else {
55                for (_k, v) in object {
56                    let result = find_ids(draft, v, base_url, visitor)?;
57                    if result.is_some() {
58                        return Ok(result);
59                    }
60                }
61            }
62        }
63        Value::Array(array) => {
64            for v in array {
65                let result = find_ids(draft, v, base_url, visitor)?;
66                if result.is_some() {
67                    return Ok(result);
68                }
69            }
70        }
71        _ => {}
72    }
73    Ok(None)
74}
75
76impl<'a> Resolver<'a> {
77    pub fn from_schema(draft: Draft, schema: &'a Value) -> Result<Resolver<'a>, ValidationError> {
78        let base_url = match id_of(draft, schema) {
79            Some(url) => url.to_string(),
80            None => DOCUMENT_PROTOCOL.to_string(),
81        };
82
83        let mut id_mapping: HashMap<String, &'a Value> = HashMap::new();
84
85        find_ids(draft, schema, &url::Url::parse(&base_url)?, &mut |id, x| {
86            id_mapping.insert(id, x);
87            None
88        })?;
89
90        Ok(Resolver {
91            base_url,
92            id_mapping,
93        })
94    }
95
96    pub fn join_url(
97        &self,
98        draft: Draft,
99        url_ref: &str,
100        ctx: &Context,
101    ) -> Result<url::Url, ValidationError> {
102        let mut urls: Vec<&str> = vec![url_ref];
103        let mut frame = ctx;
104        loop {
105            if let Some(id) = id_of(draft, frame.x) {
106                urls.push(id);
107            }
108            match frame.parent {
109                Some(x) => frame = x,
110                None => break,
111            }
112        }
113        let base_url = url::Url::parse(&self.base_url)?;
114        let url = urls.iter().rev().try_fold(base_url, |x, y| x.join(y));
115        Ok(url?)
116    }
117
118    pub fn resolve_url(
119        &self,
120        url: &url::Url,
121        instance: &'a Value,
122    ) -> Result<&'a Value, ValidationError> {
123        let url_str = url.as_str();
124        match url_str {
125            DOCUMENT_PROTOCOL => Ok(instance),
126            _ => match schemas::draft_from_url(url_str) {
127                Some(value) => Ok(value.get_schema()),
128                _ => match self.id_mapping.get(url_str) {
129                    Some(value) => Ok(value),
130                    None => Err(ValidationError::new(
131                        &format!("Can't resolve url {}", url_str),
132                        None,
133                        None,
134                    )),
135                },
136            },
137        }
138    }
139
140    pub fn resolve_fragment(
141        &self,
142        draft: Draft,
143        url: &str,
144        ctx: &Context,
145        instance: &'a Value,
146    ) -> Result<(url::Url, &'a Value), ValidationError> {
147        let url = self.join_url(draft, url, ctx)?;
148        let mut resource = url.clone();
149        resource.set_fragment(None);
150        let fragment = percent_encoding::percent_decode(url.fragment().unwrap_or("").as_bytes())
151            .decode_utf8()
152            .unwrap();
153
154        if let Some(x) = find_ids(
155            draft,
156            instance,
157            &url::Url::parse(DOCUMENT_PROTOCOL)?,
158            &mut |id, x| {
159                if id == url.as_str() {
160                    Some(x)
161                } else {
162                    None
163                }
164            },
165        )? {
166            return Ok((resource, x));
167        }
168
169        let document = self.resolve_url(&resource, instance)?;
170
171        // TODO Prevent infinite reference recursion
172        match document.pointer(&fragment) {
173            Some(x) => Ok((resource, x)),
174            None => Err(ValidationError::new(
175                &format!("Couldn't resolve JSON pointer {}", url),
176                None,
177                None,
178            )),
179        }
180    }
181}