From 9f0a9910ab349f3be67c81c457d5d1d3981bf9ca Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Tue, 24 Jan 2023 14:55:16 -0800 Subject: [PATCH] Let consumers replace types by name (#317) --- Cargo.lock | 6 +- progenitor-impl/src/lib.rs | 31 ++++++++ progenitor-macro/src/lib.rs | 44 ++++++++---- progenitor-macro/src/token_utils.rs | 107 ++++++++++++++++++++++++++++ progenitor/tests/build_nexus.rs | 7 ++ 5 files changed, 178 insertions(+), 17 deletions(-) create mode 100644 progenitor-macro/src/token_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 384e8d1..ffb5a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2025,7 +2025,7 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "typify" version = "0.0.11-dev" -source = "git+https://github.com/oxidecomputer/typify#aa5e5a6c413920385af6b2c74252ea7d55d3d138" +source = "git+https://github.com/oxidecomputer/typify#2790d798ea650c08686d014fdd87c25d942806b1" dependencies = [ "typify-impl", "typify-macro", @@ -2034,7 +2034,7 @@ dependencies = [ [[package]] name = "typify-impl" version = "0.0.11-dev" -source = "git+https://github.com/oxidecomputer/typify#aa5e5a6c413920385af6b2c74252ea7d55d3d138" +source = "git+https://github.com/oxidecomputer/typify#2790d798ea650c08686d014fdd87c25d942806b1" dependencies = [ "heck", "log", @@ -2052,7 +2052,7 @@ dependencies = [ [[package]] name = "typify-macro" version = "0.0.11-dev" -source = "git+https://github.com/oxidecomputer/typify#aa5e5a6c413920385af6b2c74252ea7d55d3d138" +source = "git+https://github.com/oxidecomputer/typify#2790d798ea650c08686d014fdd87c25d942806b1" dependencies = [ "proc-macro2", "quote", diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index f838a97..4bbd1d7 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -51,7 +51,9 @@ pub struct GenerationSettings { pre_hook: Option, post_hook: Option, extra_derives: Vec, + patch: HashMap, + replace: HashMap)>, convert: Vec<(schemars::schema::SchemaObject, String, Vec)>, } @@ -124,6 +126,26 @@ impl GenerationSettings { self } + pub fn with_replacement< + TS: ToString, + RS: ToString, + I: Iterator, + >( + &mut self, + type_name: TS, + replace_name: RS, + impls: I, + ) -> &mut Self { + self.replace.insert( + type_name.to_string(), + ( + replace_name.to_string(), + impls.map(|x| x.to_string()).collect(), + ), + ); + self + } + pub fn with_conversion>( &mut self, schema: schemars::schema::SchemaObject, @@ -164,6 +186,15 @@ impl Generator { settings.patch.iter().for_each(|(type_name, patch)| { type_settings.with_patch(type_name, patch); }); + settings.replace.iter().for_each( + |(type_name, (replace_name, impls))| { + type_settings.with_replacement( + type_name, + replace_name, + impls.iter(), + ); + }, + ); settings .convert .iter() diff --git a/progenitor-macro/src/lib.rs b/progenitor-macro/src/lib.rs index 74cd9c9..732c57e 100644 --- a/progenitor-macro/src/lib.rs +++ b/progenitor-macro/src/lib.rs @@ -17,6 +17,9 @@ use schemars::schema::SchemaObject; use serde::Deserialize; use serde_tokenstream::{OrderedMap, ParseWrapper}; use syn::LitStr; +use token_utils::TypeAndImpls; + +mod token_utils; /// Generates a client from the given OpenAPI document /// @@ -81,18 +84,20 @@ struct MacroSettings { interface: InterfaceStyle, #[serde(default)] tags: TagStyle, + inner_type: Option>, pre_hook: Option>, post_hook: Option>, + #[serde(default)] derives: Vec>, + #[serde(default)] - patch: HashMap, MacroPatch>, + patch: HashMap, MacroPatch>, #[serde(default)] - convert: OrderedMap< - SchemaObject, - (ParseWrapper, Vec), - >, + replace: HashMap, ParseWrapper>, + #[serde(default)] + convert: OrderedMap>, } #[derive(Deserialize)] @@ -187,6 +192,7 @@ fn do_generate_api(item: TokenStream) -> Result { post_hook, derives, patch, + replace, convert, } = serde_tokenstream::from_tokenstream(&item.into())?; let mut settings = GenerationSettings::default(); @@ -209,15 +215,25 @@ fn do_generate_api(item: TokenStream) -> Result { &patch.into(), ); }); - convert - .into_iter() - .for_each(|(schema, (type_name, impls))| { - settings.with_conversion( - schema, - type_name.to_token_stream(), - impls.into_iter(), - ); - }); + replace.into_iter().for_each(|(type_name, type_and_impls)| { + let type_name = type_name.to_token_stream(); + let type_and_impls = type_and_impls.into_inner(); + let replace_name = type_and_impls.type_name.to_token_stream(); + let impls = type_and_impls + .impls + .into_iter() + .map(|x| x.to_token_stream()); + settings.with_replacement(type_name, replace_name, impls); + }); + convert.into_iter().for_each(|(schema, type_and_impls)| { + let type_and_impls = type_and_impls.into_inner(); + let type_name = type_and_impls.type_name.to_token_stream(); + let impls = type_and_impls + .impls + .into_iter() + .map(|x| x.to_token_stream()); + settings.with_conversion(schema, type_name, impls); + }); (spec.into_inner(), settings) }; diff --git a/progenitor-macro/src/token_utils.rs b/progenitor-macro/src/token_utils.rs new file mode 100644 index 0000000..827dc97 --- /dev/null +++ b/progenitor-macro/src/token_utils.rs @@ -0,0 +1,107 @@ +// Copyright 2023 Oxide Computer Company + +use quote::ToTokens; +use syn::{ + parse::Parse, + punctuated::Punctuated, + token::{Add, Colon}, + Ident, Path, Token, TraitBoundModifier, +}; + +#[derive(Debug)] +pub struct TypeAndImpls { + pub type_name: Path, + pub colon_token: Option, + pub impls: Punctuated, +} + +impl Parse for TypeAndImpls { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let type_name: Path = input.parse()?; + let colon_token: Option = input.parse()?; + let mut impls = Punctuated::default(); + + if colon_token.is_some() { + loop { + let value: ImplTrait = input.parse()?; + impls.push_value(value); + if !input.peek(Token![+]) { + break; + } + let punct: Token![+] = input.parse()?; + impls.push_punct(punct); + } + } + + Ok(Self { + type_name, + colon_token, + impls, + }) + } +} + +impl ToTokens for TypeAndImpls { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.type_name.to_tokens(tokens); + self.colon_token.to_tokens(tokens); + self.impls.to_tokens(tokens); + } +} + +#[derive(Debug)] +pub struct ImplTrait { + pub modifier: TraitBoundModifier, + pub impl_name: Ident, +} + +impl Parse for ImplTrait { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let modifier: TraitBoundModifier = input.parse()?; + let impl_name: Ident = input.parse()?; + + Ok(Self { + modifier, + impl_name, + }) + } +} + +impl ToTokens for ImplTrait { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.modifier.to_tokens(tokens); + self.impl_name.to_tokens(tokens); + } +} + +#[cfg(test)] +mod tests { + use super::TypeAndImpls; + + use quote::{quote, ToTokens}; + + #[test] + fn test_parse_type_and_impls() { + let input = quote! { my_crate::MyType }; + let value = syn::parse2::(input).unwrap(); + assert_eq!( + value.type_name.to_token_stream().to_string(), + "my_crate :: MyType", + ); + assert_eq!(value.impls.len(), 0); + + let input = quote! { my_crate::MyType: ?Display + Hash }; + let value = syn::parse2::(input).unwrap(); + assert_eq!( + value.type_name.to_token_stream().to_string(), + "my_crate :: MyType", + ); + assert_eq!(value.impls.len(), 2); + let mut ii = value.impls.into_iter(); + assert_eq!( + ii.next().unwrap().to_token_stream().to_string(), + "? Display", + ); + assert_eq!(ii.next().unwrap().to_token_stream().to_string(), "Hash",); + } +} diff --git a/progenitor/tests/build_nexus.rs b/progenitor/tests/build_nexus.rs index 0245323..cfa07de 100644 --- a/progenitor/tests/build_nexus.rs +++ b/progenitor/tests/build_nexus.rs @@ -27,6 +27,7 @@ mod builder_untagged { use futures::StreamExt; mod nexus_client { + pub type MyIpv4Net = String; progenitor::generate_api!( spec = "../sample_openapi/nexus.json", interface = Builder, @@ -35,6 +36,9 @@ mod builder_untagged { Name = { derives = [Hash], } + }, + replace = { + Ipv4Net = crate::builder_untagged::nexus_client::MyIpv4Net, } ); } @@ -42,6 +46,9 @@ mod builder_untagged { use nexus_client::Client; pub fn _ignore() { + // Verify the replacement above. + let _ignore = nexus_client::types::IpNet::V4(String::new()); + let client = Client::new(""); let stream = client .instance_disk_list()