Skip to main content

openrpc_types/
resolver.rs

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/// A broken [`ReferenceOr::Reference`] was found.
10///
11/// Returned from [`resolve_within`].
12#[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
24/// Try and resolve all [`ReferenceOr`]s in the document by looking up
25/// references in [`OpenRPC::components`].
26///
27/// Returns a [`BrokenReference`] on the first [`ReferenceOr::Reference`] with no
28/// corresponding entry in [`OpenRPC::components`].
29///
30/// Note that unlike [`schemars::schema::SchemaObject::reference`]s, OpenRPC's
31/// references are acyclic, so this is guaranteed to terminate.
32pub 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                    // TODO(aatifsyed): https://github.com/open-rpc/spec/issues/389
56                    //                  update this when Components::methods is added
57                    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}