Skip to main content

openapi_deref/
resolved.rs

1use serde_json::Value;
2
3use crate::error::{PartialResolveError, RefError};
4
5/// Result of resolving all `$ref` pointers in a JSON document.
6///
7/// Contains the resolved document alongside any non-fatal ref errors.
8///
9/// # Checking results
10///
11/// - [`is_complete()`](Self::is_complete) — `true` when zero errors
12/// - [`into_value()`](Self::into_value) — strict conversion to `Result<Value, _>`
13///
14/// # Convenience iterators
15///
16/// Inspect specific error categories **without** importing [`RefError`]:
17///
18/// - [`cycles()`](Self::cycles) — circular `$ref` strings
19/// - [`missing_refs()`](Self::missing_refs) — not-found `$ref` strings
20/// - [`external_refs()`](Self::external_refs) — unsupported external URI strings
21/// - [`sibling_keys_ignored()`](Self::sibling_keys_ignored) — sibling keys dropped (non-object target)
22#[derive(Debug, Clone)]
23pub struct ResolvedDoc {
24    /// The document with all resolvable `$ref` pointers expanded inline.
25    /// Unresolvable refs are preserved as raw `{"$ref": "..."}` objects.
26    pub value: Value,
27    /// Non-fatal ref errors encountered during resolution.
28    pub ref_errors: Vec<RefError>,
29}
30
31impl ResolvedDoc {
32    /// Returns `true` if all `$ref` pointers were successfully resolved.
33    pub fn is_complete(&self) -> bool {
34        self.ref_errors.is_empty()
35    }
36
37    /// Converts into the resolved [`Value`] if no ref errors were encountered.
38    ///
39    /// On failure, the returned [`PartialResolveError`] still provides access
40    /// to the partially-resolved document via its `value` field.
41    ///
42    /// # Example
43    ///
44    /// ```
45    /// # use serde_json::json;
46    /// # use openapi_deref::resolve;
47    /// let spec = json!({
48    ///     "components": { "schemas": { "X": { "type": "string" } } },
49    ///     "f": { "$ref": "#/components/schemas/X" }
50    /// });
51    /// let value = resolve(&spec).unwrap().into_value().unwrap();
52    /// assert_eq!(value["f"]["type"], "string");
53    /// ```
54    pub fn into_value(self) -> Result<Value, PartialResolveError> {
55        if self.ref_errors.is_empty() {
56            Ok(self.value)
57        } else {
58            Err(PartialResolveError {
59                value: self.value,
60                ref_errors: self.ref_errors,
61            })
62        }
63    }
64
65    /// Iterates over `$ref` strings that form circular references.
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// # use serde_json::json;
71    /// # use openapi_deref::resolve;
72    /// let spec = json!({
73    ///     "components": { "schemas": {
74    ///         "Node": { "properties": { "child": { "$ref": "#/components/schemas/Node" } } }
75    ///     }},
76    ///     "root": { "$ref": "#/components/schemas/Node" }
77    /// });
78    /// let doc = resolve(&spec).unwrap();
79    /// let cycles: Vec<&str> = doc.cycles().collect();
80    /// assert_eq!(cycles, ["#/components/schemas/Node"]);
81    /// ```
82    pub fn cycles(&self) -> impl Iterator<Item = &str> + '_ {
83        self.ref_errors.iter().filter_map(|e| match e {
84            RefError::Cycle { ref_str } => Some(ref_str.as_str()),
85            _ => None,
86        })
87    }
88
89    /// Iterates over external `$ref` strings (unsupported by this resolver).
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// # use serde_json::json;
95    /// # use openapi_deref::resolve;
96    /// let spec = json!({ "ext": { "$ref": "https://example.com/schema.json" } });
97    /// let doc = resolve(&spec).unwrap();
98    /// assert_eq!(doc.external_refs().next(), Some("https://example.com/schema.json"));
99    /// ```
100    pub fn external_refs(&self) -> impl Iterator<Item = &str> + '_ {
101        self.ref_errors.iter().filter_map(|e| match e {
102            RefError::External { ref_str } => Some(ref_str.as_str()),
103            _ => None,
104        })
105    }
106
107    /// Iterates over `$ref` strings where sibling keys were dropped because
108    /// the resolved target was not a JSON object.
109    ///
110    /// # Example
111    ///
112    /// ```
113    /// # use serde_json::json;
114    /// # use openapi_deref::resolve;
115    /// let spec = json!({
116    ///     "definitions": { "val": 42 },
117    ///     "target": { "$ref": "#/definitions/val", "description": "dropped" }
118    /// });
119    /// let doc = resolve(&spec).unwrap();
120    /// assert_eq!(doc.sibling_keys_ignored().next(), Some("#/definitions/val"));
121    /// ```
122    pub fn sibling_keys_ignored(&self) -> impl Iterator<Item = &str> + '_ {
123        self.ref_errors.iter().filter_map(|e| match e {
124            RefError::SiblingKeysIgnored { ref_str } => Some(ref_str.as_str()),
125            _ => None,
126        })
127    }
128
129    /// Iterates over `$ref` strings whose targets were not found.
130    ///
131    /// # Example
132    ///
133    /// ```
134    /// # use serde_json::json;
135    /// # use openapi_deref::resolve;
136    /// let spec = json!({ "broken": { "$ref": "#/does/not/exist" } });
137    /// let doc = resolve(&spec).unwrap();
138    /// assert_eq!(doc.missing_refs().next(), Some("#/does/not/exist"));
139    /// ```
140    pub fn missing_refs(&self) -> impl Iterator<Item = &str> + '_ {
141        self.ref_errors.iter().filter_map(|e| match e {
142            RefError::TargetNotFound { ref_str } => Some(ref_str.as_str()),
143            _ => None,
144        })
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use serde_json::json;
151
152    use crate::resolve;
153
154    #[test]
155    fn into_value_ok_on_complete_resolution() {
156        let spec = json!({
157            "components": { "schemas": { "Id": { "type": "integer" } } },
158            "field": { "$ref": "#/components/schemas/Id" }
159        });
160        let value = resolve(&spec).unwrap().into_value().unwrap();
161        assert_eq!(value["field"]["type"], "integer");
162    }
163
164    #[test]
165    fn into_value_err_preserves_partial_value() {
166        let spec = json!({
167            "schema": { "$ref": "#/missing" }
168        });
169        let err = resolve(&spec).unwrap().into_value().unwrap_err();
170
171        assert_eq!(err.ref_errors.len(), 1);
172        assert_eq!(err.value["schema"]["$ref"], "#/missing");
173    }
174
175    #[test]
176    fn resolved_doc_cycles_iterator() {
177        let spec = json!({
178            "components": {
179                "schemas": {
180                    "Node": {
181                        "type": "object",
182                        "properties": { "child": { "$ref": "#/components/schemas/Node" } }
183                    }
184                }
185            },
186            "root": { "$ref": "#/components/schemas/Node" }
187        });
188        let doc = resolve(&spec).unwrap();
189
190        let cycles: Vec<&str> = doc.cycles().collect();
191        assert_eq!(cycles, ["#/components/schemas/Node"]);
192        assert_eq!(doc.missing_refs().count(), 0);
193        assert_eq!(doc.external_refs().count(), 0);
194    }
195
196    #[test]
197    fn resolved_doc_missing_refs_iterator() {
198        let spec = json!({
199            "a": { "$ref": "#/missing_a" },
200            "b": { "$ref": "#/missing_b" }
201        });
202        let doc = resolve(&spec).unwrap();
203
204        let missing: Vec<&str> = doc.missing_refs().collect();
205        assert_eq!(missing.len(), 2);
206        assert!(missing.contains(&"#/missing_a"));
207        assert!(missing.contains(&"#/missing_b"));
208        assert_eq!(doc.cycles().count(), 0);
209    }
210
211    #[test]
212    fn resolved_doc_sibling_keys_ignored_iterator() {
213        let spec = json!({
214            "definitions": { "val": 42 },
215            "a": { "$ref": "#/definitions/val", "desc": "dropped" }
216        });
217        let doc = resolve(&spec).unwrap();
218
219        let ignored: Vec<&str> = doc.sibling_keys_ignored().collect();
220        assert_eq!(ignored, ["#/definitions/val"]);
221        assert_eq!(doc.cycles().count(), 0);
222        assert_eq!(doc.missing_refs().count(), 0);
223    }
224
225    #[test]
226    fn resolved_doc_external_refs_iterator() {
227        let spec = json!({
228            "a": { "$ref": "https://example.com/a.json" },
229            "b": { "$ref": "./local.json#/foo" }
230        });
231        let doc = resolve(&spec).unwrap();
232
233        let external: Vec<&str> = doc.external_refs().collect();
234        assert_eq!(external.len(), 2);
235        assert!(external.contains(&"https://example.com/a.json"));
236        assert!(external.contains(&"./local.json#/foo"));
237    }
238}