Skip to content

Commit ba477ca

Browse files
authored
Merge pull request #7 from nicoburniske/master
Feature: Run client generation with multiple OpenAPI specs
2 parents b20e559 + 4e19ad8 commit ba477ca

File tree

3 files changed

+309
-18
lines changed

3 files changed

+309
-18
lines changed

src/lib.rs

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

21+
pub(crate) mod merger;
2122
pub(crate) mod printer;
2223
mod rust;
2324
mod toml;
2425

26+
pub use merger::merge_all_openapi_specs;
27+
2528
#[derive(Debug, Clone)]
2629
pub enum Error {
2730
Unexpected { message: String },
@@ -54,7 +57,9 @@ impl Error {
5457

5558
pub type Result<T> = result::Result<T, Error>;
5659

57-
pub fn gen(open_api: OpenAPI, target: &Path, name: &str, version: &str) -> Result<()> {
60+
pub fn gen(openapi_specs: Vec<OpenAPI>, target: &Path, name: &str, version: &str) -> Result<()> {
61+
let open_api = merge_all_openapi_specs(openapi_specs)?;
62+
5863
let src = target.join("src");
5964
let api = src.join("api");
6065
let model = src.join("model");

src/main.rs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use clap::Parser;
15+
use clap::{Args, Parser};
1616
use golem_openapi_client_generator::gen;
1717
use openapiv3::OpenAPI;
1818
use std::fs::File;
@@ -21,9 +21,17 @@ use std::path::PathBuf;
2121

2222
#[derive(Parser, Debug)]
2323
#[command(author, version, about, long_about = None, rename_all = "kebab-case")]
24-
struct Command {
25-
#[arg(short, long, value_name = "spec", value_hint = clap::ValueHint::FilePath)]
26-
spec_yaml: PathBuf,
24+
enum Cli {
25+
/// Generate a client from an OpenAPI spec
26+
Generate(GenerateArgs),
27+
/// Merge multiple OpenAPI specs into a single one
28+
Merge(MergeArgs),
29+
}
30+
31+
#[derive(Debug, Args)]
32+
struct GenerateArgs {
33+
#[arg(short, long, value_name = "spec", value_hint = clap::ValueHint::FilePath, num_args = 1.., required = true)]
34+
spec_yaml: Vec<PathBuf>,
2735

2836
#[arg(short, long, value_name = "DIR", value_hint = clap::ValueHint::DirPath)]
2937
output_directory: PathBuf,
@@ -35,20 +43,47 @@ struct Command {
3543
name: String,
3644
}
3745

38-
fn main() {
39-
let command = Command::parse();
40-
41-
let file = File::open(command.spec_yaml).unwrap();
46+
#[derive(Debug, Args)]
47+
struct MergeArgs {
48+
#[arg(short, long, value_name = "specs", value_hint = clap::ValueHint::FilePath, num_args = 1.., required = true)]
49+
spec_yaml: Vec<PathBuf>,
50+
#[arg(short, long, value_name = "output", value_hint = clap::ValueHint::FilePath)]
51+
output_yaml: PathBuf,
52+
}
4253

43-
let reader = BufReader::new(file);
54+
fn main() {
55+
let command = Cli::parse();
4456

45-
let openapi: OpenAPI = serde_yaml::from_reader(reader).expect("Could not deserialize input");
57+
match command {
58+
Cli::Generate(args) => {
59+
let openapi_specs = parse_openapi_specs(&args.spec_yaml);
60+
gen(
61+
openapi_specs,
62+
&args.output_directory,
63+
&args.name,
64+
&args.client_version,
65+
)
66+
.unwrap();
67+
}
68+
Cli::Merge(args) => {
69+
let openapi_specs = parse_openapi_specs(&args.spec_yaml);
70+
let openapi =
71+
golem_openapi_client_generator::merge_all_openapi_specs(openapi_specs).unwrap();
72+
let file = File::create(&args.output_yaml).unwrap();
73+
serde_yaml::to_writer(file, &openapi).unwrap();
74+
}
75+
}
76+
}
4677

47-
gen(
48-
openapi,
49-
&command.output_directory,
50-
&command.name,
51-
&command.client_version,
52-
)
53-
.unwrap();
78+
fn parse_openapi_specs(spec: &Vec<PathBuf>) -> Vec<OpenAPI> {
79+
spec.into_iter()
80+
.map(|spec| {
81+
let file =
82+
File::open(&spec).expect(format!("Could not open file: {:?}", spec).as_str());
83+
let reader = BufReader::new(file);
84+
let openapi: OpenAPI = serde_yaml::from_reader(reader)
85+
.expect(format!("Could not deserialize input: {:?}", spec).as_str());
86+
openapi
87+
})
88+
.collect::<Vec<_>>()
5489
}

src/merger.rs

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

Comments
 (0)