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]]
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",

View File

@ -51,7 +51,9 @@ pub struct GenerationSettings {
pre_hook: Option<TokenStream>,
post_hook: Option<TokenStream>,
extra_derives: Vec<String>,
patch: HashMap<String, TypePatch>,
replace: HashMap<String, (String, Vec<String>)>,
convert: Vec<(schemars::schema::SchemaObject, String, Vec<String>)>,
}
@ -124,6 +126,26 @@ impl GenerationSettings {
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>>(
&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()

View File

@ -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<ParseWrapper<syn::Type>>,
pre_hook: Option<ParseWrapper<ClosureOrPath>>,
post_hook: Option<ParseWrapper<ClosureOrPath>>,
#[serde(default)]
derives: Vec<ParseWrapper<syn::Path>>,
#[serde(default)]
patch: HashMap<ParseWrapper<syn::Type>, MacroPatch>,
patch: HashMap<ParseWrapper<syn::Ident>, MacroPatch>,
#[serde(default)]
convert: OrderedMap<
SchemaObject,
(ParseWrapper<syn::Path>, Vec<MacroSettingsImpl>),
>,
replace: HashMap<ParseWrapper<syn::Ident>, ParseWrapper<TypeAndImpls>>,
#[serde(default)]
convert: OrderedMap<SchemaObject, ParseWrapper<TypeAndImpls>>,
}
#[derive(Deserialize)]
@ -187,6 +192,7 @@ fn do_generate_api(item: TokenStream) -> Result<TokenStream, syn::Error> {
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<TokenStream, syn::Error> {
&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)
};

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