-
Notifications
You must be signed in to change notification settings - Fork 77
Use a proc macro for being able to derive schemas for our action structs #129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c5f5e81
f2f6e34
9abe8ea
addb7a3
7f00ac1
9dcc096
6d00e43
48219ee
5e62148
2cd6027
746b6f3
150b024
c4e037e
c7785c2
b51b7b1
9bca3f5
0bf4c3a
b273e3b
505983e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
[workspace] | ||
members = [ | ||
"acceptance", | ||
"derive-macros", | ||
"ffi", | ||
"kernel", | ||
"kernel/examples/*", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "derive-macros" | ||
authors.workspace = true | ||
edition.workspace = true | ||
homepage.workspace = true | ||
keywords.workspace = true | ||
license.workspace = true | ||
repository.workspace = true | ||
readme.workspace = true | ||
version.workspace = true | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
proc-macro2 = "1" | ||
syn = { version = "2.0", features = ["extra-traits"] } | ||
quote = "1.0" | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
use proc_macro2::{Ident, TokenStream}; | ||
use quote::{quote, quote_spanned}; | ||
use syn::spanned::Spanned; | ||
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields, PathArguments, Type}; | ||
|
||
#[proc_macro_derive(Schema, attributes(schema))] | ||
pub fn derive_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { | ||
let input = parse_macro_input!(input as DeriveInput); | ||
let struct_ident = input.ident; | ||
|
||
let schema_fields = gen_schema_fields(&input.data); | ||
let output = quote! { | ||
impl crate::actions::schemas::GetField for #struct_ident { | ||
fn get_field(name: impl Into<String>) -> crate::schema::StructField { | ||
use crate::actions::schemas::GetField; | ||
crate::schema::StructField::new( | ||
name, | ||
crate::schema::StructType::new(vec![ | ||
#schema_fields | ||
]), | ||
// By default not nullable. To make something nullable wrap it in an Option | ||
false, | ||
) | ||
} | ||
} | ||
}; | ||
proc_macro::TokenStream::from(output) | ||
} | ||
|
||
// turn our struct name into the schema name, goes from snake_case to camelCase | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know where to put the doc comment, but somewhere we should be careful to explain that the actual field names are all mandated by Delta spec, and so the user of this macro is responsible to ensure that e.g. The same explains why it's ok to use |
||
fn get_schema_name(name: &Ident) -> Ident { | ||
let snake_name = name.to_string(); | ||
let mut next_caps = false; | ||
let ret: String = snake_name | ||
.chars() | ||
.filter_map(|c| { | ||
if c == '_' { | ||
next_caps = true; | ||
None | ||
} else if next_caps { | ||
next_caps = false; | ||
// This assumes we're using ascii, should be okay | ||
Some(c.to_ascii_uppercase()) | ||
} else { | ||
Some(c) | ||
} | ||
}) | ||
.collect(); | ||
Ident::new(&ret, name.span()) | ||
} | ||
|
||
fn gen_schema_fields(data: &Data) -> TokenStream { | ||
let fields = match data { | ||
Data::Struct(DataStruct { | ||
fields: Fields::Named(fields), | ||
.. | ||
}) => &fields.named, | ||
_ => panic!("this derive macro only works on structs with named fields"), | ||
}; | ||
|
||
let schema_fields = fields.iter().map(|field| { | ||
let name = field.ident.as_ref().unwrap(); // we know these are named fields | ||
let name = get_schema_name(name); | ||
match field.ty { | ||
Type::Path(ref type_path) => { | ||
if let Some(fin) = type_path.path.segments.iter().last() { | ||
let type_ident = &fin.ident; | ||
if let PathArguments::AngleBracketed(angle_args) = &fin.arguments { | ||
quote_spanned! {field.span()=> | ||
#type_ident::#angle_args::get_field(stringify!(#name)) | ||
} | ||
} else { | ||
quote_spanned! {field.span()=> | ||
#type_ident::get_field(stringify!(#name)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't we need to emit the fully qualified type name, in case the user didn't |
||
} | ||
} | ||
} else { | ||
panic!("Couldn't get type"); | ||
} | ||
} | ||
_ => { | ||
panic!("Can't handle type: {:?}", field.ty); | ||
} | ||
} | ||
}); | ||
quote! { #(#schema_fields),* } | ||
} |
Uh oh!
There was an error while loading. Please reload this page.