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(())
}