Skip to main content

flowjs_rs_macros/
lib.rs

1//! Derive macro for flowjs-rs.
2//!
3//! Generates `Flow` trait implementations from Rust struct and enum definitions,
4//! producing Flow type declarations.
5
6#![deny(unused)]
7
8use proc_macro2::{Ident, TokenStream};
9use quote::{format_ident, quote};
10use syn::{
11    parse_macro_input, parse_quote, Data, DeriveInput, Expr, Fields, GenericParam, Generics, Lit,
12    Meta, Path, Result, Type, WherePredicate,
13};
14
15mod attr;
16mod utils;
17
18use attr::{ContainerAttr, FieldAttr, VariantAttr};
19
20/// Collected dependency types for visit_dependencies generation.
21struct Dependencies {
22    crate_rename: Path,
23    /// Types to visit directly: `v.visit::<T>()`
24    types: Vec<Type>,
25    /// Types whose dependencies to visit transitively: `<T as Flow>::visit_dependencies(v)`
26    transitive: Vec<Type>,
27    /// Types whose generics to visit: `<T as Flow>::visit_generics(v)`
28    generics: Vec<Type>,
29}
30
31impl Dependencies {
32    fn new(crate_rename: Path) -> Self {
33        Self {
34            crate_rename,
35            types: Vec::new(),
36            transitive: Vec::new(),
37            generics: Vec::new(),
38        }
39    }
40
41    /// Add a field type as a dependency (visit the type + its generics).
42    fn push(&mut self, ty: &Type) {
43        self.types.push(ty.clone());
44        self.generics.push(ty.clone());
45    }
46
47    /// Add a field type's transitive deps only (for inline/flatten).
48    fn append_from(&mut self, ty: &Type) {
49        self.transitive.push(ty.clone());
50    }
51
52    fn to_tokens(&self) -> TokenStream {
53        let crate_rename = &self.crate_rename;
54        let visit_types = self.types.iter().map(|ty| {
55            quote! { v.visit::<#ty>(); }
56        });
57        let visit_transitive = self.transitive.iter().map(|ty| {
58            quote! { <#ty as #crate_rename::Flow>::visit_dependencies(v); }
59        });
60        let visit_generics = self.generics.iter().map(|ty| {
61            quote! { <#ty as #crate_rename::Flow>::visit_generics(v); }
62        });
63
64        quote! {
65            #(#visit_types)*
66            #(#visit_generics)*
67            #(#visit_transitive)*
68        }
69    }
70}
71
72struct DerivedFlow {
73    crate_rename: Path,
74    flow_name: Expr,
75    docs: Vec<Expr>,
76    inline: TokenStream,
77    inline_flattened: TokenStream,
78    is_enum: TokenStream,
79    is_opaque: bool,
80    opaque_bound: Option<TokenStream>,
81    export: bool,
82    export_to: Option<Expr>,
83    bound: Option<Vec<WherePredicate>>,
84    deps: Dependencies,
85}
86
87impl DerivedFlow {
88    fn into_impl(self, rust_ty: Ident, generics: Generics) -> TokenStream {
89        let export_test = self
90            .export
91            .then(|| self.generate_export_test(&rust_ty, &generics));
92
93        let output_path_fn = {
94            let flow_name = &self.flow_name;
95            let path_string = match &self.export_to {
96                Some(dir_or_file) => quote! {{
97                    let dir_or_file = format!("{}", #dir_or_file);
98                    if dir_or_file.ends_with('/') {
99                        format!("{dir_or_file}{}.js.flow", #flow_name)
100                    } else {
101                        format!("{dir_or_file}")
102                    }
103                }},
104                None => quote![format!("{}.js.flow", #flow_name)],
105            };
106            quote! {
107                fn output_path() -> Option<std::path::PathBuf> {
108                    Some(std::path::PathBuf::from(#path_string))
109                }
110            }
111        };
112
113        let crate_rename = &self.crate_rename;
114        let flow_name = &self.flow_name;
115        let inline = &self.inline;
116        let inline_flattened = &self.inline_flattened;
117        let is_enum = &self.is_enum;
118
119        let docs_fn = if self.docs.is_empty() {
120            quote! { fn docs() -> Option<String> { None } }
121        } else {
122            let docs = &self.docs;
123            quote! {
124                fn docs() -> Option<String> {
125                    Some([#(#docs),*].join("\n"))
126                }
127            }
128        };
129
130        // Generate name() with generic parameters
131        let name_fn = {
132            let generic_names: Vec<_> = generics
133                .type_params()
134                .map(|tp| {
135                    let ident = &tp.ident;
136                    quote!(<#ident as #crate_rename::Flow>::name(cfg))
137                })
138                .collect();
139
140            if generic_names.is_empty() {
141                quote! {
142                    fn name(cfg: &#crate_rename::Config) -> String {
143                        #flow_name.to_owned()
144                    }
145                }
146            } else {
147                quote! {
148                    fn name(cfg: &#crate_rename::Config) -> String {
149                        format!("{}<{}>", #flow_name, vec![#(#generic_names),*].join(", "))
150                    }
151                }
152            }
153        };
154
155        // Generate decl() — for generic types, create placeholder dummy types
156        let decl_fn = if self.is_opaque {
157            let bound = self
158                .opaque_bound
159                .map(|b| quote! { format!("declare export opaque type {}: {};", #flow_name, #b) })
160                .unwrap_or_else(
161                    || quote! { format!("declare export opaque type {};", #flow_name) },
162                );
163            quote! {
164                fn decl(cfg: &#crate_rename::Config) -> String {
165                    #bound
166                }
167            }
168        } else {
169            let has_generics = generics.type_params().next().is_some();
170            if has_generics {
171                // For generic types: create dummy types, get inline from WithoutGenerics
172                let generic_idents: Vec<_> = generics.type_params().map(|tp| &tp.ident).collect();
173
174                // Generate dummy type declarations for each generic param
175                let dummy_decls: Vec<_> = generic_idents
176                    .iter()
177                    .map(|ident| {
178                        let dummy_name = format_ident!("{}Dummy", ident);
179                        quote! {
180                            struct #dummy_name;
181                            impl #crate_rename::Flow for #dummy_name {
182                                type WithoutGenerics = Self;
183                                type OptionInnerType = Self;
184                                fn name(_: &#crate_rename::Config) -> String {
185                                    stringify!(#ident).to_owned()
186                                }
187                                fn inline(cfg: &#crate_rename::Config) -> String {
188                                    Self::name(cfg)
189                                }
190                            }
191                        }
192                    })
193                    .collect();
194
195                let generics_str: Vec<_> = generic_idents
196                    .iter()
197                    .map(|ident| quote!(stringify!(#ident)))
198                    .collect();
199
200                // Build full generic args for instantiation (named dummies for type params)
201                let full_generic_args: Vec<_> = generics
202                    .params
203                    .iter()
204                    .map(|p| match p {
205                        GenericParam::Type(tp) => {
206                            let dummy_name = format_ident!("{}Dummy", tp.ident);
207                            quote!(#dummy_name)
208                        }
209                        GenericParam::Lifetime(lt) => {
210                            let lt = &lt.lifetime;
211                            quote!(#lt)
212                        }
213                        GenericParam::Const(c) => {
214                            let ident = &c.ident;
215                            quote!(#ident)
216                        }
217                    })
218                    .collect();
219
220                quote! {
221                    fn decl(cfg: &#crate_rename::Config) -> String {
222                        // Named dummies output the type param name (e.g. "T") instead of "any".
223                        // If the struct has non-Flow trait bounds, use #[flow(bound = "")] to override.
224                        #(#dummy_decls)*
225                        let inline = <#rust_ty<#(#full_generic_args),*> as #crate_rename::Flow>::inline(cfg);
226                        let generics = format!("<{}>", vec![#(#generics_str.to_owned()),*].join(", "));
227                        format!("type {}{generics} = {inline};", #flow_name)
228                    }
229                }
230            } else {
231                quote! {
232                    fn decl(cfg: &#crate_rename::Config) -> String {
233                        format!("type {} = {};", Self::name(cfg), Self::inline(cfg))
234                    }
235                }
236            }
237        };
238
239        // decl_concrete: always uses concrete types
240        let decl_concrete_fn = if self.is_opaque {
241            quote! {
242                fn decl_concrete(cfg: &#crate_rename::Config) -> String {
243                    Self::decl(cfg)
244                }
245            }
246        } else {
247            quote! {
248                fn decl_concrete(cfg: &#crate_rename::Config) -> String {
249                    format!("type {} = {};", Self::name(cfg), Self::inline(cfg))
250                }
251            }
252        };
253
254        // Build where clause
255        let mut bounds = generics.clone();
256        if let Some(extra) = &self.bound {
257            let where_clause = bounds.make_where_clause();
258            for pred in extra {
259                where_clause.predicates.push(pred.clone());
260            }
261        }
262        // Add Flow bound for all type params
263        for param in &generics.params {
264            if let GenericParam::Type(tp) = param {
265                let ident = &tp.ident;
266                let where_clause = bounds.make_where_clause();
267                where_clause
268                    .predicates
269                    .push(parse_quote!(#ident: #crate_rename::Flow));
270            }
271        }
272        let (impl_generics, ty_generics, where_clause) = bounds.split_for_impl();
273
274        // WithoutGenerics: if no generics, Self; otherwise replace all type params with Dummy
275        let without_generics = if generics.params.is_empty() {
276            quote!(Self)
277        } else {
278            let params = generics.params.iter().map(|p| match p {
279                GenericParam::Type(_) => quote!(#crate_rename::Dummy),
280                GenericParam::Lifetime(lt) => {
281                    let lt = &lt.lifetime;
282                    quote!(#lt)
283                }
284                GenericParam::Const(c) => {
285                    let ident = &c.ident;
286                    quote!(#ident)
287                }
288            });
289            quote!(#rust_ty<#(#params),*>)
290        };
291
292        // visit_dependencies
293        let dep_tokens = self.deps.to_tokens();
294        let visit_deps_fn = quote! {
295            fn visit_dependencies(v: &mut impl #crate_rename::TypeVisitor)
296            where
297                Self: 'static,
298            {
299                #dep_tokens
300            }
301        };
302
303        // visit_generics: iterate type params
304        let visit_generics_fn = {
305            let generic_visits: Vec<_> = generics
306                .type_params()
307                .map(|tp| {
308                    let ident = &tp.ident;
309                    quote! {
310                        v.visit::<#ident>();
311                        <#ident as #crate_rename::Flow>::visit_generics(v);
312                    }
313                })
314                .collect();
315
316            quote! {
317                fn visit_generics(v: &mut impl #crate_rename::TypeVisitor)
318                where
319                    Self: 'static,
320                {
321                    #(#generic_visits)*
322                }
323            }
324        };
325
326        // inline_flattened
327        let inline_flattened_fn = quote! {
328            fn inline_flattened(cfg: &#crate_rename::Config) -> String {
329                #inline_flattened
330            }
331        };
332
333        quote! {
334            #[automatically_derived]
335            impl #impl_generics #crate_rename::Flow for #rust_ty #ty_generics #where_clause {
336                type WithoutGenerics = #without_generics;
337                type OptionInnerType = Self;
338
339                #name_fn
340
341                fn inline(cfg: &#crate_rename::Config) -> String {
342                    #inline
343                }
344
345                #inline_flattened_fn
346                #decl_fn
347                #decl_concrete_fn
348                #docs_fn
349                #output_path_fn
350                #visit_deps_fn
351                #visit_generics_fn
352
353                const IS_ENUM: bool = #is_enum;
354            }
355
356            #export_test
357        }
358    }
359
360    fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
361        let crate_rename = &self.crate_rename;
362        let test_name = format_ident!("export_flow_bindings_{}", rust_ty);
363        let ty = if generics.params.is_empty() {
364            quote!(#rust_ty)
365        } else {
366            let dummies = generics.params.iter().map(|p| match p {
367                GenericParam::Type(_) => quote!(#crate_rename::Dummy),
368                GenericParam::Lifetime(lt) => {
369                    let lt = &lt.lifetime;
370                    quote!(#lt)
371                }
372                GenericParam::Const(c) => {
373                    let ident = &c.ident;
374                    quote!(#ident)
375                }
376            });
377            quote!(#rust_ty<#(#dummies),*>)
378        };
379
380        quote! {
381            #[cfg(test)]
382            #[test]
383            #[allow(non_snake_case)]
384            fn #test_name() {
385                let cfg = #crate_rename::Config::from_env();
386                <#ty as #crate_rename::Flow>::export_all(&cfg)
387                    .expect("could not export type");
388            }
389        }
390    }
391}
392
393/// Derive the `Flow` trait for a struct or enum.
394///
395/// # Container attributes
396/// - `#[flow(rename = "..")]` — Override the Flow type name
397/// - `#[flow(rename_all = "..")]` — Rename all fields (camelCase, snake_case, etc.)
398/// - `#[flow(export)]` — Generate a test that exports this type to disk
399/// - `#[flow(export_to = "..")]` — Custom export path
400/// - `#[flow(opaque)]` — Emit as `declare export opaque type Name` (fully opaque)
401/// - `#[flow(opaque = "string")]` — Emit as `declare export opaque type Name: string` (bounded)
402/// - `#[flow(tag = "..")]` — Tagged enum representation
403/// - `#[flow(content = "..")]` — Content field for adjacently tagged enums
404/// - `#[flow(untagged)]` — Untagged enum
405/// - `#[flow(bound = "..")]` — Additional where clause bounds
406///
407/// # Field attributes
408/// - `#[flow(rename = "..")]` — Rename this field
409/// - `#[flow(type = "..")]` — Override field type
410/// - `#[flow(skip)]` — Skip this field
411/// - `#[flow(optional)]` — Mark as optional
412/// - `#[flow(inline)]` — Inline the field type definition
413/// - `#[flow(flatten)]` — Flatten nested fields into parent
414#[proc_macro_derive(Flow, attributes(flow))]
415pub fn derive_flow(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
416    let input = parse_macro_input!(input as DeriveInput);
417    match derive_flow_impl(input) {
418        Ok(tokens) => tokens.into(),
419        Err(err) => err.to_compile_error().into(),
420    }
421}
422
423fn derive_flow_impl(input: DeriveInput) -> Result<TokenStream> {
424    let container = ContainerAttr::from_attrs(&input.attrs)?;
425    let ident = &input.ident;
426
427    let crate_rename = container
428        .crate_rename
429        .clone()
430        .unwrap_or_else(|| parse_quote!(::flowjs_rs));
431
432    let flow_name: Expr = match &container.rename {
433        Some(name) => parse_quote!(#name),
434        None => {
435            let name = ident.to_string();
436            parse_quote!(#name)
437        }
438    };
439
440    let docs: Vec<Expr> = input
441        .attrs
442        .iter()
443        .filter_map(|attr| {
444            if !attr.path().is_ident("doc") {
445                return None;
446            }
447            if let Meta::NameValue(nv) = &attr.meta {
448                if let Expr::Lit(lit) = &nv.value {
449                    if let Lit::Str(s) = &lit.lit {
450                        let val = s.value();
451                        let trimmed = val.trim();
452                        return Some(parse_quote!(#trimmed));
453                    }
454                }
455            }
456            None
457        })
458        .collect();
459
460    let mut deps = Dependencies::new(crate_rename.clone());
461
462    let (inline, inline_flattened, is_enum) = match &input.data {
463        Data::Struct(data) => {
464            let (inline, flattened) =
465                derive_struct(&crate_rename, &container, &data.fields, &mut deps)?;
466            (inline, flattened, quote!(false))
467        }
468        Data::Enum(data) => {
469            let inline = derive_enum(&crate_rename, &container, data, &mut deps)?;
470            let flattened = quote! {
471                format!("({})", Self::inline(cfg))
472            };
473            (inline, flattened, quote!(true))
474        }
475        Data::Union(_) => {
476            return Err(syn::Error::new(
477                ident.span(),
478                "Flow cannot be derived for unions",
479            ));
480        }
481    };
482
483    let (is_opaque, opaque_bound) = match &container.opaque {
484        Some(Some(bound)) => (true, Some(quote!(#bound))),
485        Some(None) => (true, None),
486        None => (false, None),
487    };
488
489    let derived = DerivedFlow {
490        crate_rename,
491        flow_name,
492        docs,
493        inline,
494        inline_flattened,
495        is_enum,
496        is_opaque,
497        opaque_bound,
498        export: container.export,
499        export_to: container.export_to.clone(),
500        bound: container.bound.clone(),
501        deps,
502    };
503
504    Ok(derived.into_impl(ident.clone(), input.generics.clone()))
505}
506
507fn derive_struct(
508    crate_rename: &Path,
509    container: &ContainerAttr,
510    fields: &Fields,
511    deps: &mut Dependencies,
512) -> Result<(TokenStream, TokenStream)> {
513    match fields {
514        Fields::Named(named) => {
515            let mut formatted_fields: Vec<TokenStream> = Vec::new();
516            let mut flattened_fields: Vec<TokenStream> = Vec::new();
517
518            for f in &named.named {
519                let field_attr = FieldAttr::from_attrs(&f.attrs)?;
520                if field_attr.skip {
521                    continue;
522                }
523
524                let field_name = f.ident.as_ref().unwrap();
525                let ty = &f.ty;
526
527                if field_attr.flatten {
528                    // Flatten: add transitive deps, push inline_flattened
529                    if field_attr.type_override.is_none() {
530                        deps.append_from(ty);
531                    }
532                    flattened_fields
533                        .push(quote!(<#ty as #crate_rename::Flow>::inline_flattened(cfg)));
534                    continue;
535                }
536
537                let name =
538                    utils::quote_property_name(&field_attr.rename.clone().unwrap_or_else(|| {
539                        let raw = field_name.to_string();
540                        container.rename_field(&raw)
541                    }));
542
543                // Resolve the effective type: `as` overrides the Rust type,
544                // `type` overrides with a literal string.
545                let effective_ty = field_attr.type_as.as_ref().unwrap_or(ty);
546
547                // Track dependencies
548                if field_attr.type_override.is_none() {
549                    if field_attr.inline {
550                        deps.append_from(effective_ty);
551                    } else {
552                        deps.push(effective_ty);
553                    }
554                }
555
556                let type_str = if let Some(override_ty) = &field_attr.type_override {
557                    quote!(#override_ty.to_owned())
558                } else if field_attr.inline {
559                    quote!(<#effective_ty as #crate_rename::Flow>::inline(cfg))
560                } else {
561                    quote!(<#effective_ty as #crate_rename::Flow>::name(cfg))
562                };
563
564                // Key-optional (`field?:`) only when explicitly marked or serde says omittable.
565                // `Option<T>` without skip_serializing_if is always-present-but-nullable (`+field: ?T`),
566                // NOT omittable (`+field?: ?T`). The `?T` nullability comes from Flow::name() for Option.
567                let is_omittable = field_attr.optional || field_attr.is_serde_optional();
568                let opt_marker = if is_omittable { "?" } else { "" };
569
570                formatted_fields.push(quote! {
571                    format!("  +{}{}: {},", #name, #opt_marker, #type_str)
572                });
573            }
574
575            // Combine normal fields and flattened fields
576            let inline = match (formatted_fields.len(), flattened_fields.len()) {
577                (0, 0) => quote!("{||}".to_owned()),
578                (_, 0) => quote! {{
579                    let fields = vec![#(#formatted_fields),*];
580                    format!("{{|\n{}\n|}}", fields.join("\n"))
581                }},
582                (0, 1) => {
583                    let flat = &flattened_fields[0];
584                    quote! {{
585                        let f = #flat;
586                        if f.starts_with('(') && f.ends_with(')') {
587                            f[1..f.len() - 1].trim().to_owned()
588                        } else {
589                            f.trim().to_owned()
590                        }
591                    }}
592                }
593                (0, _) => quote! {{
594                    let parts: Vec<String> = vec![#(#flattened_fields),*];
595                    parts.join(" & ")
596                }},
597                (_, _) => quote! {{
598                    let fields = vec![#(#formatted_fields),*];
599                    let base = format!("{{|\n{}\n|}}", fields.join("\n"));
600                    let flattened: Vec<String> = vec![#(#flattened_fields),*];
601                    format!("{} & {}", base, flattened.join(" & "))
602                }},
603            };
604
605            // inline_flattened always wraps in exact object (for use by parent flatten)
606            let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
607                (_, 0) => quote! {{
608                    let fields = vec![#(#formatted_fields),*];
609                    format!("{{|\n{}\n|}}", fields.join("\n"))
610                }},
611                (0, _) => quote! {{
612                    let parts: Vec<String> = vec![#(#flattened_fields),*];
613                    parts.join(" & ")
614                }},
615                (_, _) => quote! {{
616                    let fields = vec![#(#formatted_fields),*];
617                    let base = format!("{{|\n{}\n|}}", fields.join("\n"));
618                    let flattened: Vec<String> = vec![#(#flattened_fields),*];
619                    format!("{} & {}", base, flattened.join(" & "))
620                }},
621            };
622
623            Ok((inline, inline_flattened))
624        }
625        Fields::Unnamed(unnamed) => {
626            if unnamed.unnamed.len() == 1 {
627                // Newtype — inline the inner type
628                let ty = &unnamed.unnamed[0].ty;
629                deps.push(ty);
630                let inline = quote!(<#ty as #crate_rename::Flow>::inline(cfg));
631                let flattened = quote! {
632                    format!("({})", <#ty as #crate_rename::Flow>::inline(cfg))
633                };
634                Ok((inline, flattened))
635            } else {
636                // Tuple struct → Flow tuple
637                let elems: Vec<TokenStream> = unnamed
638                    .unnamed
639                    .iter()
640                    .map(|f| {
641                        let ty = &f.ty;
642                        deps.push(ty);
643                        quote!(<#ty as #crate_rename::Flow>::inline(cfg))
644                    })
645                    .collect();
646                let inline = quote! {{
647                    let elems: Vec<String> = vec![#(#elems),*];
648                    format!("[{}]", elems.join(", "))
649                }};
650                let flattened = quote! {
651                    format!("({})", Self::inline(cfg))
652                };
653                Ok((inline, flattened))
654            }
655        }
656        Fields::Unit => {
657            let inline = quote!(#crate_rename::flow_type::VOID.to_owned());
658            let flattened = quote!(#crate_rename::flow_type::VOID.to_owned());
659            Ok((inline, flattened))
660        }
661    }
662}
663
664fn derive_enum(
665    crate_rename: &Path,
666    container: &ContainerAttr,
667    data: &syn::DataEnum,
668    deps: &mut Dependencies,
669) -> Result<TokenStream> {
670    if data.variants.is_empty() {
671        return Ok(quote!(#crate_rename::flow_type::EMPTY.to_owned()));
672    }
673
674    let is_untagged = container.untagged;
675    let tag = &container.tag.as_deref().map(utils::quote_property_name);
676    let content = &container.content.as_deref().map(utils::quote_property_name);
677
678    let mut variant_defs: Vec<TokenStream> = Vec::new();
679    for v in &data.variants {
680        let variant_attr = VariantAttr::from_attrs(&v.attrs)?;
681        if variant_attr.skip {
682            continue;
683        }
684
685        let variant_name_raw = variant_attr.rename.clone().unwrap_or_else(|| {
686            let raw = v.ident.to_string();
687            container.rename_variant(&raw)
688        });
689        // Escaped version for use inside string literal values: 'VariantName'
690        let variant_name = utils::escape_string_literal(&variant_name_raw);
691        // Quoted version for use as an object key (externally-tagged enum)
692        let variant_key = utils::quote_property_name(&variant_name_raw);
693
694        let def = match &v.fields {
695            Fields::Unit => {
696                if is_untagged {
697                    quote!(#crate_rename::flow_type::VOID.to_owned())
698                } else if let Some(tag_field) = tag {
699                    quote!(format!("{{| +{}: '{}' |}}", #tag_field, #variant_name))
700                } else {
701                    quote!(format!("'{}'", #variant_name))
702                }
703            }
704            Fields::Unnamed(unnamed) => {
705                if unnamed.unnamed.len() == 1 {
706                    let ty = &unnamed.unnamed[0].ty;
707                    deps.push(ty);
708                    let inner = quote!(<#ty as #crate_rename::Flow>::inline(cfg));
709                    if is_untagged {
710                        inner
711                    } else if let (Some(tag_field), Some(content_field)) = (tag, content) {
712                        quote!(format!(
713                            "{{| +{}: '{}', +{}: {} |}}",
714                            #tag_field, #variant_name, #content_field, #inner
715                        ))
716                    } else if let Some(tag_field) = tag {
717                        quote!(format!(
718                            "{{| +{}: '{}' |}} & {}",
719                            #tag_field, #variant_name, #inner
720                        ))
721                    } else {
722                        quote!(format!(
723                            "{{| {}: {} |}}",
724                            #variant_key, #inner
725                        ))
726                    }
727                } else {
728                    // Multi-field tuple variant
729                    let elems: Vec<TokenStream> = unnamed
730                        .unnamed
731                        .iter()
732                        .map(|f| {
733                            let ty = &f.ty;
734                            deps.push(ty);
735                            quote!(<#ty as #crate_rename::Flow>::inline(cfg))
736                        })
737                        .collect();
738                    let tuple = quote! {{
739                        let elems: Vec<String> = vec![#(#elems),*];
740                        format!("[{}]", elems.join(", "))
741                    }};
742                    if is_untagged {
743                        tuple
744                    } else if let (Some(tag_field), Some(content_field)) = (tag, content) {
745                        quote!(format!(
746                            "{{| +{}: '{}', +{}: {} |}}",
747                            #tag_field, #variant_name, #content_field, #tuple
748                        ))
749                    } else {
750                        quote!(format!(
751                            "{{| {}: {} |}}",
752                            #variant_key, #tuple
753                        ))
754                    }
755                }
756            }
757            Fields::Named(named) => {
758                let mut field_defs: Vec<TokenStream> = Vec::new();
759                let mut flattened_defs: Vec<TokenStream> = Vec::new();
760                for f in &named.named {
761                    let field_attr = FieldAttr::from_attrs(&f.attrs)?;
762                    if field_attr.skip {
763                        continue;
764                    }
765                    let ty = &f.ty;
766
767                    if field_attr.flatten {
768                        if field_attr.type_override.is_none() {
769                            deps.append_from(ty);
770                        }
771                        flattened_defs
772                            .push(quote!(<#ty as #crate_rename::Flow>::inline_flattened(cfg)));
773                        continue;
774                    }
775
776                    let field_name = f.ident.as_ref().unwrap();
777                    let name =
778                        utils::quote_property_name(&field_attr.rename.clone().unwrap_or_else(
779                            || container.rename_variant_field(&field_name.to_string()),
780                        ));
781                    if field_attr.type_override.is_none() {
782                        if field_attr.inline {
783                            deps.append_from(ty);
784                        } else {
785                            deps.push(ty);
786                        }
787                    }
788                    let type_str = if let Some(override_ty) = &field_attr.type_override {
789                        quote!(#override_ty.to_owned())
790                    } else if field_attr.inline {
791                        quote!(<#ty as #crate_rename::Flow>::inline(cfg))
792                    } else {
793                        quote!(<#ty as #crate_rename::Flow>::name(cfg))
794                    };
795                    let is_omittable = field_attr.optional || field_attr.is_serde_optional();
796                    let opt_marker = if is_omittable { "?" } else { "" };
797                    field_defs.push(quote!(format!("+{}{}: {}", #name, #opt_marker, #type_str)));
798                }
799
800                let obj = if flattened_defs.is_empty() {
801                    quote! {{
802                        let fields: Vec<String> = vec![#(#field_defs),*];
803                        format!("{{| {} |}}", fields.join(", "))
804                    }}
805                } else if field_defs.is_empty() {
806                    quote! {{
807                        let parts: Vec<String> = vec![#(#flattened_defs),*];
808                        parts.join(" & ")
809                    }}
810                } else {
811                    quote! {{
812                        let fields: Vec<String> = vec![#(#field_defs),*];
813                        let base = format!("{{| {} |}}", fields.join(", "));
814                        let flattened: Vec<String> = vec![#(#flattened_defs),*];
815                        format!("{} & {}", base, flattened.join(" & "))
816                    }}
817                };
818
819                if is_untagged {
820                    obj
821                } else if let (Some(tag_field), Some(content_field)) = (tag, content) {
822                    quote!(format!(
823                        "{{| +{}: '{}', +{}: {} |}}",
824                        #tag_field, #variant_name, #content_field, #obj
825                    ))
826                } else if let Some(tag_field) = tag {
827                    // Internally tagged: inject tag field into the object
828                    // Build the tagged object by prepending the tag to the field list
829                    let tag_field_def = quote!(format!("+{}: '{}'", #tag_field, #variant_name));
830
831                    if flattened_defs.is_empty() {
832                        // Simple case: all fields are regular, build a single exact object
833                        let all_fields: Vec<_> = std::iter::once(tag_field_def.clone())
834                            .chain(field_defs.iter().cloned())
835                            .collect();
836                        quote! {{
837                            let fields: Vec<String> = vec![#(#all_fields),*];
838                            format!("{{| {} |}}", fields.join(", "))
839                        }}
840                    } else {
841                        // Has flattened fields: tag goes in base object, then intersect
842                        let base_fields: Vec<_> = std::iter::once(tag_field_def.clone())
843                            .chain(field_defs.iter().cloned())
844                            .collect();
845                        quote! {{
846                            let fields: Vec<String> = vec![#(#base_fields),*];
847                            let base = format!("{{| {} |}}", fields.join(", "));
848                            let flattened: Vec<String> = vec![#(#flattened_defs),*];
849                            format!("{} & {}", base, flattened.join(" & "))
850                        }}
851                    }
852                } else {
853                    quote!(format!(
854                        "{{| {}: {} |}}",
855                        #variant_key, #obj
856                    ))
857                }
858            }
859        };
860        variant_defs.push(def);
861    }
862
863    if variant_defs.is_empty() {
864        return Ok(quote!(#crate_rename::flow_type::EMPTY.to_owned()));
865    }
866
867    Ok(quote! {{
868        let variants: Vec<String> = vec![#(#variant_defs),*];
869        variants.join(" | ")
870    }})
871}