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}