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