diff --git a/Cargo.lock b/Cargo.lock index 4d0bc27..e2abe91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,180 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "openapiv3" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3c622a8249a19c1b9730e6b3127cf2b0d42f47fbdf2e5ba43c43cfd48eb5ee" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "serde_yaml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + [[package]] name = "progenitor" version = "0.0.0" +dependencies = [ + "anyhow", + "getopts", + "openapiv3", + "serde", + "serde_json", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "syn" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index b83dabd..99259e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,8 @@ repository = "https://github.com/oxidecomputer/progenitor.git" description = "An OpenAPI client generator" [dependencies] +anyhow = "1" +getopts = "0.2" +serde = { version = "1", features = [ "derive" ]} +serde_json = "1" +openapiv3 = "0.4" diff --git a/src/main.rs b/src/main.rs index e7a11a9..c3cfdba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,1040 @@ -fn main() { - println!("Hello, world!"); +#![allow(unused_imports)] + +use anyhow::{bail, 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; + +fn save

(p: P, data: &str) -> Result<()> +where + P: AsRef, +{ + 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: P) -> Result +where + P: AsRef, + 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) -> Result +where + P: AsRef, +{ + 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; +} + +impl ParameterDataExt for openapiv3::ParameterData { + fn render_type(&self) -> Result { + use openapiv3::{SchemaKind, Type}; + + Ok(match &self.format { + openapiv3::ParameterSchemaOrContent::Schema(s) => { + let s = s.item()?; + 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; + + use openapiv3::VariantOrUnknownOrEmpty::Unknown; + if let Unknown(f) = &it.format { + if f == "uint" { + uint = true; + } else { + 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 { + "u64".to_string() + } else { + "i64".to_string() + } + } + x => bail!("unexpected type {:#?}", x), + } + } + x => bail!("XXX param format {:#?}", x), + }) + } +} + +trait ExtractJsonMediaType { + fn is_binary(&self) -> Result; + fn content_json(&self) -> Result; +} + +impl ExtractJsonMediaType for openapiv3::RequestBody { + fn content_json(&self) -> Result { + 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 { + 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 { + fn item(&self) -> Result<&T>; +} + +impl ReferenceOrExt for openapiv3::ReferenceOr { + 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(HashMap), +} + +#[derive(Debug)] +struct TypeEntry { + id: TypeId, + name: Option, + details: TypeDetails, +} + +#[derive(Debug, Eq, Clone)] +struct TypeId(u64); + +impl PartialOrd for TypeId { + fn partial_cmp(&self, other: &TypeId) -> Option { + 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 + } +} + +#[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 without necesssarily having a useful distinct type name. + */ + name_to_id: BTreeMap, + id_to_entry: BTreeMap, + ref_to_id: BTreeMap, + + 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, + } + } + + /** + * 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::Unknown => { + format!("[UNKNOWN {}]", tid.0) + } + } + } else { + format!("[UNMAPPED {}]", tid.0) + } + } + + fn render_type(&self, tid: &TypeId) -> Result { + if let Some(te) = self.id_to_entry.get(&tid) { + match &te.details { + TypeDetails::Basic => { + if let Some(n) = &te.name { + Ok(n.to_string()) + } else { + bail!("basic type {:?} does not have a name?", tid); + } + } + TypeDetails::Array(itid) => { + if let Some(ite) = self.id_to_entry.get(&itid) { + if let Some(n) = &ite.name { + Ok(format!("Vec<{}>", n)) + } else { + bail!("array inner type {:?} no name?", ite); + } + } else { + bail!("array inner type {:?} missing?", itid); + } + } + TypeDetails::Optional(itid) => { + if let Some(ite) = self.id_to_entry.get(&itid) { + if let Some(n) = &ite.name { + Ok(format!("Option<{}>", n)) + } else { + bail!("optional inner type {:?} no name?", ite); + } + } else { + bail!("optional inner type {:?} missing?", itid); + } + } + TypeDetails::Object(_) => { + if let Some(n) = &te.name { + Ok(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 { + /* + * 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 { + 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 = HashMap::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".to_string()), + TypeDetails::Basic, + ) + } + Empty => { + (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, + ) -> Result { + 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>, + ) -> Result { + 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 { + 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. + */ + if ts.import_chrono { + a(""); + a("use chrono::prelude::*;"); + a(""); + } + + /* + * Declare named types we know about: + */ + for te in ts.id_to_entry.values() { + match &te.details { + TypeDetails::Object(omap) => { + a(&format!("pub struct {} {{", te.name.as_deref().unwrap())); + for (name, tid) in omap.iter() { + a(&format!(" pub {}: {},", name, ts.render_type(tid)?)); + } + a("}"); + a(""); + } + TypeDetails::Basic => {} + TypeDetails::Unknown => {} + TypeDetails::Array(_) => {} + TypeDetails::Optional(_) => {} + } + } + + /* + * Declare the client object: + */ + a("pub struct Client {"); + a("}"); + a(""); + + a("impl Client {"); + + /* + * 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 = Vec::new(); + + let body_param = if let Some(b) = &o.request_body { + let b = b.item()?; + if b.is_binary()? { + bounds.push("B: Into".to_string()); + Some("B".to_string()) + } else { + let mt = b.content_json()?; + 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)?)) + } else { + bail!("media type encoding, no schema: {:#?}", mt); + } + } + } else { + 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, + } => { + /* + * XXX Parameter types should probably go through + * the type space... + */ + let nam = ¶meter_data.name; + let typ = parameter_data.render_type()?; + 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"); + } + } + + /* + * XXX Parameter types should probably go through + * the type space... + */ + let nam = ¶meter_data.name; + let typ = parameter_data.render_type()?; + a(&format!(" {}: {},", nam, typ)); + } + 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)? + )); + } 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); + } + + a(" unimplemented!();"); + 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 output file", "OUTPUT"); + + 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. + */ + for n in components.schemas.keys() { + println!("PREPOP {}:", n); + ts.prepop_reference(n, &format!("#/components/schemas/{}", n))?; + } + println!(); + + 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); + + /* + * Each "components.schemas" entry needs an established reference + * for resolution in other parts of the document. + */ + + println!(); + } + } + + let fail = match gen(&api, &mut ts) { + Ok(out) => { + save(&args.opt_str("o").unwrap(), 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(()) }