golem_openapi_client_generator/
merger.rs

1use indexmap::IndexMap;
2use openapiv3::{Components, ExternalDocumentation, OpenAPI, Paths};
3
4use crate::Error;
5use crate::Result;
6
7pub fn merge_all_openapi_specs(openapi_specs: Vec<OpenAPI>) -> Result<OpenAPI> {
8    if openapi_specs.is_empty() {
9        Err(Error::unexpected("No OpenAPI specs provided"))
10    } else if openapi_specs.len() == 1 {
11        Ok(openapi_specs.into_iter().next().unwrap())
12    } else {
13        let mut openapi_specs = openapi_specs;
14        let first = openapi_specs.pop().unwrap();
15        let rest = openapi_specs;
16        rest.into_iter().try_fold(first, merge_openapi_specs)
17    }
18}
19
20fn merge_openapi_specs(a: OpenAPI, b: OpenAPI) -> Result<OpenAPI> {
21    let openapi_version = {
22        if a.openapi != b.openapi {
23            return Err(Error::unexpected("OpenAPI versions do not match"));
24        }
25        a.openapi
26    };
27
28    let info = {
29        if a.info != b.info {
30            return Err(Error::unexpected("Info objects do not match"));
31        }
32        a.info
33    };
34
35    let servers = {
36        if a.servers != b.servers {
37            return Err(Error::unexpected("Servers do not match"));
38        }
39        a.servers
40    };
41
42    let all_tags = {
43        let a_tags_map = a
44            .tags
45            .into_iter()
46            .map(|tag| (tag.name.clone(), tag))
47            .collect::<IndexMap<_, _>>();
48        let b_tags_map = b
49            .tags
50            .into_iter()
51            .map(|tag| (tag.name.clone(), tag))
52            .collect::<IndexMap<_, _>>();
53        let merged = merge_unique(a_tags_map, b_tags_map)?;
54
55        merged.into_values().collect::<Vec<_>>()
56    };
57
58    let all_paths = {
59        let Paths {
60            paths: a_paths,
61            extensions: a_extensions,
62        } = a.paths;
63        let Paths {
64            paths: b_paths,
65            extensions: b_extensions,
66        } = b.paths;
67        let all_paths = merge_unique(a_paths, b_paths)?;
68        let all_extensions = merge_unique(a_extensions, b_extensions)?;
69        Paths {
70            paths: all_paths,
71            extensions: all_extensions,
72        }
73    };
74
75    let components = merge_components(a.components, b.components)?;
76    let security = merge_unique_option_list(a.security, b.security);
77    let extensions = merge_unique(a.extensions, b.extensions)?;
78
79    let external_docs = merge_external_docs(a.external_docs, b.external_docs)?;
80
81    let result = OpenAPI {
82        openapi: openapi_version,
83        info,
84        servers,
85        paths: all_paths,
86        components,
87        security,
88        tags: all_tags,
89        extensions,
90        external_docs,
91    };
92
93    Ok(result)
94}
95
96fn merge_components(a: Option<Components>, b: Option<Components>) -> Result<Option<Components>> {
97    let result = match (a, b) {
98        (Some(a), Some(b)) => {
99            let Components {
100                schemas: a_schemas,
101                responses: a_responses,
102                parameters: a_parameters,
103                examples: a_examples,
104                request_bodies: a_request_bodies,
105                headers: a_headers,
106                security_schemes: a_security_schemes,
107                links: a_links,
108                callbacks: a_callbacks,
109                extensions: a_extensions,
110            } = a;
111
112            let Components {
113                schemas: b_schemas,
114                responses: b_responses,
115                parameters: b_parameters,
116                examples: b_examples,
117                request_bodies: b_request_bodies,
118                headers: b_headers,
119                security_schemes: b_security_schemes,
120                links: b_links,
121                callbacks: b_callbacks,
122                extensions: b_extensions,
123            } = b;
124
125            let merged = Components {
126                schemas: merge_unique(a_schemas, b_schemas)?,
127                responses: merge_unique(a_responses, b_responses)?,
128                parameters: merge_unique(a_parameters, b_parameters)?,
129                examples: merge_unique(a_examples, b_examples)?,
130                request_bodies: merge_unique(a_request_bodies, b_request_bodies)?,
131                headers: merge_unique(a_headers, b_headers)?,
132                security_schemes: merge_unique(a_security_schemes, b_security_schemes)?,
133                links: merge_unique(a_links, b_links)?,
134                callbacks: merge_unique(a_callbacks, b_callbacks)?,
135                extensions: merge_unique(a_extensions, b_extensions)?,
136            };
137            Some(merged)
138        }
139        (Some(a), None) => Some(a),
140        (None, Some(b)) => Some(b),
141        (None, None) => None,
142    };
143
144    Ok(result)
145}
146
147fn merge_external_docs(
148    a: Option<ExternalDocumentation>,
149    b: Option<ExternalDocumentation>,
150) -> crate::Result<Option<ExternalDocumentation>> {
151    let result = match (a, b) {
152        (Some(a), Some(b)) => {
153            let ExternalDocumentation {
154                description: a_description,
155                url: a_url,
156                extensions: a_extensions,
157            } = a;
158
159            let ExternalDocumentation {
160                description: b_description,
161                url: b_url,
162                extensions: b_extensions,
163            } = b;
164
165            let description = match (a_description, b_description) {
166                (Some(a), Some(b)) => {
167                    if a != b {
168                        return Err(Error::unexpected(
169                            "External documentation descriptions do not match",
170                        ));
171                    }
172                    Some(a)
173                }
174                (Some(a), None) => Some(a),
175                (None, Some(b)) => Some(b),
176                (None, None) => None,
177            };
178
179            let url = {
180                if a_url != b_url {
181                    return Err(Error::unexpected(
182                        "External documentation URLs do not match",
183                    ));
184                }
185                a_url
186            };
187
188            let extensions = merge_unique(a_extensions, b_extensions)?;
189
190            Some(ExternalDocumentation {
191                description,
192                url,
193                extensions,
194            })
195        }
196        (Some(a), None) => Some(a),
197        (None, Some(b)) => Some(b),
198        (None, None) => None,
199    };
200    Ok(result)
201}
202
203fn merge_unique_option_list<Key, Item>(
204    a: Option<Vec<IndexMap<Key, Item>>>,
205    b: Option<Vec<IndexMap<Key, Item>>>,
206) -> Option<Vec<IndexMap<Key, Item>>> {
207    match (a, b) {
208        (Some(a), Some(mut b)) => {
209            let mut result = a;
210            result.append(&mut b);
211            Some(result)
212        }
213        (Some(a), None) => Some(a),
214        (None, Some(b)) => Some(b),
215        (None, None) => None,
216    }
217}
218
219fn merge_unique<Key, Item>(
220    mut a: IndexMap<Key, Item>,
221    b: IndexMap<Key, Item>,
222) -> Result<IndexMap<Key, Item>>
223where
224    Key: std::fmt::Debug + Eq + std::hash::Hash,
225    Item: std::fmt::Debug + PartialEq,
226{
227    for (key, value) in b {
228        match a.entry(key) {
229            indexmap::map::Entry::Occupied(entry) => {
230                if entry.get() != &value {
231                    return Err(Error::unexpected(format!(
232                        "Duplicate key {:?} with different values \n Current {:?} \n New {:?}",
233                        entry.key(),
234                        entry.get(),
235                        value
236                    )));
237                }
238            }
239            indexmap::map::Entry::Vacant(entry) => {
240                entry.insert(value);
241            }
242        }
243    }
244    Ok(a)
245}