load request/response body types, emit entire crate
This commit is contained in:
parent
4b0c2cec68
commit
dd73bac4c0
226
src/main.rs
226
src/main.rs
|
@ -1,12 +1,12 @@
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use openapiv3::OpenAPI;
|
use openapiv3::OpenAPI;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::{PathBuf, Path};
|
||||||
|
|
||||||
mod template;
|
mod template;
|
||||||
|
|
||||||
|
@ -249,6 +249,87 @@ trait ExtractJsonMediaType {
|
||||||
fn content_json(&self) -> Result<openapiv3::MediaType>;
|
fn content_json(&self) -> Result<openapiv3::MediaType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExtractJsonMediaType for openapiv3::Response {
|
||||||
|
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
||||||
|
if self.content.len() != 1 {
|
||||||
|
bail!("expected one content entry, found {}", self.content.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mt) = self.content.get("application/json") {
|
||||||
|
Ok(mt.clone())
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
"could not find application/json, only found {}",
|
||||||
|
self.content.keys().next().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_binary(&self) -> Result<bool> {
|
||||||
|
if self.content.len() == 0 {
|
||||||
|
/*
|
||||||
|
* XXX If there are no content types, I guess it is not binary?
|
||||||
|
*/
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.content.len() != 1 {
|
||||||
|
bail!("expected one content entry, found {}", self.content.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mt) = self.content.get("application/octet-stream") {
|
||||||
|
if !mt.encoding.is_empty() {
|
||||||
|
bail!("XXX encoding");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = &mt.schema {
|
||||||
|
use openapiv3::{
|
||||||
|
SchemaKind, StringFormat, Type,
|
||||||
|
VariantOrUnknownOrEmpty::Item,
|
||||||
|
};
|
||||||
|
|
||||||
|
let s = s.item()?;
|
||||||
|
if s.schema_data.nullable {
|
||||||
|
bail!("XXX nullable binary?");
|
||||||
|
}
|
||||||
|
if s.schema_data.default.is_some() {
|
||||||
|
bail!("XXX default binary?");
|
||||||
|
}
|
||||||
|
if s.schema_data.discriminator.is_some() {
|
||||||
|
bail!("XXX binary discriminator?");
|
||||||
|
}
|
||||||
|
match &s.schema_kind {
|
||||||
|
SchemaKind::Type(Type::String(st)) => {
|
||||||
|
if st.min_length.is_some() || st.max_length.is_some() {
|
||||||
|
bail!("binary min/max length");
|
||||||
|
}
|
||||||
|
if !matches!(st.format, Item(StringFormat::Binary)) {
|
||||||
|
bail!(
|
||||||
|
"expected binary format string, got {:?}",
|
||||||
|
st.format
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if st.pattern.is_some() {
|
||||||
|
bail!("XXX pattern");
|
||||||
|
}
|
||||||
|
if !st.enumeration.is_empty() {
|
||||||
|
bail!("XXX enumeration");
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
x => {
|
||||||
|
bail!("XXX schemakind type {:?}", x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("binary thing had no schema?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ExtractJsonMediaType for openapiv3::RequestBody {
|
impl ExtractJsonMediaType for openapiv3::RequestBody {
|
||||||
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
fn content_json(&self) -> Result<openapiv3::MediaType> {
|
||||||
if self.content.len() != 1 {
|
if self.content.len() != 1 {
|
||||||
|
@ -266,6 +347,13 @@ impl ExtractJsonMediaType for openapiv3::RequestBody {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_binary(&self) -> Result<bool> {
|
fn is_binary(&self) -> Result<bool> {
|
||||||
|
if self.content.len() == 0 {
|
||||||
|
/*
|
||||||
|
* XXX If there are no content types, I guess it is not binary?
|
||||||
|
*/
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
if self.content.len() != 1 {
|
if self.content.len() != 1 {
|
||||||
bail!("expected one content entry, found {}", self.content.len());
|
bail!("expected one content entry, found {}", self.content.len());
|
||||||
}
|
}
|
||||||
|
@ -873,7 +961,7 @@ fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result<String> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let oid = o.operation_id.as_deref().unwrap();
|
let oid = o.operation_id.as_deref().unwrap();
|
||||||
a(" /*");
|
a(" /**");
|
||||||
a(&format!(" * {}: {} {}", oid, m, p));
|
a(&format!(" * {}: {} {}", oid, m, p));
|
||||||
a(" */");
|
a(" */");
|
||||||
|
|
||||||
|
@ -885,7 +973,9 @@ fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result<String> {
|
||||||
bounds.push("B: Into<reqwest::Body>".to_string());
|
bounds.push("B: Into<reqwest::Body>".to_string());
|
||||||
(Some("B".to_string()), Some("body".to_string()))
|
(Some("B".to_string()), Some("body".to_string()))
|
||||||
} else {
|
} else {
|
||||||
let mt = b.content_json()?;
|
let mt = b
|
||||||
|
.content_json()
|
||||||
|
.with_context(|| anyhow!("{} {}", m, pn))?;
|
||||||
if !mt.encoding.is_empty() {
|
if !mt.encoding.is_empty() {
|
||||||
bail!("media type encoding not empty: {:#?}", mt);
|
bail!("media type encoding not empty: {:#?}", mt);
|
||||||
}
|
}
|
||||||
|
@ -1060,7 +1150,9 @@ fn main() -> Result<()> {
|
||||||
let mut opts = getopts::Options::new();
|
let mut opts = getopts::Options::new();
|
||||||
opts.parsing_style(getopts::ParsingStyle::StopAtFirstFree);
|
opts.parsing_style(getopts::ParsingStyle::StopAtFirstFree);
|
||||||
opts.reqopt("i", "", "OpenAPI definition document (JSON)", "INPUT");
|
opts.reqopt("i", "", "OpenAPI definition document (JSON)", "INPUT");
|
||||||
opts.reqopt("o", "", "Generated Rust output file", "OUTPUT");
|
opts.reqopt("o", "", "Generated Rust crate directory", "OUTPUT");
|
||||||
|
opts.reqopt("n", "", "Target Rust crate name", "CRATE");
|
||||||
|
opts.reqopt("v", "", "Target Rust crate version", "VERSION");
|
||||||
|
|
||||||
let args = match opts.parse(std::env::args().skip(1)) {
|
let args = match opts.parse(std::env::args().skip(1)) {
|
||||||
Ok(args) => {
|
Ok(args) => {
|
||||||
|
@ -1082,7 +1174,9 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
if let Some(components) = &api.components {
|
if let Some(components) = &api.components {
|
||||||
/*
|
/*
|
||||||
* First, grant each expected reference a type ID.
|
* First, grant each expected reference a type ID. Each
|
||||||
|
* "components.schemas" entry needs an established reference for
|
||||||
|
* resolution in this and other parts of the document.
|
||||||
*/
|
*/
|
||||||
for n in components.schemas.keys() {
|
for n in components.schemas.keys() {
|
||||||
println!("PREPOP {}:", n);
|
println!("PREPOP {}:", n);
|
||||||
|
@ -1090,24 +1184,132 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Populate a type to describe each entry in the schemas section:
|
||||||
|
*/
|
||||||
for (i, (sn, s)) in components.schemas.iter().enumerate() {
|
for (i, (sn, s)) in components.schemas.iter().enumerate() {
|
||||||
println!("SCHEMA {}/{}: {}", i + 1, components.schemas.len(), sn);
|
println!("SCHEMA {}/{}: {}", i + 1, components.schemas.len(), sn);
|
||||||
|
|
||||||
let id = ts.select(Some(sn.as_str()), s)?;
|
let id = ts.select(Some(sn.as_str()), s)?;
|
||||||
println!(" -> {:?}", id);
|
println!(" -> {:?}", id);
|
||||||
|
|
||||||
/*
|
|
||||||
* Each "components.schemas" entry needs an established reference
|
|
||||||
* for resolution in other parts of the document.
|
|
||||||
*/
|
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In addition to types defined in schemas, types may be defined inline in
|
||||||
|
* request and response bodies.
|
||||||
|
*/
|
||||||
|
for (pn, p) in api.paths.iter() {
|
||||||
|
let op = p.item()?;
|
||||||
|
|
||||||
|
let grab = |pn: &str,
|
||||||
|
m: &str,
|
||||||
|
o: Option<&openapiv3::Operation>,
|
||||||
|
ts: &mut TypeSpace|
|
||||||
|
-> Result<()> {
|
||||||
|
if let Some(o) = o {
|
||||||
|
/*
|
||||||
|
* Get the request body type, if this operation has one:
|
||||||
|
*/
|
||||||
|
match &o.request_body {
|
||||||
|
Some(openapiv3::ReferenceOr::Item(body)) => {
|
||||||
|
if !body.is_binary()? {
|
||||||
|
let mt =
|
||||||
|
body.content_json().with_context(|| {
|
||||||
|
anyhow!("{} {} request", m, pn)
|
||||||
|
})?;
|
||||||
|
if let Some(s) = &mt.schema {
|
||||||
|
let id = ts.select(None, s)?;
|
||||||
|
println!(
|
||||||
|
" {} {} request body -> {:?}",
|
||||||
|
pn, m, id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get the response body type for each status code:
|
||||||
|
*/
|
||||||
|
for (code, r) in o.responses.responses.iter() {
|
||||||
|
let ri = r.item()?;
|
||||||
|
if !ri.is_binary()? && !ri.content.is_empty() {
|
||||||
|
let mt = ri.content_json().with_context(|| {
|
||||||
|
anyhow!("{} {} {}", m, pn, code)
|
||||||
|
})?;
|
||||||
|
if let Some(s) = &mt.schema {
|
||||||
|
let id = ts.select(None, s)?;
|
||||||
|
println!(
|
||||||
|
" {} {} {} response body -> {:?}",
|
||||||
|
pn, m, code, id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
grab(pn, "GET", op.get.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "POST", op.post.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "PUT", op.put.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "DELETE", op.delete.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "OPTIONS", op.options.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "HEAD", op.head.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "PATCH", op.patch.as_ref(), &mut ts)?;
|
||||||
|
grab(pn, "TRACE", op.trace.as_ref(), &mut ts)?;
|
||||||
|
}
|
||||||
|
|
||||||
let fail = match gen(&api, &mut ts) {
|
let fail = match gen(&api, &mut ts) {
|
||||||
Ok(out) => {
|
Ok(out) => {
|
||||||
save(&args.opt_str("o").unwrap(), out.as_str())?;
|
let name = args.opt_str("n").unwrap();
|
||||||
|
let version = args.opt_str("v").unwrap();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create the top-level crate directory:
|
||||||
|
*/
|
||||||
|
let root = PathBuf::from(args.opt_str("o").unwrap());
|
||||||
|
std::fs::create_dir_all(&root)?;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Write the Cargo.toml file:
|
||||||
|
*/
|
||||||
|
let mut toml = root.clone();
|
||||||
|
toml.push("Cargo.toml");
|
||||||
|
let tomlout = format!(
|
||||||
|
"[package]\n\
|
||||||
|
name = \"{}\"\n\
|
||||||
|
version = \"{}\"\n\
|
||||||
|
edition = \"2018\"\n\
|
||||||
|
\n\
|
||||||
|
[dependencies]\n\
|
||||||
|
anyhow = \"1\"\n\
|
||||||
|
chrono = \"0.4\"\n\
|
||||||
|
percent-encoding = \"2.1\"\n\
|
||||||
|
reqwest = {{ version = \"0.11\", features = [\"json\"] }}\n\
|
||||||
|
serde = {{ version = \"1\", features = [\"derive\"] }}\n",
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
);
|
||||||
|
save(&toml, tomlout.as_str())?;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create the src/ directory:
|
||||||
|
*/
|
||||||
|
let mut src = root.clone();
|
||||||
|
src.push("src");
|
||||||
|
std::fs::create_dir_all(&src)?;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create the Rust source file containing the generated client:
|
||||||
|
*/
|
||||||
|
let mut librs = src.clone();
|
||||||
|
librs.push("lib.rs");
|
||||||
|
save(librs, out.as_str())?;
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
@ -25,8 +25,11 @@ impl Template {
|
||||||
out.push_str(" self.baseurl,\n");
|
out.push_str(" self.baseurl,\n");
|
||||||
for c in self.components.iter() {
|
for c in self.components.iter() {
|
||||||
if let Component::Parameter(n) = &c {
|
if let Component::Parameter(n) = &c {
|
||||||
out.push_str(&format!(" \
|
out.push_str(&format!(
|
||||||
progenitor_support::encode_path({}),\n", n));
|
" \
|
||||||
|
progenitor_support::encode_path(&{}.to_string()),\n",
|
||||||
|
n
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.push_str(" );\n");
|
out.push_str(" );\n");
|
||||||
|
|
Loading…
Reference in New Issue