Skip to content

Commit b4b9a3b

Browse files
committed
feature: multiple openapi specs as input
1 parent b20e559 commit b4b9a3b

File tree

3 files changed

+254
-9
lines changed

3 files changed

+254
-9
lines changed

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use openapiv3::OpenAPI;
1818
use std::path::Path;
1919
use std::result;
2020

21+
mod merger;
2122
pub(crate) mod printer;
2223
mod rust;
2324
mod toml;
@@ -54,7 +55,9 @@ impl Error {
5455

5556
pub type Result<T> = result::Result<T, Error>;
5657

57-
pub fn gen(open_api: OpenAPI, target: &Path, name: &str, version: &str) -> Result<()> {
58+
pub fn gen(openapi_specs: Vec<OpenAPI>, target: &Path, name: &str, version: &str) -> Result<()> {
59+
let open_api = merger::merge_all_openapi_specs(openapi_specs)?;
60+
5861
let src = target.join("src");
5962
let api = src.join("api");
6063
let model = src.join("model");

src/main.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use std::path::PathBuf;
2222
#[derive(Parser, Debug)]
2323
#[command(author, version, about, long_about = None, rename_all = "kebab-case")]
2424
struct Command {
25-
#[arg(short, long, value_name = "spec", value_hint = clap::ValueHint::FilePath)]
26-
spec_yaml: PathBuf,
25+
#[arg(short, long, value_name = "spec", value_hint = clap::ValueHint::FilePath, num_args = 1..)]
26+
spec_yaml: Vec<PathBuf>,
2727

2828
#[arg(short, long, value_name = "DIR", value_hint = clap::ValueHint::DirPath)]
2929
output_directory: PathBuf,
@@ -38,14 +38,20 @@ struct Command {
3838
fn main() {
3939
let command = Command::parse();
4040

41-
let file = File::open(command.spec_yaml).unwrap();
42-
43-
let reader = BufReader::new(file);
44-
45-
let openapi: OpenAPI = serde_yaml::from_reader(reader).expect("Could not deserialize input");
41+
let openapi_specs = command
42+
.spec_yaml
43+
.into_iter()
44+
.map(|spec| {
45+
let file = File::open(&spec).unwrap();
46+
let reader = BufReader::new(file);
47+
let openapi: OpenAPI = serde_yaml::from_reader(reader)
48+
.expect(format!("Could not deserialize input: {:?}", spec).as_str());
49+
openapi
50+
})
51+
.collect::<Vec<_>>();
4652

4753
gen(
48-
openapi,
54+
openapi_specs,
4955
&command.output_directory,
5056
&command.name,
5157
&command.client_version,

src/merger.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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

Comments
 (0)