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