progenitor/src/main.rs

1545 lines
51 KiB
Rust

#![allow(unused_imports)]
use anyhow::{anyhow, bail, Context, Result};
use openapiv3::OpenAPI;
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
mod template;
fn save<P>(p: P, data: &str) -> Result<()>
where
P: AsRef<Path>,
{
let p = p.as_ref();
let mut f = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(p)?;
f.write_all(data.as_bytes())?;
f.flush()?;
Ok(())
}
fn load<P, T>(p: P) -> Result<T>
where
P: AsRef<Path>,
for<'de> T: Deserialize<'de>,
{
let p = p.as_ref();
let f = File::open(p)?;
Ok(serde_json::from_reader(f)?)
}
fn load_api<P>(p: P) -> Result<OpenAPI>
where
P: AsRef<Path>,
{
let api: OpenAPI = load(p)?;
if api.openapi != "3.0.3" {
/*
* XXX During development we are being very strict, but this should
* probably be relaxed.
*/
bail!("unexpected version {}", api.openapi);
}
if !api.servers.is_empty() {
bail!("servers not presently supported");
}
if !api.security.is_empty() {
bail!("security not presently supported");
}
if !api.tags.is_empty() {
bail!("tags not presently supported");
}
if let Some(components) = api.components.as_ref() {
if !components.security_schemes.is_empty() {
bail!("component security schemes not supported");
}
if !components.responses.is_empty() {
bail!("component responses not supported");
}
if !components.parameters.is_empty() {
bail!("component parameters not supported");
}
if !components.request_bodies.is_empty() {
bail!("component request bodies not supported");
}
if !components.headers.is_empty() {
bail!("component headers not supported");
}
if !components.links.is_empty() {
bail!("component links not supported");
}
if !components.callbacks.is_empty() {
bail!("component callbacks not supported");
}
/*
* XXX Ignoring "examples" and "extensions" for now.
* Explicitly allowing "schemas" through.
*/
}
/*
* XXX Ignoring "external_docs" and "extensions" for now, as they seem not
* to immediately affect our code generation.
*/
let mut opids = HashSet::new();
for p in api.paths.iter() {
match p.1 {
openapiv3::ReferenceOr::Reference { reference: _ } => {
bail!("path {} uses reference, unsupported", p.0);
}
openapiv3::ReferenceOr::Item(item) => {
/*
* Make sure every operation has an operation ID, and that each
* operation ID is only used once in the document.
*/
let mut id = |o: Option<&openapiv3::Operation>| -> Result<()> {
if let Some(o) = o {
if let Some(oid) = o.operation_id.as_ref() {
if !opids.insert(oid.to_string()) {
bail!("duplicate operation ID: {}", oid);
}
if !o.tags.is_empty() {
bail!("op {}: tags, unsupported", oid);
}
if !o.servers.is_empty() {
bail!("op {}: servers, unsupported", oid);
}
if !o.security.is_empty() {
bail!("op {}: security, unsupported", oid);
}
if o.responses.default.is_some() {
bail!("op {}: has response default", oid);
}
} else {
bail!("path {} is missing operation ID", p.0);
}
}
Ok(())
};
id(item.get.as_ref())?;
id(item.put.as_ref())?;
id(item.post.as_ref())?;
id(item.delete.as_ref())?;
id(item.options.as_ref())?;
id(item.head.as_ref())?;
id(item.patch.as_ref())?;
id(item.trace.as_ref())?;
if !item.servers.is_empty() {
bail!("path {} has servers; unsupported", p.0);
}
}
}
}
Ok(api)
}
trait ParameterDataExt {
fn render_type(&self) -> Result<String>;
fn schema(&self) -> Result<&openapiv3::ReferenceOr<openapiv3::Schema>>;
}
impl ParameterDataExt for openapiv3::ParameterData {
fn schema(&self) -> Result<&openapiv3::ReferenceOr<openapiv3::Schema>> {
match &self.format {
openapiv3::ParameterSchemaOrContent::Schema(s) => Ok(s),
x => bail!("XXX param format {:#?}", x),
}
}
fn render_type(&self) -> Result<String> {
use openapiv3::{SchemaKind, Type};
Ok(match &self.format {
openapiv3::ParameterSchemaOrContent::Schema(s) => {
let s = s.item().context("parameter data render type")?;
match &s.schema_kind {
SchemaKind::Type(Type::String(st)) => {
if !st.format.is_empty() {
bail!("XXX format");
}
if st.pattern.is_some() {
bail!("XXX pattern");
}
if !st.enumeration.is_empty() {
bail!("XXX enumeration");
}
if st.min_length.is_some() || st.max_length.is_some() {
bail!("XXX min/max length");
}
"&str".to_string()
}
SchemaKind::Type(Type::Integer(it)) => {
let mut uint;
let width;
use openapiv3::VariantOrUnknownOrEmpty::Unknown;
if let Unknown(f) = &it.format {
match f.as_str() {
"uint" | "uint32" => {
uint = true;
width = 32;
}
"uint64" => {
uint = true;
width = 32;
}
f => bail!("XXX unknown integer format {}", f),
}
} else {
bail!("XXX format {:?}", it.format);
}
if it.multiple_of.is_some() {
bail!("XXX multiple_of");
}
if it.exclusive_minimum || it.exclusive_maximum {
bail!("XXX exclusive");
}
if let Some(min) = it.minimum {
if min == 0 {
uint = true;
} else {
bail!("XXX invalid minimum: {}", min);
}
}
if it.maximum.is_some() {
bail!("XXX maximum");
}
if !it.enumeration.is_empty() {
bail!("XXX enumeration");
}
if uint {
format!("u{}", width)
} else {
format!("i{}", width)
}
}
x => bail!("unexpected type {:#?}", x),
}
}
x => bail!("XXX param format {:#?}", x),
})
}
}
trait ExtractJsonMediaType {
fn is_binary(&self) -> Result<bool>;
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 {
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)
}
}
trait ReferenceOrExt<T> {
fn item(&self) -> Result<&T>;
}
impl<T> ReferenceOrExt<T> for openapiv3::ReferenceOr<T> {
fn item(&self) -> Result<&T> {
match self {
openapiv3::ReferenceOr::Item(i) => Ok(i),
openapiv3::ReferenceOr::Reference { reference: _ } => {
bail!("reference not supported here");
}
}
}
}
#[allow(dead_code)]
#[derive(Debug, PartialEq)]
enum TypeDetails {
Unknown,
Basic,
Array(TypeId),
Optional(TypeId),
/*
* Object property names are sorted lexicographically to ensure a stable
* order in the generated code.
*/
Object(BTreeMap<String, TypeId>),
NewType(TypeId),
Enumeration(Vec<String>),
}
#[derive(Debug)]
struct TypeEntry {
id: TypeId,
name: Option<String>,
details: TypeDetails,
}
#[derive(Debug, Eq, Clone)]
struct TypeId(u64);
impl PartialOrd for TypeId {
fn partial_cmp(&self, other: &TypeId) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for TypeId {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.cmp(&other.0)
}
}
impl PartialEq for TypeId {
fn eq(&self, other: &TypeId) -> bool {
self.0 == other.0
}
}
enum UseContext {
Module,
Return,
Parameter,
}
impl UseContext {
fn is_module(&self) -> bool {
matches!(self, &UseContext::Module)
}
fn is_return(&self) -> bool {
matches!(self, &UseContext::Return)
}
fn is_parameter(&self) -> bool {
matches!(self, &UseContext::Parameter)
}
}
#[derive(Debug)]
struct TypeSpace {
next_id: u64,
/*
* Object types generally have a useful name, which we would like to match
* with anywhere that name appears in the definition document. Many other
* types, though, do not; e.g., an array of strings is just going to become
* Vec<String> without necesssarily having a useful distinct type name.
*/
name_to_id: BTreeMap<String, TypeId>,
id_to_entry: BTreeMap<TypeId, TypeEntry>,
ref_to_id: BTreeMap<String, TypeId>,
import_chrono: bool,
}
impl TypeSpace {
fn new() -> TypeSpace {
TypeSpace {
next_id: 1,
name_to_id: BTreeMap::new(),
id_to_entry: BTreeMap::new(),
ref_to_id: BTreeMap::new(),
import_chrono: false,
}
}
fn is_optional(&self, tid: &TypeId) -> bool {
if let Some(te) = self.id_to_entry.get(&tid) {
if let TypeDetails::Optional(_) = &te.details {
return true;
}
}
false
}
/**
* Emit a human-readable diagnostic description for this type ID.
*/
fn describe(&self, tid: &TypeId) -> String {
if let Some(te) = self.id_to_entry.get(&tid) {
match &te.details {
TypeDetails::Basic => {
if let Some(n) = &te.name {
n.to_string()
} else {
format!("[BASIC {} !NONAME?]", tid.0)
}
}
TypeDetails::Array(itid) => {
if let Some(ite) = self.id_to_entry.get(&itid) {
if let Some(n) = &ite.name {
return format!("array of {} <{}>", n, itid.0);
}
}
/*
* If there is no name attached, we should try a
* recursive describe.
*/
format!("array of {}", self.describe(itid))
}
TypeDetails::Optional(itid) => {
if let Some(ite) = self.id_to_entry.get(&itid) {
if let Some(n) = &ite.name {
return format!("option of {} <{}>", n, itid.0);
}
}
/*
* If there is no name attached, we should try a
* recursive describe.
*/
format!("option of {}", self.describe(itid))
}
TypeDetails::Object(_) => {
if let Some(n) = &te.name {
format!("object {}", n)
} else {
format!("[OBJECT {} !NONAME?]", tid.0)
}
}
TypeDetails::NewType(itid) => {
if let Some(ite) = self.id_to_entry.get(&itid) {
if let Some(n) = &ite.name {
return format!("newtype of {} <{}>", n, itid.0);
}
}
/*
* If there is no name attached, we should try a
* recursive describe.
*/
format!("newtype of {}", self.describe(itid))
}
TypeDetails::Enumeration(_) => {
if let Some(n) = &te.name {
format!("enum {}", n)
} else {
format!("[ENUMERATION {} !NONAME?]", tid.0)
}
}
TypeDetails::Unknown => {
format!("[UNKNOWN {}]", tid.0)
}
}
} else {
format!("[UNMAPPED {}]", tid.0)
}
}
fn render_type(&self, tid: &TypeId, ctx: UseContext) -> Result<String> {
let in_mod = ctx.is_module();
if let Some(te) = self.id_to_entry.get(&tid) {
match &te.details {
TypeDetails::Basic => {
if let Some(n) = &te.name {
Ok(if n == "String" && ctx.is_parameter() {
"&str"
} else {
n
}
.to_string())
} else {
bail!("basic type {:?} does not have a name?", tid);
}
}
TypeDetails::Array(itid) => {
Ok(format!("Vec<{}>", self.render_type(itid, ctx)?))
}
TypeDetails::Optional(itid) => {
Ok(format!("Option<{}>", self.render_type(itid, ctx)?))
}
TypeDetails::Object(_)
| TypeDetails::NewType(_)
| TypeDetails::Enumeration(_) => {
if let Some(n) = &te.name {
if in_mod {
Ok(n.to_string())
} else {
/*
* Model types are declared in the "types" module,
* and must be referenced with that prefix when not
* in the module itself.
*/
Ok(format!("types::{}", n.to_string()))
}
} else {
bail!("object type {:?} does not have a name?", tid);
}
}
TypeDetails::Unknown => {
bail!("type {:?} is unknown", tid);
}
}
} else {
bail!("could not resolve type ID {:?}", tid);
}
}
fn assign(&mut self) -> TypeId {
let id = TypeId(self.next_id);
self.next_id += 1;
id
}
fn id_for_name(&mut self, name: &str) -> TypeId {
let id = if let Some(id) = self.name_to_id.get(name) {
id.clone()
} else {
let id = self.assign();
self.name_to_id.insert(name.to_string(), id.clone());
id
};
id
}
fn id_for_optional(&mut self, want: &TypeId) -> TypeId {
for (oid, oent) in self.id_to_entry.iter() {
match &oent.details {
TypeDetails::Optional(id) if id == want => return oid.clone(),
_ => continue,
}
}
let oid = self.assign();
self.id_to_entry.insert(
oid.clone(),
TypeEntry {
id: oid.clone(),
name: None,
details: TypeDetails::Optional(want.clone()),
},
);
oid
}
fn prepop_reference(&mut self, name: &str, r: &str) -> Result<()> {
let id = self.id_for_name(name);
if let Some(rid) = self.ref_to_id.get(r) {
if rid != &id {
bail!(
"duplicate ref {:?}, name, {:?}, id {:?}, rid {:?}",
r,
name,
id,
rid
);
}
} else {
self.ref_to_id.insert(r.to_string(), id);
}
Ok(())
}
fn select_ref(&mut self, _name: Option<&str>, r: &str) -> Result<TypeId> {
/*
* As this is a reference, all we can do for now is determine
* the type ID.
*/
Ok(if let Some(id) = self.ref_to_id.get(r) {
id.clone()
} else {
let id = self.assign();
self.ref_to_id.insert(r.to_string(), id.clone());
id
})
}
fn select_schema(
&mut self,
name: Option<&str>,
s: &openapiv3::Schema,
) -> Result<TypeId> {
let (name, details) = match &s.schema_kind {
openapiv3::SchemaKind::Type(t) => match t {
openapiv3::Type::Array(at) => {
/*
* Determine the type of item that will be in this array:
*/
let itid = self.select_box(None, &at.items)?;
(None, TypeDetails::Array(itid))
}
openapiv3::Type::Object(o) => {
/*
* Object types must have a consistent name.
*/
let name = match (name, s.schema_data.title.as_deref()) {
(Some(n), None) => n.to_string(),
(None, Some(t)) => t.to_string(),
(Some(n), Some(t)) if n == t => n.to_string(),
(Some(n), Some(t)) => {
bail!("names {} and {} conflict", n, t)
}
(None, None) => bail!("types need a name? {:?}", s),
};
let mut omap = BTreeMap::new();
for (n, rb) in o.properties.iter() {
let itid = self.select_box(None, &rb)?;
if o.required.contains(n) {
omap.insert(n.to_string(), itid);
} else {
/*
* This is an optional member.
*/
omap.insert(
n.to_string(),
self.id_for_optional(&itid),
);
}
}
(Some(name), TypeDetails::Object(omap))
}
openapiv3::Type::String(st) => {
use openapiv3::{
StringFormat::DateTime,
VariantOrUnknownOrEmpty::{Empty, Item},
};
match &st.format {
Item(DateTime) => {
self.import_chrono = true;
(
Some("DateTime<Utc>".to_string()),
TypeDetails::Basic,
)
}
Empty => {
use TypeDetails::{Enumeration, NewType};
if !st.enumeration.is_empty() {
if let Some(name) = name {
(
Some(name.to_string()),
Enumeration(st.enumeration.clone()),
)
} else {
bail!("enumeration without name: {:?}", st);
}
} else if let Some(name) = name {
/*
* Create a newtype struct for strings that have
* a name of their own.
*/
let id = self.id_for_name("String");
(Some(name.to_string()), NewType(id))
} else {
(Some("String".to_string()), TypeDetails::Basic)
}
}
x => {
bail!("XXX string format {:?}", x);
}
}
}
openapiv3::Type::Boolean {} => {
(Some("bool".to_string()), TypeDetails::Basic)
}
openapiv3::Type::Number(_) => {
/*
* XXX
*/
(Some("f64".to_string()), TypeDetails::Basic)
}
openapiv3::Type::Integer(_) => {
/*
* XXX
*/
(Some("i64".to_string()), TypeDetails::Basic)
}
},
x => {
bail!("unhandled schema kind: {:?}", x);
}
};
if let Some(name) = &name {
/*
* First, determine what ID we will use to identify this named type.
*/
let id = self.id_for_name(name.as_str());
/*
* If there is already an entry for this type ID, ensure that it
* matches the entry we have constructed. If there is not yet an
* entry, we can just keep this one.
*/
if let Some(et) = self.id_to_entry.get(&id) {
if et.details != details {
bail!("{:?} != {:?}", et.details, details);
}
} else {
self.id_to_entry.insert(
id.clone(),
TypeEntry {
id: id.clone(),
name: Some(name.clone()),
details,
},
);
}
Ok(id)
} else {
/*
* If this type has no name, look for an existing unnamed type with
* the same shape.
*/
for (tid, te) in self.id_to_entry.iter() {
if te.name.is_none() && te.details == details {
return Ok(tid.clone());
}
}
/*
* Otherwise, insert a new entry.
*/
let tid = self.assign();
self.id_to_entry.insert(
tid.clone(),
TypeEntry {
id: tid.clone(),
name: None,
details,
},
);
Ok(tid)
}
}
fn select(
&mut self,
name: Option<&str>,
s: &openapiv3::ReferenceOr<openapiv3::Schema>,
) -> Result<TypeId> {
match s {
openapiv3::ReferenceOr::Reference { reference } => {
self.select_ref(name, reference.as_str())
}
openapiv3::ReferenceOr::Item(s) => self.select_schema(name, s),
}
}
fn select_box(
&mut self,
name: Option<&str>,
s: &openapiv3::ReferenceOr<Box<openapiv3::Schema>>,
) -> Result<TypeId> {
match s {
openapiv3::ReferenceOr::Reference { reference } => {
self.select_ref(name, reference.as_str())
}
openapiv3::ReferenceOr::Item(s) => {
self.select_schema(name, s.as_ref())
}
}
}
}
fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result<String> {
let mut out = String::new();
let mut a = |s: &str| {
out.push_str(s);
out.push('\n');
};
/*
* Deal with any dependencies we require to produce this client.
*/
a("");
a("use anyhow::Result;"); /* XXX */
a("");
a("mod progenitor_support {");
a(" use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};");
a("");
/*
* The percent-encoding crate abrogates its responsibility for providing
* useful percent-encoding sets, so we must provide one for path components
* here.
*/
a(" const PATH_SET: &AsciiSet = &CONTROLS");
/*
* The query percent-encode set is the C0 control percent-encode set and
* U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>).
*/
a(" .add(b' ')");
a(" .add(b'\"')");
a(" .add(b'#')");
a(" .add(b'<')");
a(" .add(b'>')");
/*
* The path percent-encode set is the query percent-encode set and U+003F
* (?), U+0060 (`), U+007B ({), and U+007D (}).
*/
a(" .add(b'?')");
a(" .add(b'`')");
a(" .add(b'{')");
a(" .add(b'}');");
a("");
a(" pub(crate) fn encode_path(pc: &str) -> String {");
a(" utf8_percent_encode(pc, PATH_SET).to_string()");
a(" }");
a("}");
a("");
/*
* Declare named types we know about. We want the declarations to appear in
* a stable order within the file, so we first collect a list of type IDs
* that we can sort by their visible name.
*/
let mut ids = ts.id_to_entry.values().filter_map(|te| match &te.details {
TypeDetails::Object(_)
| TypeDetails::NewType(_)
| TypeDetails::Enumeration(_) => {
Some((te.name.as_deref().unwrap(), &te.id))
}
TypeDetails::Basic
| TypeDetails::Unknown
| TypeDetails::Array(_)
| TypeDetails::Optional(_) => None,
}).collect::<Vec<_>>();
ids.sort_by(|a, b| a.0.cmp(&b.0));
a("pub mod types {");
if ts.import_chrono {
a(" use chrono::prelude::*;");
}
a(" use serde::{Serialize, Deserialize};");
a("");
for te in ids.iter().map(|x| ts.id_to_entry.get(x.1).unwrap()) {
match &te.details {
TypeDetails::Object(omap) => {
a(" #[derive(Serialize, Deserialize, Debug, Clone)]");
a(&format!(
" pub struct {} {{",
te.name.as_deref().unwrap()
));
for (name, tid) in omap.iter() {
if ts.is_optional(tid) {
/*
* Omit missing optionals from the document entirely; do
* not inject a "null" value.
*/
a(" #[serde(skip_serializing_if = \
\"Option::is_none\")]");
}
a(&format!(
" pub {}: {},",
name,
ts.render_type(tid, UseContext::Module)?
));
}
a(" }");
a("");
}
TypeDetails::NewType(tid) => {
let n = te.name.as_deref().unwrap();
a(" #[derive(Serialize, Deserialize, Debug, Clone)]");
a(&format!(
" pub struct {}({});",
n,
ts.render_type(tid, UseContext::Module)?,
));
a("");
/*
* Implement a basic Display trait so that to_string() works
* as well as it would have for the base type:
*/
a(&format!(" impl std::fmt::Display for {} {{", n));
a(" fn fmt(&self, f: &mut std::fmt::Formatter) -> \
std::fmt::Result {");
a(" write!(f, \"{}\", self.0)");
a(" }");
a(" }");
a("");
}
TypeDetails::Enumeration(list) => {
a(" #[derive(Serialize, Deserialize, Debug)]");
a(&format!(" pub enum {} {{", te.name.as_deref().unwrap()));
for name in list.iter() {
/*
* Attempt to make the first letter a capital letter.
*/
let mut name = name.chars().collect::<Vec<_>>();
if name[0].is_ascii_alphabetic() {
name[0] = name[0].to_ascii_uppercase();
}
a(
&format!(
" {},",
name.iter().collect::<String>(),
),
);
}
a(" }");
a("");
}
x => panic!("unexpected type details here: {:?}", x),
}
}
a("}");
a("");
/*
* Declare the client object:
*/
a("pub struct Client {");
a(" baseurl: String,");
a(" client: reqwest::Client,");
a("}");
a("");
a("impl Client {");
a(" pub fn new(baseurl: &str) -> Client {");
a(" let dur = std::time::Duration::from_secs(15);");
a(" let client = reqwest::ClientBuilder::new()");
a(" .connect_timeout(dur)");
a(" .timeout(dur)");
a(" .build()");
a(" .unwrap();");
a("");
a(" Client::new_with_client(baseurl, client)");
a(" }");
a("");
a(
" pub fn new_with_client(baseurl: &str, client: reqwest::Client) \
-> Client {",
);
a(" Client {");
a(" baseurl: baseurl.to_string(),");
a(" client,");
a(" }");
a(" }");
a("");
/*
* Generate a function for each Operation.
*
* XXX We should probably be producing an intermediate object for each of
* these, which can link in to the type space, instead of doing this inline
* here.
*/
for (pn, p) in api.paths.iter() {
let op = p.item()?;
let mut gen = |p: &str,
m: &str,
o: Option<&openapiv3::Operation>|
-> Result<()> {
let o = if let Some(o) = o {
o
} else {
return Ok(());
};
let oid = o.operation_id.as_deref().unwrap();
a(" /**");
a(&format!(" * {}: {} {}", oid, m, p));
a(" */");
let mut bounds: Vec<String> = Vec::new();
let mut query: Vec<(String, bool)> = Vec::new();
let (body_param, body_func) = if let Some(b) = &o.request_body {
let b = b.item()?;
if b.is_binary()? {
bounds.push("B: Into<reqwest::Body>".to_string());
(Some("B".to_string()), Some("body".to_string()))
} else {
let mt = b
.content_json()
.with_context(|| anyhow!("{} {}", m, pn))?;
if !mt.encoding.is_empty() {
bail!("media type encoding not empty: {:#?}", mt);
}
if let Some(s) = &mt.schema {
let tid = ts.select(None, s)?;
(
Some(format!(
"&{}",
ts.render_type(&tid, UseContext::Return)?
)),
Some("json".to_string()),
)
} else {
bail!("media type encoding, no schema: {:#?}", mt);
}
}
} else {
(None, None)
};
if bounds.is_empty() {
a(&format!(" pub async fn {}(", oid));
} else {
a(&format!(" pub async fn {}<{}>(", oid, bounds.join(", ")));
}
a(" &self,");
for par in o.parameters.iter() {
match par.item()? {
openapiv3::Parameter::Path {
parameter_data,
style: openapiv3::PathStyle::Simple,
} => {
/*
* Path parameters MUST be required.
*/
assert!(parameter_data.required);
let nam = &parameter_data.name;
let tid = ts.select(None, parameter_data.schema()?)?;
let typ =
ts.render_type(&tid, UseContext::Parameter)?;
a(&format!(" {}: {},", nam, typ));
}
openapiv3::Parameter::Query {
parameter_data,
allow_reserved: _,
style: openapiv3::QueryStyle::Form,
allow_empty_value,
} => {
if let Some(aev) = allow_empty_value {
if *aev {
bail!("allow empty value is a no go");
}
}
let nam = &parameter_data.name;
let tid = ts.select(None, parameter_data.schema()?)?;
let tid = if parameter_data.required {
tid
} else {
/*
* If this is an optional parameter, we need an
* Option version of the type.
*/
ts.id_for_optional(&tid)
};
let typ =
ts.render_type(&tid, UseContext::Parameter)?;
a(&format!(" {}: {},", nam, typ));
query.push((nam.to_string(), !parameter_data.required));
}
x => bail!("unhandled parameter type: {:#?}", x),
}
}
if let Some(bp) = &body_param {
a(&format!(" body: {},", bp));
}
// println!("{:#?}", o.responses);
if o.responses.responses.len() == 1 {
let only = o.responses.responses.iter().next().unwrap();
match only.0 {
openapiv3::StatusCode::Code(n) => {
if *n < 200 || *n > 299 {
bail!("code? {:#?}", only);
}
}
_ => bail!("code? {:#?}", only),
}
let i = only.1.item()?;
if !i.headers.is_empty() {
bail!("no response headers for now");
}
if !i.links.is_empty() {
bail!("no response links for now");
}
/*
* XXX ignoring extensions.
*/
/*
* Look at the response content. For now, support a single
* JSON-formatted response.
*/
match (i.content.len(), i.content.get("application/json")) {
(0, _) => {
a(" ) -> Result<()> {");
}
(1, Some(mt)) => {
if !mt.encoding.is_empty() {
bail!("media type encoding not empty: {:#?}", mt);
}
if let Some(s) = &mt.schema {
let tid = ts.select(None, s)?;
a(&format!(
" ) -> Result<{}> {{",
ts.render_type(&tid, UseContext::Return)?
));
} else {
bail!("media type encoding, no schema: {:#?}", mt);
}
}
(1, None) => {
bail!("response content not JSON: {:#?}", i.content);
}
(_, _) => {
bail!("too many response contents: {:#?}", i.content);
}
}
} else {
bail!("responses? {:#?}", o.responses);
}
/*
* Generate the URL for the request.
*/
let tmp = template::parse(p)?;
a(&tmp.compile());
/*
* If there is a query string, generate that now.
*/
if !query.is_empty() {
a(" let mut query = Vec::new();");
for (qn, opt) in query.iter() {
if *opt {
a(&format!(" if let Some(v) = &{} {{", qn));
a(&format!(
" \
query.push((\"{}\".to_string(), v.to_string()));",
qn
));
a(" }");
} else {
a(&format!(
" \
query.push((\"{}\".to_string(), {}.to_string()));",
qn, qn
));
}
}
a("");
}
/*
* Perform the request.
*/
a(&format!(
" let res = self.client.{}(url)",
m.to_lowercase()
));
if let Some(f) = &body_func {
a(&format!(" .{}(body)", f));
}
if !query.is_empty() {
a(" .query(&query)");
}
a(" .send()");
a(" .await?");
a(" .error_for_status()?;"); /* XXX */
a("");
a(" Ok(res.json().await?)");
a(" }");
a("");
Ok(())
};
gen(pn.as_str(), "GET", op.get.as_ref())?;
gen(pn.as_str(), "PUT", op.put.as_ref())?;
gen(pn.as_str(), "POST", op.post.as_ref())?;
gen(pn.as_str(), "DELETE", op.delete.as_ref())?;
gen(pn.as_str(), "OPTIONS", op.options.as_ref())?;
gen(pn.as_str(), "HEAD", op.head.as_ref())?;
gen(pn.as_str(), "PATCH", op.patch.as_ref())?;
gen(pn.as_str(), "TRACE", op.trace.as_ref())?;
}
a("}");
Ok(out)
}
fn main() -> Result<()> {
let mut opts = getopts::Options::new();
opts.parsing_style(getopts::ParsingStyle::StopAtFirstFree);
opts.reqopt("i", "", "OpenAPI definition document (JSON)", "INPUT");
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)) {
Ok(args) => {
if !args.free.is_empty() {
eprintln!("{}", opts.usage("progenitor"));
bail!("unexpected positional arguments");
}
args
}
Err(e) => {
eprintln!("{}", opts.usage("progenitor"));
bail!(e);
}
};
let api = load_api(&args.opt_str("i").unwrap())?;
let mut ts = TypeSpace::new();
if let Some(components) = &api.components {
/*
* 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() {
println!("PREPOP {}:", n);
ts.prepop_reference(n, &format!("#/components/schemas/{}", n))?;
}
println!();
/*
* Populate a type to describe each entry in the schemas section:
*/
for (i, (sn, s)) in components.schemas.iter().enumerate() {
println!("SCHEMA {}/{}: {}", i + 1, components.schemas.len(), sn);
let id = ts.select(Some(sn.as_str()), s)?;
println!(" -> {:?}", id);
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) {
Ok(out) => {
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 chrono = if ts.import_chrono {
"chrono = { version = \"0.4\", features = [\"serde\"] }\n"
} else {
""
};
let tomlout = format!(
"[package]\n\
name = \"{}\"\n\
version = \"{}\"\n\
edition = \"2018\"\n\
\n\
[dependencies]\n\
anyhow = \"1\"\n\
{}\
percent-encoding = \"2.1\"\n\
reqwest = {{ version = \"0.11\", features = [\"json\"] }}\n\
serde = {{ version = \"1\", features = [\"derive\"] }}\n",
name, version, chrono,
);
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
}
Err(e) => {
println!("gen fail: {:?}", e);
true
}
};
println!("-----------------------------------------------------");
println!(" TYPE SPACE");
println!("-----------------------------------------------------");
for te in ts.id_to_entry.values() {
let n = ts.describe(&te.id);
println!("{:>4} {}", te.id.0, n);
}
println!("-----------------------------------------------------");
println!();
if fail {
bail!("generation experienced errors");
}
Ok(())
}