diff --git a/Cargo.lock b/Cargo.lock
index 4ba87a4..326af4c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,10 +3,19 @@
version = 3
[[package]]
-name = "anyhow"
-version = "1.0.41"
+name = "aho-corasick"
+version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "autocfg"
@@ -14,12 +23,36 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
[[package]]
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
+[[package]]
+name = "dyn-clone"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
+
[[package]]
name = "getopts"
version = "0.2.21"
@@ -30,16 +63,36 @@ dependencies = [
]
[[package]]
-name = "hashbrown"
-version = "0.9.1"
+name = "getrandom"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "home"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
+dependencies = [
+ "winapi",
+]
[[package]]
name = "indexmap"
-version = "1.6.2"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
@@ -48,9 +101,21 @@ dependencies = [
[[package]]
name = "itoa"
-version = "0.4.7"
+version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "linked-hash-map"
@@ -59,10 +124,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
-name = "openapiv3"
-version = "1.0.0-beta.1"
+name = "memchr"
+version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abe6e1dfe8bc4931d6e90adbe2cec19e3fd67f7453d64e9a17cbf8dc16a67ec9"
+checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+
+[[package]]
+name = "openapiv3"
+version = "1.0.0-beta.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791f5f66415ca5d11fb29e8dcf0b923adff5b2c9bd1cf54621777494c8f8f688"
dependencies = [
"indexmap",
"serde",
@@ -71,10 +142,25 @@ dependencies = [
]
[[package]]
-name = "proc-macro2"
-version = "1.0.27"
+name = "pest"
+version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
dependencies = [
"unicode-xid",
]
@@ -85,9 +171,15 @@ version = "0.0.0"
dependencies = [
"anyhow",
"getopts",
+ "indexmap",
"openapiv3",
+ "proc-macro2",
+ "quote",
+ "rustfmt-wrapper",
+ "schemars",
"serde",
"serde_json",
+ "typify",
]
[[package]]
@@ -99,6 +191,91 @@ dependencies = [
"proc-macro2",
]
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rustfmt-wrapper"
+version = "0.1.0"
+source = "git+https://github.com/oxidecomputer/typify#825df8c1192f91f9edd27de0b81a858514d1865a"
+dependencies = [
+ "tempfile",
+ "thiserror",
+ "toolchain_find",
+]
+
[[package]]
name = "ryu"
version = "1.0.5"
@@ -106,19 +283,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
-name = "serde"
-version = "1.0.126"
+name = "same-file"
+version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a48d098c2a7fdf5740b19deb1181b4fb8a9e68e03ae517c14cde04b5725409"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a9ea2a613fe4cd7118b2bb101a25d8ae6192e1975179b67b2f17afd11e70ac8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn",
+]
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.126"
+version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
dependencies = [
"proc-macro2",
"quote",
@@ -127,9 +366,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.64"
+version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
+checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",
@@ -138,21 +377,21 @@ dependencies = [
[[package]]
name = "serde_yaml"
-version = "0.8.17"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
+checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
dependencies = [
"dtoa",
- "linked-hash-map",
+ "indexmap",
"serde",
"yaml-rust",
]
[[package]]
name = "syn"
-version = "1.0.73"
+version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
+checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
dependencies = [
"proc-macro2",
"quote",
@@ -160,10 +399,100 @@ dependencies = [
]
[[package]]
-name = "unicode-width"
-version = "0.1.8"
+name = "tempfile"
+version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toolchain_find"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e85654a10e7a07a47c6f19d93818f3f343e22927f2fa280c84f7c8042743413"
+dependencies = [
+ "home",
+ "lazy_static",
+ "regex",
+ "semver",
+ "walkdir",
+]
+
+[[package]]
+name = "typify"
+version = "0.0.1"
+source = "git+https://github.com/oxidecomputer/typify#825df8c1192f91f9edd27de0b81a858514d1865a"
+dependencies = [
+ "rustfmt-wrapper",
+ "typify-impl",
+ "typify-macro",
+]
+
+[[package]]
+name = "typify-impl"
+version = "0.0.1"
+source = "git+https://github.com/oxidecomputer/typify#825df8c1192f91f9edd27de0b81a858514d1865a"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "schemars",
+ "serde_json",
+ "syn",
+ "thiserror",
+]
+
+[[package]]
+name = "typify-macro"
+version = "0.0.1"
+source = "git+https://github.com/oxidecomputer/typify#825df8c1192f91f9edd27de0b81a858514d1865a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "schemars",
+ "serde_json",
+ "syn",
+ "typify-impl",
+]
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
@@ -171,6 +500,54 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
[[package]]
name = "yaml-rust"
version = "0.4.5"
diff --git a/Cargo.toml b/Cargo.toml
index cac6817..0821af4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,12 @@ description = "An OpenAPI client generator"
[dependencies]
anyhow = "1"
getopts = "0.2"
-serde = { version = "1", features = [ "derive" ]}
-serde_json = "1"
-openapiv3 = "1.0.0-beta.1"
+indexmap = "1.7.0"
+openapiv3 = "1.0.0-beta.2"
+proc-macro2 = "1.0.29"
+quote = "1.0.9"
+rustfmt-wrapper = { git = "https://github.com/oxidecomputer/typify" }
+schemars = "0.8.5"
+serde = { version = "1", features = [ "derive" ] }
+serde_json = "1.0.68"
+typify = { git = "https://github.com/oxidecomputer/typify" }
diff --git a/src/main.rs b/src/main.rs
index 46a64b9..01da9fa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,14 +2,22 @@
#![allow(clippy::single_match)]
use anyhow::{anyhow, bail, Context, Result};
-use openapiv3::OpenAPI;
+use openapiv3::{OpenAPI, ReferenceOr, Schema, SchemaData, SchemaKind};
use serde::Deserialize;
+use std::cell::Ref;
+use std::cmp::Ordering;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
+use quote::{format_ident, quote};
+use typify::TypeSpace;
+
+use crate::to_schema::ToSchema;
+
mod template;
+mod to_schema;
fn save
(p: P, data: &str) -> Result<()>
where
@@ -113,32 +121,44 @@ where
* 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);
+ }
- for o in item.iter() {
- 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.tags.is_empty() {
- bail!("op {}: tags, unsupported", oid);
- }
+ if !o.servers.is_empty() {
+ bail!("op {}: servers, unsupported", oid);
+ }
- if !o.servers.is_empty() {
- bail!("op {}: servers, unsupported", oid);
- }
+ if o.security.is_some() {
+ bail!("op {}: security, unsupported", oid);
+ }
- if o.security.is_some() {
- 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);
}
-
- 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);
@@ -471,780 +491,22 @@ impl PartialEq for TypeId {
}
}
-enum UseContext {
- Module,
- Return,
- Parameter,
+enum ParamType {
+ Path,
+ Query,
+ Body,
}
-#[allow(dead_code)]
-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 without necesssarily having a useful distinct type name.
- */
- name_to_id: BTreeMap,
- id_to_entry: BTreeMap,
- ref_to_id: BTreeMap,
-
- import_chrono: bool,
- import_uuid: 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,
- import_uuid: 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::Dictionary(itid) => {
- if let Some(ite) = self.id_to_entry.get(&itid) {
- if let Some(n) = &ite.name {
- return format!("dictionary of {} <{}>", n, itid.0);
- }
- }
-
- /*
- * If there is no name attached, we should try a
- * recursive describe.
- */
- format!("dictionary of {}", self.describe(itid))
- }
- 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 {
- 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::Dictionary(itid) => Ok(format!(
- "std::collections::HashMap",
- 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 {
- /*
- * 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) => {
- if at.items.is_none() {
- bail!("array items can't be none");
- }
- /*
- * Determine the type of item that will be in this array:
- */
- let itid =
- self.select_box(None, at.items.as_ref().unwrap())?;
- (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) => Some(n.to_string()),
- (None, Some(t)) => Some(t.to_string()),
- (Some(n), Some(t)) if n == t => Some(n.to_string()),
- (Some(n), Some(t)) => {
- bail!("names {} and {} conflict", n, t)
- }
- (None, None) => None,
- };
-
- if let Some(name) = name {
- 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))
- } else {
- /*
- * An object type without a name may be intended as a
- * map from strings to something else. If we also have
- * no properties, required or otherwise, and we have an
- * "additionalProperties" schema, we may be able to
- * represent this as a HashMap.
- */
- use openapiv3::AdditionalProperties::Schema;
-
- let tid = if o.properties.is_empty()
- && o.required.is_empty()
- {
- if let Some(Schema(s)) = &o.additional_properties {
- Some(self.select(None, &s)?)
- } else {
- None
- }
- } else {
- None
- };
-
- if let Some(tid) = tid {
- (None, TypeDetails::Dictionary(tid))
- } else {
- bail!("object types need a name? {:#?}", s);
- }
- }
- }
- openapiv3::Type::String(st) => {
- use openapiv3::{
- StringFormat::DateTime,
- VariantOrUnknownOrEmpty::{Empty, Item, Unknown},
- };
-
- match &st.format {
- Item(DateTime) => {
- self.import_chrono = true;
- (
- Some("DateTime".to_string()),
- TypeDetails::Basic,
- )
- }
- Unknown(x) if x.as_str() == "uuid" => {
- self.import_uuid = true;
- (Some("Uuid".to_string()), TypeDetails::Basic)
- }
- Empty => {
- use TypeDetails::{Enumeration, NewType};
-
- if !st.enumeration.is_empty() {
- if let Some(name) = name {
- if st.enumeration.contains(&None) {
- bail!(
- "null found in enumeration values"
- );
- }
- (
- Some(name.to_string()),
- Enumeration(
- st.enumeration
- .iter()
- .map(|value| {
- value.clone().unwrap()
- })
- .collect(),
- ),
- )
- } 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, st);
- }
- }
- }
- 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.
- */
- 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::Dictionary(_)
- | TypeDetails::Optional(_) => None,
- })
- .collect::>();
- ids.sort_by(|a, b| a.0.cmp(&b.0));
-
- a("pub mod types {");
- if ts.import_chrono {
- a(" use chrono::prelude::*;");
- }
- if ts.import_uuid {
- a(" use uuid::Uuid;");
- }
- 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::>();
- if name[0].is_ascii_alphabetic() {
- name[0] = name[0].to_ascii_uppercase();
- }
-
- a(
- &format!(
- " {},",
- name.iter().collect::(),
- ),
- );
- }
- 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.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();
+fn generate(api: &OpenAPI, ts: &mut TypeSpace) -> Result {
+ let methods = api
+ .operations()
+ .map(|(path, method, operation)| {
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".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,");
-
- /*
- * The order of parameters in the specification is effectively
- * arbitrary, and both path and query style parameters end up
- * mingled in the same list.
- */
- let mut parms = o
+ let mut raw_params = operation
.parameters
.iter()
- .map(|par| {
- match par.item()? {
+ .map(|parameter| {
+ match parameter.item()? {
openapiv3::Parameter::Path {
parameter_data,
style: openapiv3::PathStyle::Simple,
@@ -1254,12 +516,11 @@ fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result {
*/
assert!(parameter_data.required);
- let nam = ¶meter_data.name;
- let tid =
- ts.select(None, parameter_data.schema()?)?;
- let typ =
- ts.render_type(&tid, UseContext::Parameter)?;
- return Ok((true, nam, typ));
+ let nam = parameter_data.name.clone();
+ let schema = parameter_data.schema()?.to_schema();
+ let typ = ts.add_type(&schema)?;
+
+ Ok((ParamType::Path, nam, quote! { typ }))
}
openapiv3::Parameter::Query {
parameter_data,
@@ -1273,65 +534,93 @@ fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result {
}
}
- let nam = ¶meter_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)?;
+ let nam = parameter_data.name.clone();
+ let schema = parameter_data.schema()?.to_schema();
+ let mut typ = ts.add_type(&schema)?;
+ if !parameter_data.required {
+ typ = quote! { Option<#typ> };
+ }
query.push((
nam.to_string(),
!parameter_data.required,
));
- return Ok((false, nam, typ));
+ Ok((ParamType::Query, nam, quote! { typ }))
}
x => bail!("unhandled parameter type: {:#?}", x),
}
})
.collect::>>()?;
- /*
- * Deal first with path parameters, ordered by their position in the
- * path template string.
- */
- let tmp = template::parse(p)?;
- for pn in tmp.names() {
- let par = parms.iter().find(|p| p.0 && p.1 == &pn).unwrap();
- a(&format!(" {}: {},", par.1, par.2));
- }
+ let mut bounds = Vec::new();
- /*
- * Second, include query parameters, ordered by parameter name.
- */
- parms.sort_by(|a, b| a.1.cmp(&b.1));
- for par in parms.iter().filter(|p| !p.0) {
- a(&format!(" {}: {},", par.1, par.2));
- }
-
- /*
- * Include the body parameter, if there is one, last in the list:
- */
- if let Some(bp) = &body_param {
- a(&format!(" body: {},", bp));
- }
-
- let decode_response = 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);
- }
+ let (body_param, body_func) = if let Some(b) =
+ &operation.request_body
+ {
+ let b = b.item()?;
+ if b.is_binary()? {
+ bounds.push(quote! {B: Into});
+ (Some(quote! {B}), Some(quote! { .body(body) }))
+ } else {
+ let mt = b
+ .content_json()
+ .with_context(|| anyhow!("{} {}", method, path))?;
+ if !mt.encoding.is_empty() {
+ bail!("media type encoding not empty: {:#?}", mt);
}
- _ => bail!("code? {:#?}", only),
+
+ if let Some(s) = &mt.schema {
+ let schema = s.to_schema();
+ let typ = ts.add_type(&schema)?;
+ (Some(quote! { typ }), Some(quote! { .json(body) }))
+ } else {
+ bail!("media type encoding, no schema: {:#?}", mt);
+ }
+ }
+ } else {
+ (None, None)
+ };
+
+ if let Some(body) = body_param {
+ raw_params.push((ParamType::Body, "body".to_string(), body));
+ }
+
+ let tmp = template::parse(path)?;
+ let names = tmp.names();
+ let url_path = tmp.compile();
+
+ // Put parameters in a deterministic order.
+ raw_params.sort_by(|a, b| match (&a.0, &b.0) {
+ // Path params are first and are in positional order.
+ (ParamType::Path, ParamType::Path) => {
+ let aa = names.iter().position(|x| x == &a.1).unwrap();
+ let bb = names.iter().position(|x| x == &b.1).unwrap();
+ aa.cmp(&bb)
+ }
+ (ParamType::Path, ParamType::Query) => Ordering::Less,
+ (ParamType::Path, ParamType::Body) => Ordering::Less,
+
+ // Query params are in lexicographic order.
+ (ParamType::Query, ParamType::Body) => Ordering::Less,
+ (ParamType::Query, ParamType::Query) => a.1.cmp(&b.1),
+ (ParamType::Query, ParamType::Path) => Ordering::Greater,
+
+ // Body params are last and should be unique
+ (ParamType::Body, ParamType::Path) => Ordering::Greater,
+ (ParamType::Body, ParamType::Query) => Ordering::Greater,
+ (ParamType::Body, ParamType::Body) => {
+ panic!("should only be one body")
+ }
+ });
+
+ let (response_type, decode_response) = if operation
+ .responses
+ .responses
+ .len()
+ == 1
+ {
+ let only = operation.responses.responses.first().unwrap();
+ if !matches!(only.0, openapiv3::StatusCode::Code(200..=299)) {
+ bail!("code? {:#?}", only);
}
let i = only.1.item()?;
@@ -1342,29 +631,24 @@ fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result {
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<()> {");
- }
+ let typ = match (
+ i.content.len(),
+ i.content.get("application/json"),
+ ) {
+ (0, _) => quote! { () },
(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)?
- ));
+ if let Some(schema) = &mt.schema {
+ let schema = schema.to_schema();
+ ts.add_type(&schema)?
} else {
bail!("media type encoding, no schema: {:#?}", mt);
}
@@ -1375,88 +659,174 @@ fn gen(api: &OpenAPI, ts: &mut TypeSpace) -> Result {
(_, _) => {
bail!("too many response contents: {:#?}", i.content);
}
- }
- true
- } else if o.responses.responses.is_empty() {
- a(" ) -> Result {");
- false
+ };
+ (typ, quote! { res.json().await? })
+ } else if operation.responses.responses.is_empty() {
+ (quote! { reqwest::Response }, quote! { res })
} else {
- bail!("responses? {:#?}", o.responses);
+ bail!("responses? {:#?}", operation.responses);
};
- /*
- * Generate the URL for the request.
- */
- a(&tmp.compile());
+ let operation_id =
+ format_ident!("{}", operation.operation_id.as_deref().unwrap());
- /*
- * 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("");
-
- if decode_response {
- a(" Ok(res.json().await?)");
+ let bounds = if bounds.is_empty() {
+ quote! {}
} else {
- a(" Ok(res)");
- }
- a(" }");
- a("");
+ quote! {
+ < #(#bounds),* >
+ }
+ };
- Ok(())
- };
+ let params = raw_params.into_iter().map(|(_, name, typ)| {
+ let name = format_ident!("{}", name);
+ quote! {
+ #name: #typ
+ }
+ });
- 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())?;
+ let (query_build, query_use) = if query.is_empty() {
+ (quote! {}, quote! {})
+ } else {
+ let query_items = query.iter().map(|(qn, opt)| {
+ if *opt {
+ let qn_ident = format_ident!("{}", qn);
+ quote! {
+ if let Some(v) = & #qn_ident {
+ query.push((#qn, v.to_string()));
+ }
+ }
+ } else {
+ quote! {
+ query.push((#qn, #qn.to_string()));
+ }
+ }
+ });
+
+ let query_build = quote! {
+ let mut query = Vec::new();
+ #(#query_items)*
+ };
+ let query_use = quote! {
+ .query(&query)
+ };
+
+ (query_build, query_use)
+ };
+
+ let doc_comment = format!(
+ "{}: {} {}",
+ operation.operation_id.as_deref().unwrap(),
+ method.to_ascii_uppercase(),
+ path
+ );
+
+ let method_func = format_ident!("{}", method);
+
+ let method = quote! {
+ #[doc = #doc_comment]
+ pub async fn #operation_id #bounds (
+ &self,
+ #(#params),*
+ ) -> Result<#response_type> {
+ #url_path
+ #query_build
+
+ let res = self.client
+ . #method_func (url)
+ #body_func
+ #query_use
+ .send()
+ .await?
+ .error_for_status()?;
+
+ Ok(#decode_response)
+ }
+ };
+
+ Ok(method)
+ })
+ .collect::>>()?;
+
+ let mut types = ts
+ .iter_types()
+ .map(|type_entry| (type_entry.type_name(ts), type_entry.output(ts)))
+ .collect::>();
+ types.sort_by(|a, b| a.0.cmp(&b.0));
+ let types = types.into_iter().map(|(_, def)| def);
+
+ println!("-----------------------------------------------------");
+ println!(" TYPE SPACE");
+ println!("-----------------------------------------------------");
+ for (idx, type_entry) in ts.iter_types().enumerate() {
+ let n = type_entry.describe();
+ println!("{:>4} {}", idx, n);
}
+ println!("-----------------------------------------------------");
+ println!();
- a("}");
+ let file = quote! {
+ use anyhow::Result;
- Ok(out)
+ mod progenitor_support {
+ use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
+
+ #[allow(dead_code)]
+ const PATH_SET: &AsciiSet = &CONTROLS
+ .add(b' ')
+ .add(b'"')
+ .add(b'#')
+ .add(b'<')
+ .add(b'>')
+ .add(b'?')
+ .add(b'`')
+ .add(b'{')
+ .add(b'}');
+
+ #[allow(dead_code)]
+ pub(crate) fn encode_path(pc: &str) -> String {
+ utf8_percent_encode(pc, PATH_SET).to_string()
+ }
+ }
+
+ pub mod types {
+ use serde::{Deserialize, Serialize};
+ #(#types)*
+ }
+
+ pub struct Client {
+ baseurl: String,
+ client: reqwest::Client,
+ }
+
+ impl Client {
+ pub fn new(baseurl: &str) -> Client {
+ let dur = std::time::Duration::from_secs(15);
+ let client = reqwest::ClientBuilder::new()
+ .connect_timeout(dur)
+ .timeout(dur)
+ .build()
+ .unwrap();
+
+ Client::new_with_client(baseurl, client)
+ }
+
+ pub fn new_with_client(
+ baseurl: &str,
+ client: reqwest::Client,
+ ) -> Client {
+ Client {
+ baseurl: baseurl.to_string(),
+ client,
+ }
+ }
+
+ #(#methods)*
+ }
+ };
+ let file = rustfmt_wrapper::rustfmt(file).unwrap();
+
+ Ok(file)
}
fn main() -> Result<()> {
@@ -1483,101 +853,24 @@ fn main() -> Result<()> {
let api = load_api(&args.opt_str("i").unwrap())?;
- let mut ts = TypeSpace::new();
+ // Convert our components dictionary to schemars
+ let schemas = api
+ .components
+ .iter()
+ .flat_map(|components| {
+ components.schemas.iter().map(|(name, ref_or_schema)| {
+ (name.clone(), ref_or_schema.to_schema())
+ })
+ })
+ .collect::>();
- 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!();
+ // Create a new type space, prepopulated with our referenced schemas.
+ let mut ts = TypeSpace::default();
+ ts.set_type_mod("types");
- /*
- * 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);
+ ts.add_ref_types(schemas)?;
- 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.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 generate(&api, &mut ts) {
Ok(out) => {
let name = args.opt_str("n").unwrap();
let version = args.opt_str("v").unwrap();
@@ -1593,16 +886,21 @@ fn main() -> Result<()> {
*/
let mut toml = root.clone();
toml.push("Cargo.toml");
- let chrono = if ts.import_chrono {
+ let chrono = if ts.uses_chrono() {
"chrono = { version = \"0.4\", features = [\"serde\"] }\n"
} else {
""
};
- let uuid = if ts.import_uuid {
+ let uuid = if ts.uses_uuid() {
"uuid = { version = \"0.8\", features = [\"serde\", \"v4\"] }\n"
} else {
""
};
+ let serde_json = if ts.uses_uuid() {
+ "serde_json = \"1\"\n"
+ } else {
+ ""
+ };
let tomlout = format!(
"[package]\n\
name = \"{}\"\n\
@@ -1615,8 +913,9 @@ fn main() -> Result<()> {
{}\
percent-encoding = \"2.1\"\n\
reqwest = {{ version = \"0.11\", features = [\"json\"] }}\n\
- serde = {{ version = \"1\", features = [\"derive\"] }}\n",
- name, version, chrono, uuid,
+ serde = {{ version = \"1\", features = [\"derive\"] }}\n\
+ {}",
+ name, version, chrono, uuid, serde_json,
);
save(&toml, tomlout.as_str())?;
@@ -1641,16 +940,6 @@ fn main() -> Result<()> {
}
};
- 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");
}
diff --git a/src/template.rs b/src/template.rs
index 8d7ffab..5c64e02 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -1,4 +1,6 @@
use anyhow::{anyhow, bail, Context, Result};
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
#[derive(Eq, PartialEq, Clone, Debug)]
enum Component {
@@ -12,28 +14,31 @@ pub struct Template {
}
impl Template {
- pub fn compile(&self) -> String {
- let mut out = " let url = format!(\"{}".to_string();
+ pub fn compile(&self) -> TokenStream {
+ let mut fmt = String::new();
+ fmt.push_str("{}");
for c in self.components.iter() {
- out.push('/');
+ fmt.push('/');
match c {
- Component::Constant(n) => out.push_str(n),
- Component::Parameter(_) => out.push_str("{}"),
+ Component::Constant(n) => fmt.push_str(n),
+ Component::Parameter(_) => fmt.push_str("{}"),
}
}
- out.push_str("\",\n");
- out.push_str(" self.baseurl,\n");
- for c in self.components.iter() {
- if let Component::Parameter(n) = &c {
- out.push_str(&format!(
- " \
- progenitor_support::encode_path(&{}.to_string()),\n",
- n
- ));
+
+ let components = self.components.iter().filter_map(|component| {
+ if let Component::Parameter(n) = &component {
+ let param = format_ident!("{}", n);
+ Some(quote! {
+ progenitor_support::encode_path(param.to_string())
+ })
+ } else {
+ None
}
+ });
+
+ quote! {
+ let url = format!(#fmt, self.baseurl, #(#components,)*);
}
- out.push_str(" );\n");
- out
}
pub fn names(&self) -> Vec {
@@ -192,11 +197,13 @@ mod test {
fn compile() -> Result<()> {
let t = parse("/measure/{number}")?;
let out = t.compile();
- let want = " let url = format!(\"{}/measure/{}\",\
- \n self.baseurl,\
- \n progenitor_support::encode_path(&number.to_string()),\
- \n );\n";
- assert_eq!(want, &out);
+ let want = quote::quote! {
+ let url = format!("{}/measure/{}",
+ self.baseurl,
+ progenitor_support::encode_path(&number.to_string()),
+ );
+ };
+ assert_eq!(want.to_string(), out.to_string());
Ok(())
}
}
diff --git a/src/to_schema.rs b/src/to_schema.rs
new file mode 100644
index 0000000..6f2d48f
--- /dev/null
+++ b/src/to_schema.rs
@@ -0,0 +1,523 @@
+use indexmap::IndexMap;
+use openapiv3::AnySchema;
+use serde_json::Value;
+
+pub trait ToSchema {
+ fn to_schema(&self) -> schemars::schema::Schema;
+}
+
+trait Convert {
+ fn convert(&self) -> T;
+}
+
+impl ToSchema for openapiv3::Schema {
+ fn to_schema(&self) -> schemars::schema::Schema {
+ self.convert()
+ }
+}
+
+impl ToSchema for openapiv3::ReferenceOr {
+ fn to_schema(&self) -> schemars::schema::Schema {
+ self.convert()
+ }
+}
+
+impl Convert> for Vec
+where
+ I: Convert,
+{
+ fn convert(&self) -> Vec {
+ self.iter().map(Convert::convert).collect()
+ }
+}
+
+impl Convert