Add Destruct macro and trait for automatically acessing struct fields

This commit is contained in:
Dennis Kobert 2025-04-14 08:15:26 +02:00
parent 33de539d6d
commit 4016653371
No known key found for this signature in database
GPG key ID: 5A4358CB9530F933
5 changed files with 106 additions and 8 deletions

View file

@ -252,6 +252,11 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
let output_fields = match attributes.deconstruct_output {
false => quote!(&[]),
true => quote!(#output_type::fields),
};
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier);
Ok(quote! {
/// Underlying implementation for [#struct_name]
@ -310,6 +315,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
category: #category,
description: #description,
properties: #properties,
output_fields: #output_fields,
fields: vec![
#(
FieldMetadata {
@ -371,7 +377,7 @@ fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::G
impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> {
const INDEX: usize = #input_index;
fn identifier() -> &'static str {
protonode_identifier()
#identifier
}
type Result = #ty;
}
@ -381,12 +387,6 @@ fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::G
quote! {
pub mod #inputs_module_name {
use super::*;
pub fn protonode_identifier() -> &'static str {
// Storing the string in a once lock should reduce allocations (since we call this in a loop)?
static NODE_NAME: std::sync::OnceLock<String> = std::sync::OnceLock::new();
NODE_NAME.get_or_init(|| #identifier )
}
#(#generated_input_accessor)*
}
}

View file

@ -0,0 +1,54 @@
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Error, Ident, spanned::Spanned};
pub fn derive(struct_name: Ident, data: syn::Data) -> syn::Result<TokenStream2> {
let syn::Data::Struct(data_struct) = data else {
return Err(Error::new(proc_macro2::Span::call_site(), String::from("Deriving `Destruct` is currently only supported for structs")));
};
let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| {
Error::new(
proc_macro2::Span::call_site(),
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e),
)
})?;
let path = quote!(std::module_path!().rsplit_once("::").unwrap().0);
let mut node_implementations = Vec::with_capacity(data_struct.fields.len());
let mut field_structs = Vec::with_capacity(data_struct.fields.len());
for field in data_struct.fields {
let Some(field_name) = field.ident else {
return Err(Error::new(field.span(), String::from("Destruct cant be used on tuple structs")));
};
let ty = field.ty;
let fn_name = quote::format_ident!("extract_ {field_name}");
node_implementations.push(quote! {
#[node_macro(category(""))]
fn #fn_name(_: impl Ctx, data: #struct_name) -> #ty {
data.#field_name
}
});
field_structs.push(quote! {
#crate_name::registry::FieldStruct {
name: stringify!(#field_name),
node_path: concat!()
}
})
}
Ok(quote! {
impl graphene_core::registry::Destruct for #struct_name {
fn fields() -> &[graphene_core::registry::FieldStruct] {
&[
]
}
}
})
}

View file

@ -125,6 +125,21 @@ pub fn old_node_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
node_impl_proxy(attr, item)
}
mod destruct;
#[proc_macro_error]
#[proc_macro_derive(Destruct)]
/// Derives the `Destruct` trait for structs and creates acessor node implementations.
pub fn derive_destruct(item: TokenStream) -> TokenStream {
let s = syn::parse_macro_input!(item as syn::DeriveInput);
let parse_result = destruct::derive(s.ident, s.data).into();
let Ok(parsed_node) = parse_result else {
let e = parse_result.unwrap_err();
return syn::Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error().into();
};
parsed_node.into()
}
fn node_new_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let node = parse_macro_input!(attr as syn::PathSegment);

View file

@ -43,6 +43,7 @@ pub(crate) struct NodeFnAttributes {
pub(crate) path: Option<Path>,
pub(crate) skip_impl: bool,
pub(crate) properties_string: Option<LitStr>,
pub(crate) deconstruct_output: bool,
// Add more attributes as needed
}
@ -173,6 +174,7 @@ impl Parse for NodeFnAttributes {
let mut display_name = None;
let mut path = None;
let mut skip_impl = false;
let mut deconstruct_output = false;
let mut properties_string = None;
let content = input;
@ -213,6 +215,12 @@ impl Parse for NodeFnAttributes {
}
skip_impl = true;
}
Meta::Path(path) if path.is_ident("deconstruct_output") => {
if skip_impl {
return Err(Error::new_spanned(path, "Multiple 'deconstruct_output' attributes are not allowed"));
}
deconstruct_output = true;
}
Meta::List(meta) if meta.path.is_ident("properties") => {
if properties_string.is_some() {
return Err(Error::new_spanned(path, "Multiple 'properties_string' attributes are not allowed"));
@ -229,7 +237,7 @@ impl Parse for NodeFnAttributes {
indoc!(
r#"
Unsupported attribute in `node`.
Supported attributes are 'category', 'path' and 'name'.
Supported attributes are 'category', 'path', 'skip_impl', 'deconstruct_output', 'properties_string' and 'name'.
Example usage:
#[node_macro::node(category("Value"), name("Test Node"))]
@ -246,6 +254,7 @@ impl Parse for NodeFnAttributes {
path,
skip_impl,
properties_string,
deconstruct_output,
})
}
}