Let consumers replace types by name (#317)

This commit is contained in:
Adam Leventhal 2023-01-24 14:55:16 -08:00 committed by GitHub
parent 35eab2f017
commit 9f0a9910ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 17 deletions

6
Cargo.lock generated
View File

@ -2025,7 +2025,7 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]] [[package]]
name = "typify" name = "typify"
version = "0.0.11-dev" version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#aa5e5a6c413920385af6b2c74252ea7d55d3d138" source = "git+https://github.com/oxidecomputer/typify#2790d798ea650c08686d014fdd87c25d942806b1"
dependencies = [ dependencies = [
"typify-impl", "typify-impl",
"typify-macro", "typify-macro",
@ -2034,7 +2034,7 @@ dependencies = [
[[package]] [[package]]
name = "typify-impl" name = "typify-impl"
version = "0.0.11-dev" version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#aa5e5a6c413920385af6b2c74252ea7d55d3d138" source = "git+https://github.com/oxidecomputer/typify#2790d798ea650c08686d014fdd87c25d942806b1"
dependencies = [ dependencies = [
"heck", "heck",
"log", "log",
@ -2052,7 +2052,7 @@ dependencies = [
[[package]] [[package]]
name = "typify-macro" name = "typify-macro"
version = "0.0.11-dev" version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#aa5e5a6c413920385af6b2c74252ea7d55d3d138" source = "git+https://github.com/oxidecomputer/typify#2790d798ea650c08686d014fdd87c25d942806b1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -51,7 +51,9 @@ pub struct GenerationSettings {
pre_hook: Option<TokenStream>, pre_hook: Option<TokenStream>,
post_hook: Option<TokenStream>, post_hook: Option<TokenStream>,
extra_derives: Vec<String>, extra_derives: Vec<String>,
patch: HashMap<String, TypePatch>, patch: HashMap<String, TypePatch>,
replace: HashMap<String, (String, Vec<String>)>,
convert: Vec<(schemars::schema::SchemaObject, String, Vec<String>)>, convert: Vec<(schemars::schema::SchemaObject, String, Vec<String>)>,
} }
@ -124,6 +126,26 @@ impl GenerationSettings {
self self
} }
pub fn with_replacement<
TS: ToString,
RS: ToString,
I: Iterator<Item = impl ToString>,
>(
&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<S: ToString, I: Iterator<Item = impl ToString>>( pub fn with_conversion<S: ToString, I: Iterator<Item = impl ToString>>(
&mut self, &mut self,
schema: schemars::schema::SchemaObject, schema: schemars::schema::SchemaObject,
@ -164,6 +186,15 @@ impl Generator {
settings.patch.iter().for_each(|(type_name, patch)| { settings.patch.iter().for_each(|(type_name, patch)| {
type_settings.with_patch(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 settings
.convert .convert
.iter() .iter()

View File

@ -17,6 +17,9 @@ use schemars::schema::SchemaObject;
use serde::Deserialize; use serde::Deserialize;
use serde_tokenstream::{OrderedMap, ParseWrapper}; use serde_tokenstream::{OrderedMap, ParseWrapper};
use syn::LitStr; use syn::LitStr;
use token_utils::TypeAndImpls;
mod token_utils;
/// Generates a client from the given OpenAPI document /// Generates a client from the given OpenAPI document
/// ///
@ -81,18 +84,20 @@ struct MacroSettings {
interface: InterfaceStyle, interface: InterfaceStyle,
#[serde(default)] #[serde(default)]
tags: TagStyle, tags: TagStyle,
inner_type: Option<ParseWrapper<syn::Type>>, inner_type: Option<ParseWrapper<syn::Type>>,
pre_hook: Option<ParseWrapper<ClosureOrPath>>, pre_hook: Option<ParseWrapper<ClosureOrPath>>,
post_hook: Option<ParseWrapper<ClosureOrPath>>, post_hook: Option<ParseWrapper<ClosureOrPath>>,
#[serde(default)] #[serde(default)]
derives: Vec<ParseWrapper<syn::Path>>, derives: Vec<ParseWrapper<syn::Path>>,
#[serde(default)] #[serde(default)]
patch: HashMap<ParseWrapper<syn::Type>, MacroPatch>, patch: HashMap<ParseWrapper<syn::Ident>, MacroPatch>,
#[serde(default)] #[serde(default)]
convert: OrderedMap< replace: HashMap<ParseWrapper<syn::Ident>, ParseWrapper<TypeAndImpls>>,
SchemaObject, #[serde(default)]
(ParseWrapper<syn::Path>, Vec<MacroSettingsImpl>), convert: OrderedMap<SchemaObject, ParseWrapper<TypeAndImpls>>,
>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -187,6 +192,7 @@ fn do_generate_api(item: TokenStream) -> Result<TokenStream, syn::Error> {
post_hook, post_hook,
derives, derives,
patch, patch,
replace,
convert, convert,
} = serde_tokenstream::from_tokenstream(&item.into())?; } = serde_tokenstream::from_tokenstream(&item.into())?;
let mut settings = GenerationSettings::default(); let mut settings = GenerationSettings::default();
@ -209,14 +215,24 @@ fn do_generate_api(item: TokenStream) -> Result<TokenStream, syn::Error> {
&patch.into(), &patch.into(),
); );
}); });
convert 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() .into_iter()
.for_each(|(schema, (type_name, impls))| { .map(|x| x.to_token_stream());
settings.with_conversion( settings.with_replacement(type_name, replace_name, impls);
schema, });
type_name.to_token_stream(), convert.into_iter().for_each(|(schema, type_and_impls)| {
impls.into_iter(), 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) (spec.into_inner(), settings)
}; };

View File

@ -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<Colon>,
pub impls: Punctuated<ImplTrait, Add>,
}
impl Parse for TypeAndImpls {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let type_name: Path = input.parse()?;
let colon_token: Option<Colon> = 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<Self> {
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::<TypeAndImpls>(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::<TypeAndImpls>(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",);
}
}

View File

@ -27,6 +27,7 @@ mod builder_untagged {
use futures::StreamExt; use futures::StreamExt;
mod nexus_client { mod nexus_client {
pub type MyIpv4Net = String;
progenitor::generate_api!( progenitor::generate_api!(
spec = "../sample_openapi/nexus.json", spec = "../sample_openapi/nexus.json",
interface = Builder, interface = Builder,
@ -35,6 +36,9 @@ mod builder_untagged {
Name = { Name = {
derives = [Hash], derives = [Hash],
} }
},
replace = {
Ipv4Net = crate::builder_untagged::nexus_client::MyIpv4Net,
} }
); );
} }
@ -42,6 +46,9 @@ mod builder_untagged {
use nexus_client::Client; use nexus_client::Client;
pub fn _ignore() { pub fn _ignore() {
// Verify the replacement above.
let _ignore = nexus_client::types::IpNet::V4(String::new());
let client = Client::new(""); let client = Client::new("");
let stream = client let stream = client
.instance_disk_list() .instance_disk_list()