From 98d5f1b0b789509ce2546a16796c1997fcc18b53 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Tue, 14 May 2024 14:54:03 -0700 Subject: [PATCH] pass through typify `x-rust-type` settings (#804) --- Cargo.lock | 79 +++++++++++---------- cargo-progenitor/Cargo.toml | 2 +- progenitor-impl/src/lib.rs | 54 +++++++++++++++ progenitor-macro/src/lib.rs | 135 ++++++++++++++++++++++++++++++++++-- 4 files changed, 226 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3951f65..06e71f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,7 +132,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -143,7 +143,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -340,7 +340,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -533,7 +533,7 @@ dependencies = [ "quote", "serde", "serde_tokenstream", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -610,7 +610,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.60", + "syn 2.0.63", "uuid", ] @@ -638,7 +638,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.60", + "syn 2.0.63", "uuid", "wasm-bindgen-test", ] @@ -746,7 +746,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1381,14 +1381,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -1450,7 +1450,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.60", + "syn 2.0.63", "thiserror", "tokio", "typify", @@ -1470,7 +1470,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1777,7 +1777,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1817,31 +1817,31 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.16" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.201" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1852,7 +1852,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -1894,7 +1894,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2066,9 +2066,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" dependencies = [ "proc-macro2", "quote", @@ -2158,7 +2158,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2243,7 +2243,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2392,8 +2392,8 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "typify" -version = "0.0.16" -source = "git+https://github.com/oxidecomputer/typify#08a53a7ef9da9b81838002332fa414d433f10067" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/typify#ad1296f6ceb998ae8c247d999b7828703a232bdd" dependencies = [ "typify-impl", "typify-macro", @@ -2401,8 +2401,8 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.0.16" -source = "git+https://github.com/oxidecomputer/typify#08a53a7ef9da9b81838002332fa414d433f10067" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/typify#ad1296f6ceb998ae8c247d999b7828703a232bdd" dependencies = [ "heck", "log", @@ -2410,24 +2410,27 @@ dependencies = [ "quote", "regress", "schemars", + "semver", + "serde", "serde_json", - "syn 2.0.60", + "syn 2.0.63", "thiserror", "unicode-ident", ] [[package]] name = "typify-macro" -version = "0.0.16" -source = "git+https://github.com/oxidecomputer/typify#08a53a7ef9da9b81838002332fa414d433f10067" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/typify#ad1296f6ceb998ae8c247d999b7828703a232bdd" dependencies = [ "proc-macro2", "quote", "schemars", + "semver", "serde", "serde_json", "serde_tokenstream", - "syn 2.0.60", + "syn 2.0.63", "typify-impl", ] @@ -2574,7 +2577,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", "wasm-bindgen-shared", ] @@ -2608,7 +2611,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2641,7 +2644,7 @@ checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] @@ -2975,7 +2978,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.63", ] [[package]] diff --git a/cargo-progenitor/Cargo.toml b/cargo-progenitor/Cargo.toml index 353d13c..5e9b7b9 100644 --- a/cargo-progenitor/Cargo.toml +++ b/cargo-progenitor/Cargo.toml @@ -25,5 +25,5 @@ serde_json = "1.0" serde_yaml = "0.9" [build-dependencies] -built = { version = "0.7", features = ["cargo-lock", "git2"] } +built = { version = "0.7.2", features = ["cargo-lock", "git2"] } project-root = "0.2" diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index b59e256..7226c52 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -15,8 +15,10 @@ use typify::{TypeSpace, TypeSpaceSettings}; use crate::to_schema::ToSchema; +pub use typify::CrateVers; pub use typify::TypeSpaceImpl as TypeImpl; pub use typify::TypeSpacePatch as TypePatch; +pub use typify::UnknownPolicy; mod cli; mod httpmock; @@ -64,11 +66,20 @@ pub struct GenerationSettings { post_hook: Option, extra_derives: Vec, + unknown_crates: UnknownPolicy, + crates: BTreeMap, + patch: HashMap, replace: HashMap)>, convert: Vec<(schemars::schema::SchemaObject, String, Vec)>, } +#[derive(Debug, Clone)] +struct CrateSpec { + version: CrateVers, + rename: Option, +} + /// Style of generated client. #[derive(Clone, Deserialize, PartialEq, Eq)] pub enum InterfaceStyle { @@ -190,6 +201,34 @@ impl GenerationSettings { .push((schema, type_name.to_string(), impls.collect())); self } + + /// Policy regarding crates referenced by the schema extension + /// `x-rust-type` not explicitly specified via [Self::with_crate]. + /// See [typify::TypeSpaceSettings::with_unknown_crates]. + pub fn with_unknown_crates(&mut self, policy: UnknownPolicy) -> &mut Self { + self.unknown_crates = policy; + self + } + + /// Explicitly named crates whose types may be used during generation + /// rather than generating new types based on their schemas (base on the + /// presence of the x-rust-type extension). + /// See [typify::TypeSpaceSettings::with_crate]. + pub fn with_crate( + &mut self, + crate_name: S1, + version: CrateVers, + rename: Option<&String>, + ) -> &mut Self { + self.crates.insert( + crate_name.to_string(), + CrateSpec { + version, + rename: rename.cloned(), + }, + ); + self + } } impl Default for Generator { @@ -215,6 +254,20 @@ impl Generator { settings.extra_derives.iter().for_each(|derive| { let _ = type_settings.with_derive(derive.clone()); }); + + // Control use of crates found in x-rust-type extension + type_settings.with_unknown_crates(settings.unknown_crates); + settings.crates.iter().for_each( + |(crate_name, CrateSpec { version, rename })| { + type_settings.with_crate( + crate_name, + version.clone(), + rename.as_ref(), + ); + }, + ); + + // Adjust generation by type, name, or schema. settings.patch.iter().for_each(|(type_name, patch)| { type_settings.with_patch(type_name, patch); }); @@ -237,6 +290,7 @@ impl Generator { impls.iter().cloned(), ); }); + Self { type_space: TypeSpace::new(&type_settings), settings: settings.clone(), diff --git a/progenitor-macro/src/lib.rs b/progenitor-macro/src/lib.rs index 5c769e1..60111f3 100644 --- a/progenitor-macro/src/lib.rs +++ b/progenitor-macro/src/lib.rs @@ -14,7 +14,8 @@ use std::{ use openapiv3::OpenAPI; use proc_macro::TokenStream; use progenitor_impl::{ - GenerationSettings, Generator, InterfaceStyle, TagStyle, TypePatch, + CrateVers, GenerationSettings, Generator, InterfaceStyle, TagStyle, + TypePatch, UnknownPolicy, }; use quote::{quote, ToTokens}; use schemars::schema::SchemaObject; @@ -41,12 +42,21 @@ mod token_utils; /// [ tags = ( Merged | Separate ), ] /// [ pre_hook = closure::or::path::to::function, ] /// [ post_hook = closure::or::path::to::function, ] +/// /// [ derives = [ path::to::DeriveMacro ], ] +/// +/// [ unknown_crates = (Generate | Allow | Deny ), ] +/// [ crates = { "" = ("" | "*" | "!" ) } ] +/// +/// [ patch = { TypeName = { [rename = NewTypeName], [derives = []] }, } ] +/// [ replace = { TypeName = full_path::to::other::TypeName, }] +/// [ convert = { { } = full_path::to::TypeName, }] +/// /// ); /// ``` /// -/// The `spec` key is required; it is the OpenAPI document (JSON or YAML) from which the -/// client is derived. +/// The `spec` key is required; it is the OpenAPI document (JSON or YAML) from +/// which the client is derived. /// /// The optional `interface` lets you specify either a `Positional` argument or /// `Builder` argument style; `Positional` is the default. @@ -71,8 +81,39 @@ mod token_utils; /// `&Result`. This allows clients to /// examine responses, for example to log them. /// -/// The optional `derives` array allows consumers to specify additional derive -/// macros to apply to generated types. +/// Additional options control type generation: +/// - `derives`: optional array of derive macro paths; the derive macros to be +/// applied to all generated types +/// +/// - `struct_builder`: optional boolean; (if true) generates a `::builder()` +/// method for each generated struct that can be used to specify each +/// property and construct the struct +/// +/// - `unknown_crates`: optional policy regarding the handling of schemas that +/// contain the `x-rust-type` extension whose crates are not explicitly named +/// in the `crates` section. The options are `generate` to ignore the +/// extension and generate a *de novo* type, `allow` to use the named type +/// (which may require the addition of a new dependency to compile, and which +/// ignores version compatibility checks), or `deny` to produce a +/// compile-time error (requiring the user to specify the crate's disposition +/// in the `crates` section). +/// +/// - `crates`: optional map from crate name to the version of the crate in +/// use. Types encountered with the Rust type extension (`x-rust-type`) will +/// use types from the specified crates rather than generating them (within +/// the constraints of type compatibility). +/// +/// - `patch`: optional map from type to an object with the optional members +/// `rename` and `derives`. This may be used to renamed generated types or +/// to apply additional (non-default) derive macros to them. +/// +/// - `replace`: optional map from definition name to a replacement type. This +/// may be used to skip generation of the named type and use a existing Rust +/// type. +/// +/// - `convert`: optional map from a JSON schema type defined in `$defs` to a +/// replacement type. This may be used to skip generation of the schema and +/// use an existing Rust type. #[proc_macro] pub fn generate_api(item: TokenStream) -> TokenStream { match do_generate_api(item) { @@ -97,6 +138,11 @@ struct MacroSettings { #[serde(default)] derives: Vec>, + #[serde(default)] + unknown_crates: UnknownPolicy, + #[serde(default)] + crates: HashMap, + #[serde(default)] patch: HashMap, MacroPatch>, #[serde(default)] @@ -173,6 +219,68 @@ impl syn::parse::Parse for ClosureOrPath { } } +struct MacroCrateSpec { + original: Option, + version: CrateVers, +} + +impl<'de> Deserialize<'de> for MacroCrateSpec { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ss = String::deserialize(deserializer)?; + + let (original, vers_str) = if let Some(ii) = ss.find('@') { + let original_str = &ss[..ii]; + let rest = &ss[ii + 1..]; + if !is_crate(original_str) { + return Err(::invalid_value( + serde::de::Unexpected::Str(&ss), + &"valid crate name", + )); + } + + (Some(original_str.to_string()), rest) + } else { + (None, ss.as_ref()) + }; + + let Some(version) = CrateVers::parse(vers_str) else { + return Err(::invalid_value( + serde::de::Unexpected::Str(&ss), + &"valid version", + )); + }; + + Ok(Self { original, version }) + } +} + +#[derive(Hash, PartialEq, Eq)] +struct CrateName(String); +impl<'de> Deserialize<'de> for CrateName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let ss = String::deserialize(deserializer)?; + + if is_crate(&ss) { + Ok(Self(ss)) + } else { + Err(::invalid_value( + serde::de::Unexpected::Str(&ss), + &"valid crate name", + )) + } + } +} + +fn is_crate(s: &str) -> bool { + !s.contains(|cc: char| !cc.is_alphanumeric() && cc != '_' && cc != '-') +} + fn open_file( path: PathBuf, span: proc_macro2::Span, @@ -196,6 +304,8 @@ fn do_generate_api(item: TokenStream) -> Result { pre_hook, pre_hook_async, post_hook, + unknown_crates, + crates, derives, patch, replace, @@ -215,6 +325,21 @@ fn do_generate_api(item: TokenStream) -> Result { post_hook .map(|post_hook| settings.with_post_hook(post_hook.into_inner().0)); + settings.with_unknown_crates(unknown_crates); + crates.into_iter().for_each( + |(CrateName(crate_name), MacroCrateSpec { original, version })| { + if let Some(original_crate) = original { + settings.with_crate( + original_crate, + version, + Some(&crate_name), + ); + } else { + settings.with_crate(crate_name, version, None); + } + }, + ); + derives.into_iter().for_each(|derive| { settings.with_derive(derive.to_token_stream()); });