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, spanned::Spanned, Data, DeriveInput, Expr, Fields,
12    GenericParam, Generics, Lit, Meta, Path, Result, Type, WherePredicate,
13};
14
15mod attr;
16mod config;
17mod utils;
18
19use attr::{ContainerAttr, FieldAttr, FlowEnumRepr, VariantAttr};
20
21/// Collected dependency types for visit_dependencies generation.
22struct Dependencies {
23    crate_rename: Path,
24    /// Types to visit directly: `v.visit::<T>()`
25    types: Vec<Type>,
26    /// Types whose dependencies to visit transitively: `<T as Flow>::visit_dependencies(v)`
27    transitive: Vec<Type>,
28    /// Types whose generics to visit: `<T as Flow>::visit_generics(v)`
29    generics: Vec<Type>,
30}
31
32impl Dependencies {
33    fn new(crate_rename: Path) -> Self {
34        Self {
35            crate_rename,
36            types: Vec::new(),
37            transitive: Vec::new(),
38            generics: Vec::new(),
39        }
40    }
41
42    /// Add a field type as a dependency (visit the type + its generics).
43    fn push(&mut self, ty: &Type) {
44        self.types.push(ty.clone());
45        self.generics.push(ty.clone());
46    }
47
48    /// Add a field type's transitive deps only (for inline/flatten).
49    fn append_from(&mut self, ty: &Type) {
50        self.transitive.push(ty.clone());
51    }
52
53    fn to_tokens(&self) -> TokenStream {
54        let crate_rename = &self.crate_rename;
55        let visit_types = self.types.iter().map(|ty| {
56            quote! { v.visit::<#ty>(); }
57        });
58        let visit_transitive = self.transitive.iter().map(|ty| {
59            quote! { <#ty as #crate_rename::Flow>::visit_dependencies(v); }
60        });
61        let visit_generics = self.generics.iter().map(|ty| {
62            quote! { <#ty as #crate_rename::Flow>::visit_generics(v); }
63        });
64
65        quote! {
66            #(#visit_types)*
67            #(#visit_generics)*
68            #(#visit_transitive)*
69        }
70    }
71}
72
73struct DerivedFlow {
74    crate_rename: Path,
75    flow_name: Expr,
76    docs: Vec<Expr>,
77    inline: TokenStream,
78    inline_flattened: TokenStream,
79    is_enum: TokenStream,
80    is_opaque: bool,
81    opaque_bound: Option<TokenStream>,
82    /// Override decl() for Flow enum declarations.
83    enum_decl_override: Option<TokenStream>,
84    export: bool,
85    export_to: Option<Expr>,
86    bound: Option<Vec<WherePredicate>>,
87    deps: Dependencies,
88    /// `#[flow(concrete(T = i32, ...))]` — replace generic params with concrete types.
89    concrete: std::collections::HashMap<Ident, Type>,
90}
91
92impl DerivedFlow {
93    fn into_impl(self, rust_ty: Ident, generics: Generics) -> TokenStream {
94        let export_test = self
95            .export
96            .then(|| self.generate_export_test(&rust_ty, &generics));
97
98        let output_path_fn = {
99            let flow_name = &self.flow_name;
100            // output_path returns a base path WITHOUT extension.
101            // The extension is added at export time from Config::file_extension().
102            // If export_to specifies a full file path (with extension), it's used as-is.
103            let path_string = match &self.export_to {
104                Some(dir_or_file) => quote! {{
105                    let dir_or_file = format!("{}", #dir_or_file);
106                    if dir_or_file.ends_with('/') {
107                        // Directory — append type name (no extension; Config adds it)
108                        format!("{dir_or_file}{}", #flow_name)
109                    } else {
110                        // Full file path — keep as-is (has extension)
111                        format!("{dir_or_file}")
112                    }
113                }},
114                // Default — just the type name (no extension; Config adds it)
115                None => quote![format!("{}", #flow_name)],
116            };
117            quote! {
118                fn output_path() -> Option<std::path::PathBuf> {
119                    Some(std::path::PathBuf::from(#path_string))
120                }
121            }
122        };
123
124        let crate_rename = &self.crate_rename;
125        let flow_name = &self.flow_name;
126        let inline = &self.inline;
127        let inline_flattened = &self.inline_flattened;
128        let is_enum = &self.is_enum;
129
130        let docs_fn = if self.docs.is_empty() {
131            quote! { fn docs() -> Option<String> { None } }
132        } else {
133            let docs = &self.docs;
134            quote! {
135                fn docs() -> Option<String> {
136                    Some([#(#docs),*].join("\n"))
137                }
138            }
139        };
140
141        // Generate name() with generic parameters (excluding concrete ones)
142        let name_fn = {
143            let generic_names: Vec<_> = generics
144                .type_params()
145                .filter(|tp| !self.concrete.contains_key(&tp.ident))
146                .map(|tp| {
147                    let ident = &tp.ident;
148                    quote!(<#ident as #crate_rename::Flow>::name(cfg))
149                })
150                .collect();
151
152            if generic_names.is_empty() {
153                quote! {
154                    fn name(cfg: &#crate_rename::Config) -> String {
155                        #flow_name.to_owned()
156                    }
157                }
158            } else {
159                quote! {
160                    fn name(cfg: &#crate_rename::Config) -> String {
161                        format!("{}<{}>", #flow_name, vec![#(#generic_names),*].join(", "))
162                    }
163                }
164            }
165        };
166
167        // Generate decl() — for generic types, create placeholder dummy types
168        let decl_fn = if let Some(enum_decl) = &self.enum_decl_override {
169            quote! {
170                fn decl(cfg: &#crate_rename::Config) -> String {
171                    #enum_decl
172                }
173            }
174        } else if self.is_opaque {
175            // Opaque types emit the full body (visible in the defining module).
176            // `opaque type Foo = body` or `opaque type Foo: bound = body`
177            // The `export` prefix is added by format_decl in export.rs.
178            let has_generics = generics.type_params().next().is_some();
179
180            if has_generics {
181                // Generic opaque: opaque type Foo<T> = {| thing: T |}
182                let generic_idents: Vec<_> = generics.type_params().map(|tp| &tp.ident).collect();
183                let dummy_decls: Vec<_> = generic_idents
184                    .iter()
185                    .map(|ident| {
186                        let dummy_name = format_ident!("{}Dummy", ident);
187                        quote! {
188                            struct #dummy_name;
189                            impl #crate_rename::Flow for #dummy_name {
190                                type WithoutGenerics = Self;
191                                type OptionInnerType = Self;
192                                fn name(_: &#crate_rename::Config) -> String {
193                                    stringify!(#ident).to_owned()
194                                }
195                                fn inline(cfg: &#crate_rename::Config) -> String {
196                                    Self::name(cfg)
197                                }
198                            }
199                        }
200                    })
201                    .collect();
202                let generics_str: Vec<_> = generic_idents
203                    .iter()
204                    .map(|ident| quote!(stringify!(#ident)))
205                    .collect();
206                let full_generic_args: Vec<_> = generics
207                    .params
208                    .iter()
209                    .map(|p| match p {
210                        GenericParam::Type(tp) => {
211                            let dummy_name = format_ident!("{}Dummy", tp.ident);
212                            quote!(#dummy_name)
213                        }
214                        GenericParam::Lifetime(lt) => {
215                            let lt = &lt.lifetime;
216                            quote!(#lt)
217                        }
218                        GenericParam::Const(c) => {
219                            let ident = &c.ident;
220                            quote!(#ident)
221                        }
222                    })
223                    .collect();
224
225                let bound_part = self.opaque_bound
226                    .map(|b| quote! { format!(": {}", #b) })
227                    .unwrap_or_else(|| quote! { String::new() });
228
229                quote! {
230                    fn decl(cfg: &#crate_rename::Config) -> String {
231                        #(#dummy_decls)*
232                        let inline = <#rust_ty<#(#full_generic_args),*> as #crate_rename::Flow>::inline(cfg);
233                        let generics = format!("<{}>", vec![#(#generics_str.to_owned()),*].join(", "));
234                        let bound = #bound_part;
235                        format!("opaque type {}{generics}{bound} = {inline};", #flow_name)
236                    }
237                }
238            } else {
239                let bound_part = self.opaque_bound
240                    .map(|b| quote! { format!(": {}", #b) })
241                    .unwrap_or_else(|| quote! { String::new() });
242
243                quote! {
244                    fn decl(cfg: &#crate_rename::Config) -> String {
245                        let inline = Self::inline(cfg);
246                        let bound = #bound_part;
247                        format!("opaque type {}{bound} = {inline};", #flow_name)
248                    }
249                }
250            }
251        } else {
252            let has_generics = generics.type_params().next().is_some();
253            if has_generics {
254                // For generic types: create dummy types, get inline from WithoutGenerics
255                // Non-concrete params get dummy structs; concrete params use their concrete type.
256
257                // Generate dummy type declarations only for NON-concrete generic params
258                let dummy_decls: Vec<_> = generics
259                    .type_params()
260                    .filter(|tp| !self.concrete.contains_key(&tp.ident))
261                    .map(|tp| {
262                        let ident = &tp.ident;
263                        let dummy_name = format_ident!("{}Dummy", ident);
264                        quote! {
265                            struct #dummy_name;
266                            impl #crate_rename::Flow for #dummy_name {
267                                type WithoutGenerics = Self;
268                                type OptionInnerType = Self;
269                                fn name(_: &#crate_rename::Config) -> String {
270                                    stringify!(#ident).to_owned()
271                                }
272                                fn inline(cfg: &#crate_rename::Config) -> String {
273                                    Self::name(cfg)
274                                }
275                            }
276                        }
277                    })
278                    .collect();
279
280                // Generics string only includes non-concrete params
281                let generics_str: Vec<_> = generics
282                    .type_params()
283                    .filter(|tp| !self.concrete.contains_key(&tp.ident))
284                    .map(|tp| {
285                        let ident = &tp.ident;
286                        quote!(stringify!(#ident))
287                    })
288                    .collect();
289
290                // Build full generic args for instantiation:
291                // - non-concrete type params → named dummies
292                // - concrete type params → the concrete type
293                // - lifetime/const params → pass through
294                let full_generic_args: Vec<_> = generics
295                    .params
296                    .iter()
297                    .map(|p| match p {
298                        GenericParam::Type(tp) => {
299                            if let Some(concrete_ty) = self.concrete.get(&tp.ident) {
300                                quote!(#concrete_ty)
301                            } else {
302                                let dummy_name = format_ident!("{}Dummy", tp.ident);
303                                quote!(#dummy_name)
304                            }
305                        }
306                        GenericParam::Lifetime(lt) => {
307                            let lt = &lt.lifetime;
308                            quote!(#lt)
309                        }
310                        GenericParam::Const(c) => {
311                            let ident = &c.ident;
312                            quote!(#ident)
313                        }
314                    })
315                    .collect();
316
317                // If all type params are concrete, no generics suffix in the declaration
318                let decl_format = if generics_str.is_empty() {
319                    quote! {
320                        format!("type {} = {inline};", #flow_name)
321                    }
322                } else {
323                    quote! {
324                        let generics = format!("<{}>", vec![#(#generics_str.to_owned()),*].join(", "));
325                        format!("type {}{generics} = {inline};", #flow_name)
326                    }
327                };
328
329                quote! {
330                    fn decl(cfg: &#crate_rename::Config) -> String {
331                        // Named dummies output the type param name (e.g. "T") instead of "any".
332                        // Concrete params use the specified concrete type directly.
333                        // If the struct has non-Flow trait bounds, use #[flow(bound = "")] to override.
334                        #(#dummy_decls)*
335                        let inline = <#rust_ty<#(#full_generic_args),*> as #crate_rename::Flow>::inline(cfg);
336                        #decl_format
337                    }
338                }
339            } else {
340                quote! {
341                    fn decl(cfg: &#crate_rename::Config) -> String {
342                        format!("type {} = {};", Self::name(cfg), Self::inline(cfg))
343                    }
344                }
345            }
346        };
347
348        // decl_concrete: always uses concrete types
349        let decl_concrete_fn = if self.is_opaque || self.enum_decl_override.is_some() {
350            quote! {
351                fn decl_concrete(cfg: &#crate_rename::Config) -> String {
352                    Self::decl(cfg)
353                }
354            }
355        } else {
356            quote! {
357                fn decl_concrete(cfg: &#crate_rename::Config) -> String {
358                    format!("type {} = {};", Self::name(cfg), Self::inline(cfg))
359                }
360            }
361        };
362
363        // Build where clause
364        let mut bounds = generics.clone();
365        if let Some(extra) = &self.bound {
366            let where_clause = bounds.make_where_clause();
367            for pred in extra {
368                where_clause.predicates.push(pred.clone());
369            }
370        }
371        // Add Flow bound for type params that are NOT concrete
372        for param in &generics.params {
373            if let GenericParam::Type(tp) = param {
374                if self.concrete.contains_key(&tp.ident) {
375                    continue;
376                }
377                let ident = &tp.ident;
378                let where_clause = bounds.make_where_clause();
379                where_clause
380                    .predicates
381                    .push(parse_quote!(#ident: #crate_rename::Flow));
382            }
383        }
384        let (impl_generics, ty_generics, where_clause) = bounds.split_for_impl();
385
386        // WithoutGenerics: if no generics, Self; otherwise replace type params with Dummy
387        // (concrete params use the concrete type instead of Dummy)
388        let without_generics = if generics.params.is_empty() {
389            quote!(Self)
390        } else {
391            let params = generics.params.iter().map(|p| match p {
392                GenericParam::Type(tp) => {
393                    if let Some(concrete_ty) = self.concrete.get(&tp.ident) {
394                        quote!(#concrete_ty)
395                    } else {
396                        quote!(#crate_rename::Dummy)
397                    }
398                }
399                GenericParam::Lifetime(lt) => {
400                    let lt = &lt.lifetime;
401                    quote!(#lt)
402                }
403                GenericParam::Const(c) => {
404                    let ident = &c.ident;
405                    quote!(#ident)
406                }
407            });
408            quote!(#rust_ty<#(#params),*>)
409        };
410
411        // visit_dependencies
412        let dep_tokens = self.deps.to_tokens();
413        let visit_deps_fn = quote! {
414            fn visit_dependencies(v: &mut impl #crate_rename::TypeVisitor)
415            where
416                Self: 'static,
417            {
418                #dep_tokens
419            }
420        };
421
422        // visit_generics: iterate type params (skip concrete ones)
423        let visit_generics_fn = {
424            let generic_visits: Vec<_> = generics
425                .type_params()
426                .filter(|tp| !self.concrete.contains_key(&tp.ident))
427                .map(|tp| {
428                    let ident = &tp.ident;
429                    quote! {
430                        v.visit::<#ident>();
431                        <#ident as #crate_rename::Flow>::visit_generics(v);
432                    }
433                })
434                .collect();
435
436            quote! {
437                fn visit_generics(v: &mut impl #crate_rename::TypeVisitor)
438                where
439                    Self: 'static,
440                {
441                    #(#generic_visits)*
442                }
443            }
444        };
445
446        // inline_flattened
447        let inline_flattened_fn = quote! {
448            fn inline_flattened(cfg: &#crate_rename::Config) -> String {
449                #inline_flattened
450            }
451        };
452
453        quote! {
454            #[automatically_derived]
455            impl #impl_generics #crate_rename::Flow for #rust_ty #ty_generics #where_clause {
456                type WithoutGenerics = #without_generics;
457                type OptionInnerType = Self;
458
459                #name_fn
460
461                fn inline(cfg: &#crate_rename::Config) -> String {
462                    #inline
463                }
464
465                #inline_flattened_fn
466                #decl_fn
467                #decl_concrete_fn
468                #docs_fn
469                #output_path_fn
470                #visit_deps_fn
471                #visit_generics_fn
472
473                const IS_ENUM: bool = #is_enum;
474            }
475
476            #export_test
477        }
478    }
479
480    fn generate_export_test(&self, rust_ty: &Ident, generics: &Generics) -> TokenStream {
481        let crate_rename = &self.crate_rename;
482        let test_name = format_ident!("export_flow_bindings_{}", rust_ty);
483        let ty = if generics.params.is_empty() {
484            quote!(#rust_ty)
485        } else {
486            let dummies = generics.params.iter().map(|p| match p {
487                GenericParam::Type(tp) => {
488                    if let Some(concrete_ty) = self.concrete.get(&tp.ident) {
489                        quote!(#concrete_ty)
490                    } else {
491                        quote!(#crate_rename::Dummy)
492                    }
493                }
494                GenericParam::Lifetime(lt) => {
495                    let lt = &lt.lifetime;
496                    quote!(#lt)
497                }
498                GenericParam::Const(c) => {
499                    let ident = &c.ident;
500                    quote!(#ident)
501                }
502            });
503            quote!(#rust_ty<#(#dummies),*>)
504        };
505
506        quote! {
507            #[cfg(test)]
508            #[test]
509            #[allow(non_snake_case)]
510            fn #test_name() {
511                let cfg = #crate_rename::Config::from_env();
512                <#ty as #crate_rename::Flow>::export_all(&cfg)
513                    .expect("could not export type");
514            }
515        }
516    }
517}
518
519/// Derive the `Flow` trait for a struct or enum.
520///
521/// # Container attributes
522/// - `#[flow(rename = "..")]` — Override the Flow type name
523/// - `#[flow(rename_all = "..")]` — Rename all fields (camelCase, snake_case, etc.)
524/// - `#[flow(export)]` — Generate a test that exports this type to disk
525/// - `#[flow(export_to = "..")]` — Custom export path
526/// - `#[flow(opaque)]` — Emit as `declare export opaque type Name` (fully opaque)
527/// - `#[flow(opaque = "string")]` — Emit as `declare export opaque type Name: string` (bounded)
528/// - `#[flow(tag = "..")]` — Tagged enum representation
529/// - `#[flow(content = "..")]` — Content field for adjacently tagged enums
530/// - `#[flow(untagged)]` — Untagged enum
531/// - `#[flow(bound = "..")]` — Additional where clause bounds
532///
533/// # Field attributes
534/// - `#[flow(rename = "..")]` — Rename this field
535/// - `#[flow(type = "..")]` — Override field type
536/// - `#[flow(skip)]` — Skip this field
537/// - `#[flow(optional)]` — Mark as optional
538/// - `#[flow(inline)]` — Inline the field type definition
539/// - `#[flow(flatten)]` — Flatten nested fields into parent
540#[proc_macro_derive(Flow, attributes(flow))]
541pub fn derive_flow(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
542    let input = parse_macro_input!(input as DeriveInput);
543    match derive_flow_impl(input) {
544        Ok(tokens) => tokens.into(),
545        Err(err) => err.to_compile_error().into(),
546    }
547}
548
549fn derive_flow_impl(input: DeriveInput) -> Result<TokenStream> {
550    let container = ContainerAttr::from_attrs(&input.attrs)?;
551    let ident = &input.ident;
552
553    let crate_rename = container
554        .crate_rename
555        .clone()
556        .unwrap_or_else(|| parse_quote!(::flowjs_rs));
557
558    let flow_name: Expr = match &container.rename {
559        Some(name) => parse_quote!(#name),
560        None => {
561            let name = ident.to_string();
562            parse_quote!(#name)
563        }
564    };
565
566    let docs: Vec<Expr> = input
567        .attrs
568        .iter()
569        .filter_map(|attr| {
570            if !attr.path().is_ident("doc") {
571                return None;
572            }
573            if let Meta::NameValue(nv) = &attr.meta {
574                if let Expr::Lit(lit) = &nv.value {
575                    if let Lit::Str(s) = &lit.lit {
576                        let val = s.value();
577                        let trimmed = val.trim();
578                        return Some(parse_quote!(#trimmed));
579                    }
580                }
581            }
582            None
583        })
584        .collect();
585
586    let mut deps = Dependencies::new(crate_rename.clone());
587
588    // Container-level type/as override — short-circuits all other codegen
589    if let Some(type_str) = &container.type_override {
590        let inline = quote!(#type_str.to_owned());
591        let derived = DerivedFlow {
592            crate_rename: crate_rename.clone(),
593            flow_name,
594            docs,
595            inline: inline.clone(),
596            inline_flattened: quote!(format!("({})", #type_str)),
597            is_enum: quote!(false),
598            is_opaque: false,
599            opaque_bound: None,
600            enum_decl_override: None,
601            export: container.export,
602            export_to: container.export_to.clone(),
603            bound: container.bound.clone(),
604            deps,
605            concrete: container.concrete.clone(),
606        };
607        return Ok(derived.into_impl(ident.clone(), input.generics.clone()));
608    }
609
610    if let Some(as_ty) = &container.type_as {
611        let cr = &crate_rename;
612        let inline = quote!(<#as_ty as #cr::Flow>::inline(cfg));
613        deps.push(&syn::parse_quote!(#as_ty));
614        let derived = DerivedFlow {
615            crate_rename: crate_rename.clone(),
616            flow_name,
617            docs,
618            inline: inline.clone(),
619            inline_flattened: quote!(format!("({})", <#as_ty as #cr::Flow>::inline(cfg))),
620            is_enum: quote!(false),
621            is_opaque: false,
622            opaque_bound: None,
623            enum_decl_override: None,
624            export: container.export,
625            export_to: container.export_to.clone(),
626            bound: container.bound.clone(),
627            deps,
628            concrete: container.concrete.clone(),
629        };
630        return Ok(derived.into_impl(ident.clone(), input.generics.clone()));
631    }
632
633    let (inline, inline_flattened, is_enum, enum_decl_override) = match &input.data {
634        Data::Struct(data) => {
635            let (inline, flattened) =
636                derive_struct(&crate_rename, &container, &data.fields, &mut deps)?;
637            (inline, flattened, quote!(false), None)
638        }
639        Data::Enum(data) => {
640            if let Some(repr) = &container.flow_enum {
641                // Validate: flow_enum is incompatible with generics
642                if input.generics.type_params().next().is_some() {
643                    return Err(syn::Error::new(
644                        ident.span(),
645                        "#[flow(flow_enum)] cannot be used on generic enums",
646                    ));
647                }
648                // Validate: flow_enum is incompatible with tagging/opaque
649                if container.tag.is_some() {
650                    return Err(syn::Error::new(
651                        ident.span(),
652                        "#[flow(flow_enum)] cannot be combined with #[flow(tag = \"...\")]",
653                    ));
654                }
655                if container.content.is_some() {
656                    return Err(syn::Error::new(
657                        ident.span(),
658                        "#[flow(flow_enum)] cannot be combined with #[flow(content = \"...\")]",
659                    ));
660                }
661                if container.untagged {
662                    return Err(syn::Error::new(
663                        ident.span(),
664                        "#[flow(flow_enum)] cannot be combined with #[flow(untagged)]",
665                    ));
666                }
667                if container.opaque.is_some() {
668                    return Err(syn::Error::new(
669                        ident.span(),
670                        "#[flow(flow_enum)] cannot be combined with #[flow(opaque)]",
671                    ));
672                }
673                let (inline, flattened, enum_decl) =
674                    derive_flow_enum(&container, data, repr)?;
675                (inline, flattened, quote!(true), Some(enum_decl))
676            } else {
677                let inline = derive_enum(&crate_rename, &container, data, &mut deps)?;
678                let flattened = quote! {
679                    format!("({})", Self::inline(cfg))
680                };
681                (inline, flattened, quote!(true), None)
682            }
683        }
684        Data::Union(_) => {
685            return Err(syn::Error::new(
686                ident.span(),
687                "Flow cannot be derived for unions",
688            ));
689        }
690    };
691
692    let (is_opaque, opaque_bound) = match &container.opaque {
693        Some(Some(bound)) => (true, Some(quote!(#bound))),
694        Some(None) => (true, None),
695        None => {
696            // Auto-opaque newtypes when configured in Cargo.toml:
697            // [package.metadata.flowjs-rs]
698            // opaque_newtypes = true
699            let is_newtype = matches!(&input.data, Data::Struct(s) if matches!(&s.fields, Fields::Unnamed(u) if u.unnamed.len() == 1));
700            if is_newtype && config::project_config().opaque_newtypes {
701                // Use the inner type as the opaque bound
702                if let Data::Struct(s) = &input.data {
703                    if let Fields::Unnamed(u) = &s.fields {
704                        let inner_ty = &u.unnamed[0].ty;
705                        let bound_str = quote!(<#inner_ty as #crate_rename::Flow>::name(cfg));
706                        (true, Some(bound_str))
707                    } else {
708                        (false, None)
709                    }
710                } else {
711                    (false, None)
712                }
713            } else {
714                (false, None)
715            }
716        }
717    };
718
719    let derived = DerivedFlow {
720        crate_rename,
721        flow_name,
722        docs,
723        inline,
724        inline_flattened,
725        is_enum,
726        is_opaque,
727        opaque_bound,
728        enum_decl_override,
729        export: container.export,
730        export_to: container.export_to.clone(),
731        bound: container.bound.clone(),
732        deps,
733        concrete: container.concrete.clone(),
734    };
735
736    Ok(derived.into_impl(ident.clone(), input.generics.clone()))
737}
738
739fn derive_struct(
740    crate_rename: &Path,
741    container: &ContainerAttr,
742    fields: &Fields,
743    deps: &mut Dependencies,
744) -> Result<(TokenStream, TokenStream)> {
745    match fields {
746        Fields::Named(named) => {
747            let mut formatted_fields: Vec<TokenStream> = Vec::new();
748            let mut flattened_fields: Vec<TokenStream> = Vec::new();
749
750            for f in &named.named {
751                let field_attr = FieldAttr::from_attrs(&f.attrs)?;
752                if field_attr.skip {
753                    continue;
754                }
755
756                let field_name = f.ident.as_ref().unwrap();
757                let ty = &f.ty;
758
759                if field_attr.flatten {
760                    // Flatten: add transitive deps, push inline_flattened
761                    if field_attr.type_override.is_none() {
762                        deps.append_from(ty);
763                    }
764                    flattened_fields
765                        .push(quote!(<#ty as #crate_rename::Flow>::inline_flattened(cfg)));
766                    continue;
767                }
768
769                let name =
770                    utils::quote_property_name(&field_attr.rename.clone().unwrap_or_else(|| {
771                        let raw = field_name.to_string();
772                        container.rename_field(&raw)
773                    }));
774
775                // Resolve the effective type: `as` overrides the Rust type,
776                // `type` overrides with a literal string.
777                let effective_ty = field_attr.type_as.as_ref().unwrap_or(ty);
778
779                // Track dependencies
780                if field_attr.type_override.is_none() {
781                    if field_attr.inline {
782                        deps.append_from(effective_ty);
783                    } else {
784                        deps.push(effective_ty);
785                    }
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!(<#effective_ty as #crate_rename::Flow>::inline(cfg))
792                } else {
793                    quote!(<#effective_ty as #crate_rename::Flow>::name(cfg))
794                };
795
796                // Key-optional (`field?:`) only when explicitly marked or serde says omittable.
797                // `Option<T>` without skip_serializing_if is always-present-but-nullable (`+field: ?T`),
798                // NOT omittable (`+field?: ?T`). The `?T` nullability comes from Flow::name() for Option.
799                let is_omittable = field_attr.is_optional(&container.optional_fields);
800                let opt_marker = if is_omittable { "?" } else { "" };
801                let variance = if field_attr.readonly { "+" } else { "" };
802
803                formatted_fields.push(quote! {
804                    format!("  {}{}{}: {},", #variance, #name, #opt_marker, #type_str)
805                });
806            }
807
808            // Combine normal fields and flattened fields
809            let inline = match (formatted_fields.len(), flattened_fields.len()) {
810                (0, 0) => quote!("{||}".to_owned()),
811                (_, 0) => quote! {{
812                    let fields = vec![#(#formatted_fields),*];
813                    format!("{{|\n{}\n|}}", fields.join("\n"))
814                }},
815                (0, 1) => {
816                    let flat = &flattened_fields[0];
817                    quote! {{
818                        let f = #flat;
819                        if f.starts_with('(') && f.ends_with(')') {
820                            f[1..f.len() - 1].trim().to_owned()
821                        } else {
822                            f.trim().to_owned()
823                        }
824                    }}
825                }
826                (0, _) => quote! {{
827                    let parts: Vec<String> = vec![#(#flattened_fields),*];
828                    parts.join(" & ")
829                }},
830                (_, _) => quote! {{
831                    let fields = vec![#(#formatted_fields),*];
832                    let base = format!("{{|\n{}\n|}}", fields.join("\n"));
833                    let flattened: Vec<String> = vec![#(#flattened_fields),*];
834                    format!("{} & {}", base, flattened.join(" & "))
835                }},
836            };
837
838            // inline_flattened always wraps in exact object (for use by parent flatten)
839            let inline_flattened = match (formatted_fields.len(), flattened_fields.len()) {
840                (_, 0) => quote! {{
841                    let fields = vec![#(#formatted_fields),*];
842                    format!("{{|\n{}\n|}}", fields.join("\n"))
843                }},
844                (0, _) => quote! {{
845                    let parts: Vec<String> = vec![#(#flattened_fields),*];
846                    parts.join(" & ")
847                }},
848                (_, _) => quote! {{
849                    let fields = vec![#(#formatted_fields),*];
850                    let base = format!("{{|\n{}\n|}}", fields.join("\n"));
851                    let flattened: Vec<String> = vec![#(#flattened_fields),*];
852                    format!("{} & {}", base, flattened.join(" & "))
853                }},
854            };
855
856            Ok((inline, inline_flattened))
857        }
858        Fields::Unnamed(unnamed) => {
859            if unnamed.unnamed.len() == 1 {
860                // Newtype — inline the inner type
861                let ty = &unnamed.unnamed[0].ty;
862                deps.push(ty);
863                let inline = quote!(<#ty as #crate_rename::Flow>::inline(cfg));
864                let flattened = quote! {
865                    format!("({})", <#ty as #crate_rename::Flow>::inline(cfg))
866                };
867                Ok((inline, flattened))
868            } else {
869                // Tuple struct → Flow tuple
870                let elems: Vec<TokenStream> = unnamed
871                    .unnamed
872                    .iter()
873                    .map(|f| {
874                        let ty = &f.ty;
875                        deps.push(ty);
876                        quote!(<#ty as #crate_rename::Flow>::inline(cfg))
877                    })
878                    .collect();
879                let inline = quote! {{
880                    let elems: Vec<String> = vec![#(#elems),*];
881                    format!("[{}]", elems.join(", "))
882                }};
883                let flattened = quote! {
884                    format!("({})", Self::inline(cfg))
885                };
886                Ok((inline, flattened))
887            }
888        }
889        Fields::Unit => {
890            let inline = quote!(#crate_rename::flow_type::NULL.to_owned());
891            let flattened = quote!(#crate_rename::flow_type::NULL.to_owned());
892            Ok((inline, flattened))
893        }
894    }
895}
896
897fn derive_enum(
898    crate_rename: &Path,
899    container: &ContainerAttr,
900    data: &syn::DataEnum,
901    deps: &mut Dependencies,
902) -> Result<TokenStream> {
903    if data.variants.is_empty() {
904        return Ok(quote!(#crate_rename::flow_type::EMPTY.to_owned()));
905    }
906
907    let is_untagged = container.untagged;
908    let tag = &container.tag.as_deref().map(utils::quote_property_name);
909    let content = &container.content.as_deref().map(utils::quote_property_name);
910
911    let mut variant_defs: Vec<TokenStream> = Vec::new();
912    for v in &data.variants {
913        let variant_attr = VariantAttr::from_attrs(&v.attrs)?;
914        if variant_attr.skip {
915            continue;
916        }
917
918        let variant_name_raw = variant_attr.rename.clone().unwrap_or_else(|| {
919            let raw = v.ident.to_string();
920            container.rename_variant(&raw)
921        });
922        let variant_name = utils::escape_string_literal(&variant_name_raw);
923        let variant_key = utils::quote_property_name(&variant_name_raw);
924
925        // Per-variant untagged: bypass tagging for this specific variant
926        let is_variant_untagged = is_untagged || variant_attr.untagged;
927
928        // Per-variant type/as override: short-circuit all field processing
929        if let Some(ref type_override) = variant_attr.type_override {
930            let type_str = quote!(#type_override.to_owned());
931            let def = if is_variant_untagged {
932                type_str
933            } else if let Some(tag_field) = tag {
934                quote!(format!(
935                    "{{| {}: '{}' |}} & {}",
936                    #tag_field, #variant_name, #type_str
937                ))
938            } else {
939                quote!(format!("{{| {}: {} |}}", #variant_key, #type_str))
940            };
941            variant_defs.push(def);
942            continue;
943        }
944        if let Some(ref type_as) = variant_attr.type_as {
945            deps.push(&syn::parse_quote!(#type_as));
946            let type_str = quote!(<#type_as as #crate_rename::Flow>::inline(cfg));
947            let def = if is_variant_untagged {
948                type_str
949            } else if let Some(tag_field) = tag {
950                quote!(format!(
951                    "{{| {}: '{}' |}} & {}",
952                    #tag_field, #variant_name, #type_str
953                ))
954            } else {
955                quote!(format!("{{| {}: {} |}}", #variant_key, #type_str))
956            };
957            variant_defs.push(def);
958            continue;
959        }
960
961        let def = match &v.fields {
962            Fields::Unit => {
963                if is_variant_untagged {
964                    quote!(#crate_rename::flow_type::NULL.to_owned())
965                } else if let Some(tag_field) = tag {
966                    quote!(format!("{{| {}: '{}' |}}", #tag_field, #variant_name))
967                } else {
968                    quote!(format!("'{}'", #variant_name))
969                }
970            }
971            Fields::Unnamed(unnamed) => {
972                if unnamed.unnamed.len() == 1 {
973                    let ty = &unnamed.unnamed[0].ty;
974                    deps.push(ty);
975                    let inner = quote!(<#ty as #crate_rename::Flow>::inline(cfg));
976                    if is_variant_untagged {
977                        inner
978                    } else if let (Some(tag_field), Some(content_field)) = (tag, content) {
979                        quote!(format!(
980                            "{{| {}: '{}', {}: {} |}}",
981                            #tag_field, #variant_name, #content_field, #inner
982                        ))
983                    } else if let Some(tag_field) = tag {
984                        quote!(format!(
985                            "{{| {}: '{}' |}} & {}",
986                            #tag_field, #variant_name, #inner
987                        ))
988                    } else {
989                        quote!(format!(
990                            "{{| {}: {} |}}",
991                            #variant_key, #inner
992                        ))
993                    }
994                } else {
995                    // Multi-field tuple variant
996                    let elems: Vec<TokenStream> = unnamed
997                        .unnamed
998                        .iter()
999                        .map(|f| {
1000                            let ty = &f.ty;
1001                            deps.push(ty);
1002                            quote!(<#ty as #crate_rename::Flow>::inline(cfg))
1003                        })
1004                        .collect();
1005                    let tuple = quote! {{
1006                        let elems: Vec<String> = vec![#(#elems),*];
1007                        format!("[{}]", elems.join(", "))
1008                    }};
1009                    if is_variant_untagged {
1010                        tuple
1011                    } else if let (Some(tag_field), Some(content_field)) = (tag, content) {
1012                        quote!(format!(
1013                            "{{| {}: '{}', {}: {} |}}",
1014                            #tag_field, #variant_name, #content_field, #tuple
1015                        ))
1016                    } else {
1017                        quote!(format!(
1018                            "{{| {}: {} |}}",
1019                            #variant_key, #tuple
1020                        ))
1021                    }
1022                }
1023            }
1024            Fields::Named(named) => {
1025                let mut field_defs: Vec<TokenStream> = Vec::new();
1026                let mut flattened_defs: Vec<TokenStream> = Vec::new();
1027                for f in &named.named {
1028                    let field_attr = FieldAttr::from_attrs(&f.attrs)?;
1029                    if field_attr.skip {
1030                        continue;
1031                    }
1032                    let ty = &f.ty;
1033
1034                    if field_attr.flatten {
1035                        if field_attr.type_override.is_none() {
1036                            deps.append_from(ty);
1037                        }
1038                        flattened_defs
1039                            .push(quote!(<#ty as #crate_rename::Flow>::inline_flattened(cfg)));
1040                        continue;
1041                    }
1042
1043                    let field_name = f.ident.as_ref().unwrap();
1044                    // Per-variant rename_all takes priority over container
1045                    let name =
1046                        utils::quote_property_name(&field_attr.rename.clone().unwrap_or_else(
1047                            || {
1048                                let raw = field_name.to_string();
1049                                if let Some(ref inflection) = variant_attr.rename_all {
1050                                    inflection.apply(&raw)
1051                                } else {
1052                                    container.rename_variant_field(&raw)
1053                                }
1054                            },
1055                        ));
1056                    // Per-variant inline propagates to field dep tracking
1057                    let effective_inline = field_attr.inline || variant_attr.inline;
1058                    if field_attr.type_override.is_none() {
1059                        if effective_inline {
1060                            deps.append_from(ty);
1061                        } else {
1062                            deps.push(ty);
1063                        }
1064                    }
1065                    let effective_ty = field_attr.type_as.as_ref().unwrap_or(ty);
1066                    let type_str = if let Some(override_ty) = &field_attr.type_override {
1067                        quote!(#override_ty.to_owned())
1068                    } else if effective_inline {
1069                        quote!(<#effective_ty as #crate_rename::Flow>::inline(cfg))
1070                    } else {
1071                        quote!(<#effective_ty as #crate_rename::Flow>::name(cfg))
1072                    };
1073                    // Per-variant optional_fields overrides container
1074                    let variant_optional = match &variant_attr.optional_fields {
1075                        attr::Optional::Inherit => &container.optional_fields,
1076                        explicit => explicit,
1077                    };
1078                    let is_omittable = field_attr.is_optional(variant_optional);
1079                    let opt_marker = if is_omittable { "?" } else { "" };
1080                    let variance = if field_attr.readonly { "+" } else { "" };
1081                    field_defs.push(quote!(format!("{}{}{}: {}", #variance, #name, #opt_marker, #type_str)));
1082                }
1083
1084                let obj = if flattened_defs.is_empty() {
1085                    quote! {{
1086                        let fields: Vec<String> = vec![#(#field_defs),*];
1087                        format!("{{| {} |}}", fields.join(", "))
1088                    }}
1089                } else if field_defs.is_empty() {
1090                    quote! {{
1091                        let parts: Vec<String> = vec![#(#flattened_defs),*];
1092                        parts.join(" & ")
1093                    }}
1094                } else {
1095                    quote! {{
1096                        let fields: Vec<String> = vec![#(#field_defs),*];
1097                        let base = format!("{{| {} |}}", fields.join(", "));
1098                        let flattened: Vec<String> = vec![#(#flattened_defs),*];
1099                        format!("{} & {}", base, flattened.join(" & "))
1100                    }}
1101                };
1102
1103                if is_variant_untagged {
1104                    obj
1105                } else if let (Some(tag_field), Some(content_field)) = (tag, content) {
1106                    quote!(format!(
1107                        "{{| {}: '{}', {}: {} |}}",
1108                        #tag_field, #variant_name, #content_field, #obj
1109                    ))
1110                } else if let Some(tag_field) = tag {
1111                    // Internally tagged: inject tag field into the object
1112                    // Build the tagged object by prepending the tag to the field list
1113                    let tag_field_def = quote!(format!("{}: '{}'", #tag_field, #variant_name));
1114
1115                    if flattened_defs.is_empty() {
1116                        // Simple case: all fields are regular, build a single exact object
1117                        let all_fields: Vec<_> = std::iter::once(tag_field_def.clone())
1118                            .chain(field_defs.iter().cloned())
1119                            .collect();
1120                        quote! {{
1121                            let fields: Vec<String> = vec![#(#all_fields),*];
1122                            format!("{{| {} |}}", fields.join(", "))
1123                        }}
1124                    } else {
1125                        // Has flattened fields: tag goes in base object, then intersect
1126                        let base_fields: Vec<_> = std::iter::once(tag_field_def.clone())
1127                            .chain(field_defs.iter().cloned())
1128                            .collect();
1129                        quote! {{
1130                            let fields: Vec<String> = vec![#(#base_fields),*];
1131                            let base = format!("{{| {} |}}", fields.join(", "));
1132                            let flattened: Vec<String> = vec![#(#flattened_defs),*];
1133                            format!("{} & {}", base, flattened.join(" & "))
1134                        }}
1135                    }
1136                } else {
1137                    quote!(format!(
1138                        "{{| {}: {} |}}",
1139                        #variant_key, #obj
1140                    ))
1141                }
1142            }
1143        };
1144        variant_defs.push(def);
1145    }
1146
1147    if variant_defs.is_empty() {
1148        return Ok(quote!(#crate_rename::flow_type::EMPTY.to_owned()));
1149    }
1150
1151    Ok(quote! {{
1152        let variants: Vec<String> = vec![#(#variant_defs),*];
1153        variants.join(" | ")
1154    }})
1155}
1156
1157/// Derive a Flow `enum` declaration for Rust enums with only unit variants.
1158///
1159/// Produces Flow enum syntax instead of union type aliases:
1160/// - `#[flow(flow_enum)]` → `enum Name { A, B, }` (symbol enum)
1161/// - `#[flow(flow_enum = "string")]` → `enum Name of string { A = 'a', B = 'b', }` (string enum)
1162/// - `#[flow(flow_enum = "number")]` → `enum Name of number { A = 0, B = 1, }` (number enum)
1163/// - `#[flow(flow_enum = "boolean")]` → `enum Name of boolean { A = true, B = false, }` (boolean enum)
1164fn derive_flow_enum(
1165    container: &ContainerAttr,
1166    data: &syn::DataEnum,
1167    repr: &FlowEnumRepr,
1168) -> Result<(TokenStream, TokenStream, TokenStream)> {
1169    // Validate: all variants must be unit
1170    for v in &data.variants {
1171        if !matches!(v.fields, Fields::Unit) {
1172            return Err(syn::Error::new(
1173                v.ident.span(),
1174                "#[flow(flow_enum)] requires all variants to be unit (no fields)",
1175            ));
1176        }
1177    }
1178
1179    // For boolean enums, validate exactly 2 non-skipped variants
1180    if matches!(repr, FlowEnumRepr::Boolean) {
1181        let active_count = data
1182            .variants
1183            .iter()
1184            .filter(|v| !VariantAttr::from_attrs(&v.attrs).map(|a| a.skip).unwrap_or(false))
1185            .count();
1186        if active_count != 2 {
1187            return Err(syn::Error::new(
1188                data.variants.first().map(|v| v.ident.span()).unwrap_or_else(proc_macro2::Span::call_site),
1189                "#[flow(flow_enum = \"boolean\")] requires exactly 2 non-skipped variants",
1190            ));
1191        }
1192    }
1193
1194    let mut member_defs: Vec<TokenStream> = Vec::new();
1195    let mut next_discriminant: i64 = 0;
1196    let mut bool_index: usize = 0;
1197
1198    for v in &data.variants {
1199        let variant_attr = VariantAttr::from_attrs(&v.attrs)?;
1200        if variant_attr.skip {
1201            continue;
1202        }
1203
1204        let member_name = v.ident.to_string();
1205
1206        match repr {
1207            FlowEnumRepr::Symbol => {
1208                member_defs.push(quote!(format!("  {},", #member_name)));
1209            }
1210            FlowEnumRepr::String => {
1211                let value = variant_attr.rename.clone().unwrap_or_else(|| {
1212                    container.rename_variant(&member_name)
1213                });
1214                let escaped = utils::escape_string_literal(&value);
1215                member_defs.push(quote!(format!("  {} = '{}',", #member_name, #escaped)));
1216            }
1217            FlowEnumRepr::Number => {
1218                // Extract discriminant value from `Variant = expr`, or auto-increment
1219                let disc_value = if let Some((_, expr)) = &v.discriminant {
1220                    match expr {
1221                        Expr::Lit(lit) => match &lit.lit {
1222                            Lit::Int(i) => {
1223                                let val = i.base10_parse::<i64>().map_err(|e| {
1224                                    syn::Error::new(i.span(), format!("invalid discriminant: {e}"))
1225                                })?;
1226                                next_discriminant = val + 1;
1227                                val
1228                            }
1229                            _ => {
1230                                return Err(syn::Error::new(
1231                                    expr.span(),
1232                                    "flow_enum = \"number\" requires integer literal discriminants",
1233                                ));
1234                            }
1235                        },
1236                        Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
1237                            if let Expr::Lit(lit) = &*unary.expr {
1238                                if let Lit::Int(i) = &lit.lit {
1239                                    let val = -(i.base10_parse::<i64>().map_err(|e| {
1240                                        syn::Error::new(i.span(), format!("invalid discriminant: {e}"))
1241                                    })?);
1242                                    next_discriminant = val + 1;
1243                                    val
1244                                } else {
1245                                    return Err(syn::Error::new(
1246                                        expr.span(),
1247                                        "flow_enum = \"number\" requires integer literal discriminants",
1248                                    ));
1249                                }
1250                            } else {
1251                                return Err(syn::Error::new(
1252                                    expr.span(),
1253                                    "flow_enum = \"number\" requires integer literal discriminants",
1254                                ));
1255                            }
1256                        }
1257                        _ => {
1258                            return Err(syn::Error::new(
1259                                expr.span(),
1260                                "flow_enum = \"number\" requires integer literal discriminants",
1261                            ));
1262                        }
1263                    }
1264                } else {
1265                    let val = next_discriminant;
1266                    next_discriminant = val + 1;
1267                    val
1268                };
1269                member_defs.push(quote!(format!("  {} = {},", #member_name, #disc_value)));
1270            }
1271            FlowEnumRepr::Boolean => {
1272                let bool_val = if bool_index == 0 { "true" } else { "false" };
1273                bool_index += 1;
1274                member_defs.push(quote!(format!("  {} = {},", #member_name, #bool_val)));
1275            }
1276        }
1277    }
1278
1279    // If all variants were skipped (or enum is empty), fall back to empty type
1280    if member_defs.is_empty() {
1281        let inline = quote!("empty".to_owned());
1282        let inline_flattened = quote!("empty".to_owned());
1283        let enum_decl = quote! {
1284            format!("type {} = empty;", Self::ident(cfg))
1285        };
1286        return Ok((inline, inline_flattened, enum_decl));
1287    }
1288
1289    let repr_suffix = match repr {
1290        FlowEnumRepr::Symbol => "",
1291        FlowEnumRepr::String => " of string",
1292        FlowEnumRepr::Number => " of number",
1293        FlowEnumRepr::Boolean => " of boolean",
1294    };
1295
1296    let enum_decl = quote! {{
1297        let members: Vec<String> = vec![#(#member_defs),*];
1298        format!("enum {}{} {{\n{}\n}}", Self::ident(cfg), #repr_suffix, members.join("\n"))
1299    }};
1300
1301    // inline: flow enums are referenced by name, not expanded
1302    let inline = quote!(Self::name(cfg));
1303    let inline_flattened = quote! {
1304        format!("({})", Self::name(cfg))
1305    };
1306
1307    Ok((inline, inline_flattened, enum_decl))
1308}