1use core::fmt;
2use std::collections::BTreeMap;
3
4use crate::{
5 resolved, Components, ContentDescriptor, Error, Example, ExamplePairing, Method, OpenRPC,
6 ReferenceOr, Tag,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct BrokenReference(pub String);
14
15impl fmt::Display for BrokenReference {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 f.write_str("failed to resolve the following `$ref`: ")?;
18 f.write_str(self.0.as_str())
19 }
20}
21
22impl std::error::Error for BrokenReference {}
23
24pub fn resolve_within(openrpc: OpenRPC) -> Result<resolved::OpenRPC, BrokenReference> {
33 self::openrpc(openrpc).map_err(BrokenReference)
34}
35
36fn openrpc(
37 OpenRPC {
38 openrpc,
39 info,
40 servers,
41 methods,
42 components,
43 external_docs,
44 extensions,
45 }: OpenRPC,
46) -> Result<resolved::OpenRPC, String> {
47 Ok(resolved::OpenRPC {
48 openrpc,
49 info,
50 servers,
51 methods: methods
52 .into_iter()
53 .map(|it| {
54 match it {
55 ReferenceOr::Reference(it) => Err(it),
58 ReferenceOr::Item(it) => Ok(it),
59 }
60 .and_then(|it| method(components.as_ref(), it))
61 })
62 .collect::<Result<_, _>>()?,
63 components,
64 external_docs,
65 extensions,
66 })
67}
68
69fn method(
70 components: Option<&Components>,
71 Method {
72 name,
73 tags,
74 summary,
75 description,
76 external_docs,
77 params,
78 result,
79 deprecated,
80 servers,
81 errors,
82 param_structure,
83 examples,
84 extensions,
85 }: Method,
86) -> Result<resolved::Method, String> {
87 Ok(resolved::Method {
88 name,
89 tags: tags
90 .map(|it| {
91 it.into_iter()
92 .map(|it| resolve(components, it))
93 .collect::<Result<_, _>>()
94 })
95 .transpose()?,
96 summary,
97 description,
98 external_docs,
99 params: params
100 .into_iter()
101 .map(|it| resolve(components, it))
102 .collect::<Result<_, _>>()?,
103 result: result.map(|it| resolve(components, it)).transpose()?,
104 deprecated,
105 servers,
106 errors: errors
107 .map(|it| {
108 it.into_iter()
109 .map(|it| resolve(components, it))
110 .collect::<Result<_, _>>()
111 })
112 .transpose()?,
113 param_structure,
114 examples: examples
115 .map(|it| {
116 it.into_iter()
117 .map(|it| {
118 resolve(components, it).and_then(|it| example_pairing(components, it))
119 })
120 .collect::<Result<_, _>>()
121 })
122 .transpose()?,
123 extensions,
124 })
125}
126
127fn example_pairing(
128 components: Option<&Components>,
129 ExamplePairing {
130 name,
131 description,
132 summary,
133 params,
134 result,
135 extensions,
136 }: ExamplePairing,
137) -> Result<resolved::ExamplePairing, String> {
138 Ok(resolved::ExamplePairing {
139 name,
140 description,
141 summary,
142 params: params
143 .into_iter()
144 .map(|it| resolve(components, it))
145 .collect::<Result<_, _>>()?,
146 result: result.map(|it| resolve(components, it)).transpose()?,
147 extensions,
148 })
149}
150
151trait Resolvable: Sized {
152 const KEY: &'static str;
153 fn get(components: &Components) -> Option<&BTreeMap<String, Self>>;
154}
155macro_rules! impl_resolvable {
156 ($($ty:ty = $key:literal / $field:ident);* $(;)?) => {
157 $(
158 impl Resolvable for $ty {
159 const KEY: &'static str = $key;
160 fn get(components: &Components) -> Option<&BTreeMap<String, Self>> {
161 components.$field.as_ref()
162 }
163 }
164 )*
165 };
166}
167impl_resolvable! {
168 ContentDescriptor = "contentDescriptors" / content_descriptors;
169 Error = "errors" / errors;
170 Example = "examples" / examples;
171 ExamplePairing = "examplePairingObjects" / example_pairing_objects;
172 Tag = "tags" / tags
173}
174fn resolve<T: Resolvable + Clone>(
175 components: Option<&Components>,
176 refr: ReferenceOr<T>,
177) -> Result<T, String> {
178 match refr {
179 ReferenceOr::Reference(it) => {
180 let Some(key) = it
181 .strip_prefix("#/components/")
182 .and_then(|it| it.strip_prefix(T::KEY))
183 .and_then(|it| it.strip_prefix('/'))
184 else {
185 return Err(it);
186 };
187 components
188 .and_then(T::get)
189 .and_then(|it| it.get(key))
190 .cloned()
191 .ok_or(it)
192 }
193 ReferenceOr::Item(it) => Ok(it),
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use expect_test::{expect, Expect};
201 use serde_json::{json, Value};
202
203 #[track_caller]
204 fn do_test(before: Value, after: Expect) {
205 after.assert_eq(
206 &serde_json::to_string_pretty(
207 &resolve_within(serde_json::from_value(before).expect("invalid `before` document"))
208 .expect("resolution failed"),
209 )
210 .unwrap(),
211 );
212 }
213
214 #[test]
215 fn test() {
216 do_test(
217 json!(
218 {
219 "openrpc": "1.0.0",
220 "info": {
221 "title": "",
222 "version": ""
223 },
224 "methods": [
225 {
226 "name": "fooMethod",
227 "params": [
228 {
229 "$ref": "#/components/contentDescriptors/fooParam"
230 }
231 ],
232 }
233 ],
234 "components": {
235 "contentDescriptors": {
236 "fooParam": {
237 "name": "fooParam0",
238 "schema": true
239 }
240 }
241 }
242 }
243 ),
244 expect![[r#"
245 {
246 "openrpc": "1.0.0",
247 "info": {
248 "title": "",
249 "version": ""
250 },
251 "methods": [
252 {
253 "name": "fooMethod",
254 "params": [
255 {
256 "name": "fooParam0",
257 "schema": true
258 }
259 ]
260 }
261 ],
262 "components": {
263 "contentDescriptors": {
264 "fooParam": {
265 "name": "fooParam0",
266 "schema": true
267 }
268 }
269 }
270 }"#]],
271 );
272 }
273}