Skip to content

Commit e27f090

Browse files
committed
Complete node output destructuring implementation
1 parent 4016653 commit e27f090

File tree

5 files changed

+121
-49
lines changed

5 files changed

+121
-49
lines changed

node-graph/gcore/src/registry.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,7 @@ pub struct StructField {
5959
}
6060

6161
pub trait Destruct {
62-
fn fields(&self) -> &'static [StructField];
63-
}
64-
65-
impl<T> Destruct for &T
66-
where
67-
T: Default,
68-
{
69-
fn fields(&self) -> &'static [StructField] {
62+
fn fields() -> &'static [StructField] {
7063
&[]
7164
}
7265
}

node-graph/node-macro/src/codegen.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,8 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
253253
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
254254

255255
let output_fields = match attributes.deconstruct_output {
256-
false => quote!(&[]),
257-
true => quote!(#output_type::fields),
256+
false => quote!(&[] as &[#graphene_core::registry::StructField]),
257+
true => quote!(<#output_type as #graphene_core::registry::Destruct>::fields()),
258258
};
259259

260260
let node_input_accessor = generate_node_input_references(parsed, fn_generics, &field_idents, &graphene_core, &identifier);
@@ -377,7 +377,7 @@ fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::G
377377
impl <#(#used),*> #graphene_core::NodeInputDecleration for #struct_name <#(#fn_generic_params),*> {
378378
const INDEX: usize = #input_index;
379379
fn identifier() -> &'static str {
380-
#identifier
380+
protonode_identifier()
381381
}
382382
type Result = #ty;
383383
}
@@ -387,6 +387,14 @@ fn generate_node_input_references(parsed: &ParsedNodeFn, fn_generics: &[crate::G
387387
quote! {
388388
pub mod #inputs_module_name {
389389
use super::*;
390+
391+
pub fn protonode_identifier() -> &'static str {
392+
static NODE_NAME: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
393+
NODE_NAME.get_or_init(|| {
394+
let name = #identifier;
395+
Box::leak(name.into_boxed_str())
396+
})
397+
}
390398
#(#generated_input_accessor)*
391399
}
392400
}
Lines changed: 103 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,127 @@
1-
use proc_macro2::TokenStream as TokenStream2;
2-
use quote::quote;
3-
use syn::{Error, Ident, spanned::Spanned};
1+
use convert_case::{Case, Casing};
2+
use proc_macro_crate::FoundCrate;
3+
use proc_macro2::{Span, TokenStream as TokenStream2};
4+
use quote::{format_ident, quote};
5+
use syn::{Data, DeriveInput, Error, LitStr, Meta, Type, spanned::Spanned};
46

5-
pub fn derive(struct_name: Ident, data: syn::Data) -> syn::Result<TokenStream2> {
6-
let syn::Data::Struct(data_struct) = data else {
7-
return Err(Error::new(proc_macro2::Span::call_site(), String::from("Deriving `Destruct` is currently only supported for structs")));
7+
pub fn derive(input: DeriveInput) -> syn::Result<TokenStream2> {
8+
let struct_name = input.ident;
9+
let generics = input.generics;
10+
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
11+
12+
let Data::Struct(data_struct) = input.data else {
13+
return Err(Error::new(
14+
Span::call_site(),
15+
"Deriving `Destruct` is currently only supported for structs",
16+
));
817
};
918

10-
let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| {
19+
let graphene_core = match proc_macro_crate::crate_name("graphene-core").map_err(|e| {
1120
Error::new(
12-
proc_macro2::Span::call_site(),
13-
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e),
21+
Span::call_site(),
22+
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {e}"),
1423
)
15-
})?;
16-
17-
let path = quote!(std::module_path!().rsplit_once("::").unwrap().0);
24+
})? {
25+
FoundCrate::Itself => quote!(crate),
26+
FoundCrate::Name(name) => {
27+
let ident = syn::Ident::new(&name, Span::call_site());
28+
quote!(#ident)
29+
}
30+
};
1831

1932
let mut node_implementations = Vec::with_capacity(data_struct.fields.len());
20-
let mut field_structs = Vec::with_capacity(data_struct.fields.len());
33+
let mut output_fields = Vec::with_capacity(data_struct.fields.len());
2134

2235
for field in data_struct.fields {
2336
let Some(field_name) = field.ident else {
24-
return Err(Error::new(field.span(), String::from("Destruct cant be used on tuple structs")));
37+
return Err(Error::new(field.span(), "Destruct cannot be used on tuple structs"));
2538
};
39+
2640
let ty = field.ty;
27-
let fn_name = quote::format_ident!("extract_ {field_name}");
28-
node_implementations.push(quote! {
29-
#[node_macro(category(""))]
30-
fn #fn_name(_: impl Ctx, data: #struct_name) -> #ty {
31-
data.#field_name
32-
}
33-
});
41+
let output_name = parse_output_name(&field.attrs)?.unwrap_or_else(|| field_name.to_string().to_case(Case::Title));
42+
let output_name_lit = LitStr::new(&output_name, field_name.span());
3443

35-
field_structs.push(quote! {
36-
#crate_name::registry::FieldStruct {
37-
name: stringify!(#field_name),
38-
node_path: concat!()
44+
let fn_name = format_ident!("extract_{}_{}", struct_name.to_string().to_case(Case::Snake), field_name);
45+
let node_struct_name = format_ident!("{}Node", fn_name.to_string().to_case(Case::Pascal));
3946

47+
node_implementations.push(generate_extractor_node(&graphene_core, &fn_name, &struct_name, &field_name, &ty, &output_name_lit));
48+
output_fields.push(quote! {
49+
#graphene_core::registry::StructField {
50+
name: #output_name_lit,
51+
node_path: concat!(std::module_path!().rsplit_once("::").unwrap().0, "::", stringify!(#node_struct_name)),
52+
ty: #graphene_core::concrete!(#ty),
4053
}
41-
})
54+
});
4255
}
4356

4457
Ok(quote! {
45-
impl graphene_core::registry::Destruct for #struct_name {
46-
fn fields() -> &[graphene_core::registry::FieldStruct] {
47-
&[
58+
#(#node_implementations)*
4859

60+
impl #impl_generics #graphene_core::registry::Destruct for #struct_name #ty_generics #where_clause {
61+
fn fields() -> &'static [#graphene_core::registry::StructField] {
62+
&[
63+
#(#output_fields,)*
4964
]
5065
}
5166
}
52-
5367
})
5468
}
69+
70+
fn generate_extractor_node(
71+
graphene_core: &TokenStream2,
72+
fn_name: &syn::Ident,
73+
struct_name: &syn::Ident,
74+
field_name: &syn::Ident,
75+
ty: &Type,
76+
output_name: &LitStr,
77+
) -> TokenStream2 {
78+
quote! {
79+
#[node_macro::node(category(""), name(#output_name))]
80+
fn #fn_name(_: impl #graphene_core::Ctx, data: #struct_name) -> #ty {
81+
data.#field_name
82+
}
83+
}
84+
}
85+
86+
fn parse_output_name(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> {
87+
let mut output_name = None;
88+
89+
for attr in attrs {
90+
if !attr.path().is_ident("output") {
91+
continue;
92+
}
93+
94+
let mut this_output_name = None;
95+
match &attr.meta {
96+
Meta::Path(_) => {
97+
return Err(Error::new_spanned(attr, "Expected output metadata like #[output(name = \"Result\")]"));
98+
}
99+
Meta::NameValue(_) => {
100+
return Err(Error::new_spanned(attr, "Expected output metadata like #[output(name = \"Result\")]"));
101+
}
102+
Meta::List(_) => {
103+
attr.parse_nested_meta(|meta| {
104+
if meta.path.is_ident("name") {
105+
if this_output_name.is_some() {
106+
return Err(meta.error("Multiple output names provided for one field"));
107+
}
108+
let value = meta.value()?;
109+
let lit: LitStr = value.parse()?;
110+
this_output_name = Some(lit.value());
111+
Ok(())
112+
} else {
113+
Err(meta.error("Unsupported output metadata. Supported syntax is #[output(name = \"...\")]"))
114+
}
115+
})?;
116+
}
117+
}
118+
119+
let this_output_name = this_output_name.ok_or_else(|| Error::new_spanned(attr, "Missing output name. Use #[output(name = \"...\")]"))?;
120+
if output_name.is_some() {
121+
return Err(Error::new_spanned(attr, "Multiple #[output(...)] attributes are not allowed on one field"));
122+
}
123+
output_name = Some(this_output_name);
124+
}
125+
126+
Ok(output_name)
127+
}

node-graph/node-macro/src/lib.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,11 @@ mod destruct;
131131
#[proc_macro_derive(Destruct)]
132132
/// Derives the `Destruct` trait for structs and creates acessor node implementations.
133133
pub fn derive_destruct(item: TokenStream) -> TokenStream {
134-
let s = syn::parse_macro_input!(item as syn::DeriveInput);
135-
let parse_result = destruct::derive(s.ident, s.data).into();
136-
let Ok(parsed_node) = parse_result else {
137-
let e = parse_result.unwrap_err();
138-
return syn::Error::new(e.span(), format!("Failed to parse node function: {e}")).to_compile_error().into();
139-
};
140-
parsed_node.into()
134+
let input = syn::parse_macro_input!(item as syn::DeriveInput);
135+
match destruct::derive(input) {
136+
Ok(tokens) => tokens.into(),
137+
Err(error) => syn::Error::new(error.span(), format!("Failed to derive Destruct: {error}")).to_compile_error().into(),
138+
}
141139
}
142140

143141
fn node_new_impl(attr: TokenStream, item: TokenStream) -> TokenStream {

node-graph/node-macro/src/parsing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ impl Parse for NodeFnAttributes {
216216
skip_impl = true;
217217
}
218218
Meta::Path(path) if path.is_ident("deconstruct_output") => {
219-
if skip_impl {
219+
if deconstruct_output {
220220
return Err(Error::new_spanned(path, "Multiple 'deconstruct_output' attributes are not allowed"));
221221
}
222222
deconstruct_output = true;

0 commit comments

Comments
 (0)