runtara_agent_macro/
lib.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Procedural macros for agent capability and step metadata generation
4//!
5//! This crate provides macros to declaratively define agent capability and step metadata:
6//! - `#[capability]` - marks a function as an agent capability
7//! - `#[derive(CapabilityInput)]` - generates input metadata from struct fields
8//! - `#[derive(CapabilityOutput)]` - generates output metadata from struct fields
9//! - `#[derive(StepMeta)]` - generates step type metadata for DSL generation
10//!
11//! The macros generate static metadata that can be collected at runtime using
12//! the `inventory` crate for agent and step discovery.
13//!
14//! Note: The metadata types (CapabilityMeta, InputTypeMeta, StepTypeMeta, etc.) are defined
15//! in `runtara-dsl` crate to avoid proc-macro crate limitations.
16
17use darling::{FromDeriveInput, FromField, FromMeta};
18use proc_macro::TokenStream;
19use quote::{format_ident, quote};
20use syn::{DeriveInput, ItemFn, Type, parse_macro_input};
21
22/// Attributes for the `#[capability]` macro
23#[derive(Debug, FromMeta)]
24struct CapabilityArgs {
25    /// The agent module name (e.g., "utils", "transform")
26    #[darling(default)]
27    module: Option<String>,
28    /// Capability ID in kebab-case (e.g., "random-double")
29    #[darling(default)]
30    id: Option<String>,
31    /// Display name for UI
32    #[darling(default)]
33    display_name: Option<String>,
34    /// Description of the capability
35    #[darling(default)]
36    description: Option<String>,
37    /// Whether this capability has side effects
38    #[darling(default)]
39    side_effects: bool,
40    /// Whether this capability is idempotent
41    #[darling(default)]
42    idempotent: Option<bool>,
43    /// Whether this capability requires rate limiting (external API calls)
44    #[darling(default)]
45    rate_limited: bool,
46
47    // === Module registration attributes ===
48    // When module_display_name is provided, automatically registers an AgentModuleConfig
49    /// Display name for auto-registered module (e.g., "SMO Test")
50    #[darling(default)]
51    module_display_name: Option<String>,
52    /// Description for auto-registered module
53    #[darling(default)]
54    module_description: Option<String>,
55    /// Whether the auto-registered module has side effects (default: false)
56    #[darling(default)]
57    module_has_side_effects: Option<bool>,
58    /// Whether the auto-registered module supports connections (default: false)
59    #[darling(default)]
60    module_supports_connections: Option<bool>,
61    /// Integration IDs for the auto-registered module (comma-separated)
62    #[darling(default)]
63    module_integration_ids: Option<String>,
64    /// Whether the auto-registered module is secure (default: false)
65    #[darling(default)]
66    module_secure: Option<bool>,
67}
68
69/// Field attributes for CapabilityInput derive
70#[derive(Debug, FromField)]
71#[darling(attributes(field), forward_attrs(serde))]
72struct InputFieldArgs {
73    ident: Option<syn::Ident>,
74    ty: syn::Type,
75    /// Forwarded serde attributes to detect #[serde(default)]
76    attrs: Vec<syn::Attribute>,
77    #[darling(default)]
78    display_name: Option<String>,
79    #[darling(default)]
80    description: Option<String>,
81    #[darling(default)]
82    example: Option<String>,
83    #[darling(default)]
84    default: Option<String>,
85    /// Skip this field in metadata (e.g., connection_id)
86    #[darling(default)]
87    skip: bool,
88    /// Enum type name for fields that are enums (to get variant names)
89    #[darling(default)]
90    enum_type: Option<String>,
91}
92
93/// Container attributes for CapabilityInput derive
94#[derive(Debug, FromDeriveInput)]
95#[darling(attributes(capability_input))]
96struct InputContainerArgs {
97    ident: syn::Ident,
98    data: darling::ast::Data<(), InputFieldArgs>,
99    #[darling(default)]
100    display_name: Option<String>,
101    #[darling(default)]
102    description: Option<String>,
103}
104
105/// Field attributes for CapabilityOutput derive
106#[derive(Debug, FromField)]
107#[darling(attributes(field))]
108struct OutputFieldArgs {
109    ident: Option<syn::Ident>,
110    ty: syn::Type,
111    #[darling(default)]
112    display_name: Option<String>,
113    #[darling(default)]
114    description: Option<String>,
115    #[darling(default)]
116    example: Option<String>,
117}
118
119/// Container attributes for CapabilityOutput derive
120#[derive(Debug, FromDeriveInput)]
121#[darling(attributes(capability_output))]
122struct OutputContainerArgs {
123    ident: syn::Ident,
124    data: darling::ast::Data<(), OutputFieldArgs>,
125    #[darling(default)]
126    display_name: Option<String>,
127    #[darling(default)]
128    description: Option<String>,
129}
130
131/// Attribute macro for marking agent capability functions
132///
133/// # Example
134/// ```ignore
135/// #[capability(
136///     module = "utils",
137///     id = "random-double",
138///     display_name = "Random Double",
139///     description = "Generate a random double between 0 and 1"
140/// )]
141/// pub fn random_double(input: RandomDoubleInput) -> Result<f64, String> {
142///     // ...
143/// }
144/// ```
145#[proc_macro_attribute]
146pub fn capability(attr: TokenStream, item: TokenStream) -> TokenStream {
147    let args = match darling::ast::NestedMeta::parse_meta_list(attr.into()) {
148        Ok(v) => v,
149        Err(e) => return TokenStream::from(e.into_compile_error()),
150    };
151
152    let args = match CapabilityArgs::from_list(&args) {
153        Ok(v) => v,
154        Err(e) => return TokenStream::from(e.write_errors()),
155    };
156
157    let input_fn = parse_macro_input!(item as ItemFn);
158    let fn_name = &input_fn.sig.ident;
159    let fn_name_str = fn_name.to_string();
160
161    // Derive capability_id from function name if not provided (snake_case -> kebab-case)
162    let capability_id = args.id.unwrap_or_else(|| fn_name_str.replace('_', "-"));
163
164    // Extract input type from first parameter
165    let input_type = input_fn
166        .sig
167        .inputs
168        .iter()
169        .find_map(|arg| {
170            if let syn::FnArg::Typed(pat_type) = arg {
171                if let Type::Path(type_path) = &*pat_type.ty {
172                    return type_path.path.segments.last().map(|s| s.ident.to_string());
173                }
174            }
175            None
176        })
177        .unwrap_or_else(|| "Unknown".to_string());
178
179    // Extract output type from Result<T, String>
180    let output_type = extract_result_ok_type(&input_fn.sig.output);
181
182    let display_name = args.display_name;
183    let description = args.description;
184    let side_effects = args.side_effects;
185    let idempotent = args.idempotent.unwrap_or(!side_effects);
186    let rate_limited = args.rate_limited;
187    let module = args.module;
188
189    // Generate metadata registration
190    let meta_ident = format_ident!("__CAPABILITY_META_{}", fn_name.to_string().to_uppercase());
191    let executor_ident = format_ident!(
192        "__CAPABILITY_EXECUTOR_{}",
193        fn_name.to_string().to_uppercase()
194    );
195    let executor_fn_ident = format_ident!("__executor_{}", fn_name);
196
197    let display_name_token = option_to_tokens(&display_name);
198    let description_token = option_to_tokens(&description);
199    let module_token = option_to_tokens(&module);
200
201    // For executor, module must be provided
202    let module_str = module.clone().unwrap_or_else(|| "unknown".to_string());
203
204    // Parse the input type as an identifier for the executor function
205    let input_type_ident = format_ident!("{}", input_type);
206
207    // Generate module registration if module_display_name is provided
208    let module_registration =
209        if let (Some(module_id), Some(mod_display_name)) = (&module, &args.module_display_name) {
210            let module_meta_ident = format_ident!(
211                "__AGENT_MODULE_META_{}_{}",
212                module_id.to_uppercase().replace('-', "_"),
213                fn_name.to_string().to_uppercase()
214            );
215
216            let mod_description = args
217                .module_description
218                .clone()
219                .unwrap_or_else(|| format!("{} agent module", mod_display_name));
220            let mod_has_side_effects = args.module_has_side_effects.unwrap_or(false);
221            let mod_supports_connections = args.module_supports_connections.unwrap_or(false);
222            let mod_secure = args.module_secure.unwrap_or(false);
223
224            // Parse integration_ids from comma-separated string
225            let integration_ids_tokens = if let Some(ref ids_str) = args.module_integration_ids {
226                let ids: Vec<&str> = ids_str
227                    .split(',')
228                    .map(|s| s.trim())
229                    .filter(|s| !s.is_empty())
230                    .collect();
231                quote! { &[#(#ids),*] }
232            } else {
233                quote! { &[] }
234            };
235
236            Some(quote! {
237                #[allow(non_upper_case_globals)]
238                #[doc(hidden)]
239                pub static #module_meta_ident: runtara_dsl::agent_meta::AgentModuleConfig =
240                    runtara_dsl::agent_meta::AgentModuleConfig {
241                        id: #module_id,
242                        name: #mod_display_name,
243                        description: #mod_description,
244                        has_side_effects: #mod_has_side_effects,
245                        supports_connections: #mod_supports_connections,
246                        integration_ids: #integration_ids_tokens,
247                        secure: #mod_secure,
248                    };
249
250                inventory::submit! {
251                    &#module_meta_ident
252                }
253            })
254        } else {
255            None
256        };
257
258    // Detect if the function is async
259    let is_async = input_fn.sig.asyncness.is_some();
260
261    // Generate executor wrapper based on sync/async
262    let executor_wrapper = if is_async {
263        // Async function: directly await the result
264        quote! {
265            #[doc(hidden)]
266            fn #executor_fn_ident(input: serde_json::Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<serde_json::Value, String>> + Send>> {
267                Box::pin(async move {
268                    let typed_input: #input_type_ident = serde_json::from_value(input)
269                        .map_err(|e| format!("Invalid input for {}: {}", #capability_id, e))?;
270                    let result = #fn_name(typed_input).await?;
271                    serde_json::to_value(result)
272                        .map_err(|e| format!("Failed to serialize result: {}", e))
273                })
274            }
275        }
276    } else {
277        // Sync function: wrap with spawn_blocking
278        quote! {
279            #[doc(hidden)]
280            fn #executor_fn_ident(input: serde_json::Value) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<serde_json::Value, String>> + Send>> {
281                Box::pin(async move {
282                    tokio::task::spawn_blocking(move || {
283                        let typed_input: #input_type_ident = serde_json::from_value(input)
284                            .map_err(|e| format!("Invalid input for {}: {}", #capability_id, e))?;
285                        let result = #fn_name(typed_input)?;
286                        serde_json::to_value(result)
287                            .map_err(|e| format!("Failed to serialize result: {}", e))
288                    }).await.map_err(|e| format!("Task panicked: {}", e))?
289                })
290            }
291        }
292    };
293
294    let expanded = quote! {
295        #input_fn
296
297        #[allow(non_upper_case_globals)]
298        #[doc(hidden)]
299        pub static #meta_ident: runtara_dsl::agent_meta::CapabilityMeta = runtara_dsl::agent_meta::CapabilityMeta {
300            module: #module_token,
301            capability_id: #capability_id,
302            function_name: #fn_name_str,
303            input_type: #input_type,
304            output_type: #output_type,
305            display_name: #display_name_token,
306            description: #description_token,
307            has_side_effects: #side_effects,
308            is_idempotent: #idempotent,
309            rate_limited: #rate_limited,
310        };
311
312        inventory::submit! {
313            &#meta_ident
314        }
315
316        #executor_wrapper
317
318        #[allow(non_upper_case_globals)]
319        #[doc(hidden)]
320        pub static #executor_ident: runtara_dsl::agent_meta::CapabilityExecutor = runtara_dsl::agent_meta::CapabilityExecutor {
321            module: #module_str,
322            capability_id: #capability_id,
323            execute: #executor_fn_ident,
324        };
325
326        inventory::submit! {
327            &#executor_ident
328        }
329
330        #module_registration
331    };
332
333    TokenStream::from(expanded)
334}
335
336/// Convert Option<String> to tokens
337fn option_to_tokens(opt: &Option<String>) -> proc_macro2::TokenStream {
338    match opt {
339        Some(s) => quote! { Some(#s) },
340        None => quote! { None },
341    }
342}
343
344/// Extract the Ok type from Result<T, E>
345fn extract_result_ok_type(output: &syn::ReturnType) -> String {
346    if let syn::ReturnType::Type(_, ty) = output {
347        if let Type::Path(type_path) = &**ty {
348            if let Some(segment) = type_path.path.segments.first() {
349                if segment.ident == "Result" {
350                    if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
351                        if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
352                            return type_to_string(inner_ty);
353                        }
354                    }
355                }
356            }
357        }
358    }
359    "Unknown".to_string()
360}
361
362/// Convert a Type to a string representation
363fn type_to_string(ty: &Type) -> String {
364    match ty {
365        Type::Path(type_path) => {
366            let segments: Vec<String> = type_path
367                .path
368                .segments
369                .iter()
370                .map(|s| {
371                    let ident = s.ident.to_string();
372                    if let syn::PathArguments::AngleBracketed(args) = &s.arguments {
373                        let inner: Vec<String> = args
374                            .args
375                            .iter()
376                            .filter_map(|arg| {
377                                if let syn::GenericArgument::Type(inner_ty) = arg {
378                                    Some(type_to_string(inner_ty))
379                                } else {
380                                    None
381                                }
382                            })
383                            .collect();
384                        if !inner.is_empty() {
385                            format!("{}<{}>", ident, inner.join(", "))
386                        } else {
387                            ident
388                        }
389                    } else {
390                        ident
391                    }
392                })
393                .collect();
394            segments.join("::")
395        }
396        Type::Tuple(tuple) if tuple.elems.is_empty() => "()".to_string(),
397        _ => "Unknown".to_string(),
398    }
399}
400
401/// Derive macro for capability input structs
402///
403/// Generates metadata about input fields that can be collected at runtime.
404///
405/// # Example
406/// ```ignore
407/// #[derive(CapabilityInput)]
408/// #[capability_input(display_name = "Random Double Input")]
409/// pub struct RandomDoubleInput {
410///     #[field(display_name = "Minimum", description = "Minimum value")]
411///     pub min: Option<f64>,
412/// }
413/// ```
414#[proc_macro_derive(CapabilityInput, attributes(capability_input, field))]
415pub fn derive_capability_input(input: TokenStream) -> TokenStream {
416    let input = parse_macro_input!(input as DeriveInput);
417
418    let args = match InputContainerArgs::from_derive_input(&input) {
419        Ok(v) => v,
420        Err(e) => return TokenStream::from(e.write_errors()),
421    };
422
423    let struct_name = &args.ident;
424    let struct_name_str = struct_name.to_string();
425
426    let fields = match args.data {
427        darling::ast::Data::Struct(fields) => fields.fields,
428        _ => {
429            return TokenStream::from(
430                quote! { compile_error!("CapabilityInput can only be derived for structs"); },
431            );
432        }
433    };
434
435    let field_metas: Vec<_> = fields
436        .iter()
437        .filter(|f| !f.skip)
438        .filter(|f| {
439            // Skip connection_id field
440            f.ident.as_ref().map(|i| i.to_string()) != Some("connection_id".to_string())
441        })
442        .map(|f| {
443            let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
444            let type_str = type_to_string(&f.ty);
445            let (inner_type, is_option_type) = unwrap_option_type(&type_str);
446            // Field is optional if it's Option<T>, has #[field(default = "...")], or has #[serde(default)]
447            let is_optional = is_option_type || f.default.is_some() || has_serde_default(&f.attrs);
448
449            let display_name_token = option_to_tokens(&f.display_name);
450            let description_token = option_to_tokens(&f.description);
451            let example_token = option_to_tokens(&f.example);
452            let default_token = option_to_tokens(&f.default);
453
454            let enum_values_fn_token = if let Some(ref enum_type) = f.enum_type {
455                let enum_ident = format_ident!("{}", enum_type);
456                quote! { Some(<#enum_ident as runtara_dsl::agent_meta::EnumVariants>::variant_names) }
457            } else {
458                quote! { None }
459            };
460
461            quote! {
462                runtara_dsl::agent_meta::InputFieldMeta {
463                    name: #name,
464                    type_name: #inner_type,
465                    is_optional: #is_optional,
466                    display_name: #display_name_token,
467                    description: #description_token,
468                    example: #example_token,
469                    default_value: #default_token,
470                    enum_values_fn: #enum_values_fn_token,
471                }
472            }
473        })
474        .collect();
475
476    let container_display_name = option_to_tokens(&args.display_name);
477    let container_description = option_to_tokens(&args.description);
478
479    let meta_ident = format_ident!("__INPUT_META_{}", struct_name);
480
481    let expanded = quote! {
482        #[allow(non_upper_case_globals)]
483        #[doc(hidden)]
484        pub static #meta_ident: runtara_dsl::agent_meta::InputTypeMeta = runtara_dsl::agent_meta::InputTypeMeta {
485            type_name: #struct_name_str,
486            display_name: #container_display_name,
487            description: #container_description,
488            fields: &[#(#field_metas),*],
489        };
490
491        inventory::submit! {
492            &#meta_ident
493        }
494    };
495
496    TokenStream::from(expanded)
497}
498
499// ============================================================================
500// Connection Params Derive Macro
501// ============================================================================
502
503/// Field attributes for ConnectionParams derive
504#[derive(Debug, FromField)]
505#[darling(attributes(field), forward_attrs(serde))]
506struct ConnectionFieldArgs {
507    ident: Option<syn::Ident>,
508    ty: syn::Type,
509    /// Forwarded serde attributes to detect #[serde(default)]
510    attrs: Vec<syn::Attribute>,
511    #[darling(default)]
512    display_name: Option<String>,
513    #[darling(default)]
514    description: Option<String>,
515    #[darling(default)]
516    placeholder: Option<String>,
517    #[darling(default)]
518    default: Option<String>,
519    /// Mark this field as a secret (password, API key, etc.)
520    #[darling(default)]
521    secret: bool,
522}
523
524/// Container attributes for ConnectionParams derive
525#[derive(Debug, FromDeriveInput)]
526#[darling(attributes(connection))]
527struct ConnectionContainerArgs {
528    ident: syn::Ident,
529    data: darling::ast::Data<(), ConnectionFieldArgs>,
530    /// Unique identifier for this connection type
531    integration_id: String,
532    /// Display name for UI
533    #[darling(default)]
534    display_name: Option<String>,
535    /// Description of this connection type
536    #[darling(default)]
537    description: Option<String>,
538    /// Category for grouping (e.g., "ecommerce", "file_storage", "llm")
539    #[darling(default)]
540    category: Option<String>,
541}
542
543/// Derive macro for connection parameter structs
544///
545/// Generates metadata about connection fields that can be collected at runtime
546/// for automatic form generation in the UI.
547///
548/// # Example
549/// ```ignore
550/// #[derive(ConnectionParams)]
551/// #[connection(
552///     integration_id = "bearer",
553///     display_name = "Bearer Token",
554///     description = "Connect using a Bearer token for authentication",
555///     category = "http"
556/// )]
557/// struct BearerParams {
558///     #[field(display_name = "Token", description = "Bearer authentication token", secret)]
559///     token: String,
560///     #[field(display_name = "Base URL", description = "API base URL (e.g., https://api.example.com)", placeholder = "https://api.example.com")]
561///     base_url: String,
562/// }
563/// ```
564#[proc_macro_derive(ConnectionParams, attributes(connection, field))]
565pub fn derive_connection_params(input: TokenStream) -> TokenStream {
566    let input = parse_macro_input!(input as DeriveInput);
567
568    let args = match ConnectionContainerArgs::from_derive_input(&input) {
569        Ok(v) => v,
570        Err(e) => return TokenStream::from(e.write_errors()),
571    };
572
573    let struct_name = &args.ident;
574    let integration_id = &args.integration_id;
575
576    let fields = match args.data {
577        darling::ast::Data::Struct(fields) => fields.fields,
578        _ => {
579            return TokenStream::from(
580                quote! { compile_error!("ConnectionParams can only be derived for structs"); },
581            );
582        }
583    };
584
585    let field_metas: Vec<_> = fields
586        .iter()
587        .map(|f| {
588            let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
589            let type_str = type_to_string(&f.ty);
590            let (inner_type, is_option_type) = unwrap_option_type(&type_str);
591            // Field is optional if it's Option<T>, has #[field(default = "...")], or has #[serde(default)]
592            let is_optional = is_option_type || f.default.is_some() || has_serde_default(&f.attrs);
593
594            let display_name_token = option_to_tokens(&f.display_name);
595            let description_token = option_to_tokens(&f.description);
596            let placeholder_token = option_to_tokens(&f.placeholder);
597            let default_token = option_to_tokens(&f.default);
598            let is_secret = f.secret;
599
600            quote! {
601                runtara_dsl::agent_meta::ConnectionFieldMeta {
602                    name: #name,
603                    type_name: #inner_type,
604                    is_optional: #is_optional,
605                    display_name: #display_name_token,
606                    description: #description_token,
607                    placeholder: #placeholder_token,
608                    default_value: #default_token,
609                    is_secret: #is_secret,
610                }
611            }
612        })
613        .collect();
614
615    // Default display name from integration_id if not provided
616    let display_name = args.display_name.unwrap_or_else(|| {
617        // Convert snake_case to Title Case
618        integration_id
619            .split('_')
620            .map(|s| {
621                let mut c = s.chars();
622                match c.next() {
623                    None => String::new(),
624                    Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
625                }
626            })
627            .collect::<Vec<_>>()
628            .join(" ")
629    });
630
631    let description_token = option_to_tokens(&args.description);
632    let category_token = option_to_tokens(&args.category);
633
634    let meta_ident = format_ident!("__CONNECTION_META_{}", struct_name);
635
636    let expanded = quote! {
637        #[allow(non_upper_case_globals)]
638        #[doc(hidden)]
639        pub static #meta_ident: runtara_dsl::agent_meta::ConnectionTypeMeta = runtara_dsl::agent_meta::ConnectionTypeMeta {
640            integration_id: #integration_id,
641            display_name: #display_name,
642            description: #description_token,
643            category: #category_token,
644            fields: &[#(#field_metas),*],
645        };
646
647        inventory::submit! {
648            &#meta_ident
649        }
650    };
651
652    TokenStream::from(expanded)
653}
654
655/// Unwrap Option<T> to get T and whether it's optional
656fn unwrap_option_type(type_str: &str) -> (String, bool) {
657    if type_str.starts_with("Option<") && type_str.ends_with('>') {
658        let inner = type_str
659            .strip_prefix("Option<")
660            .unwrap()
661            .strip_suffix('>')
662            .unwrap();
663        (inner.to_string(), true)
664    } else {
665        (type_str.to_string(), false)
666    }
667}
668
669/// Check if a field has `#[serde(default)]` or `#[serde(default = "...")]` attribute
670fn has_serde_default(attrs: &[syn::Attribute]) -> bool {
671    for attr in attrs {
672        if attr.path().is_ident("serde") {
673            let mut found_default = false;
674            let _ = attr.parse_nested_meta(|meta| {
675                if meta.path.is_ident("default") {
676                    found_default = true;
677                }
678                Ok(())
679            });
680            if found_default {
681                return true;
682            }
683        }
684    }
685    false
686}
687
688/// Analyze a type string and extract nested type information
689/// Returns (is_nullable, items_type, nested_type)
690fn analyze_type_for_nesting(type_str: &str) -> (bool, Option<String>, Option<String>) {
691    let mut is_nullable = false;
692    let mut working_type = type_str.to_string();
693
694    // Check for Option<T> - unwrap and mark as nullable
695    if let Some(inner) = working_type
696        .strip_prefix("Option<")
697        .and_then(|s| s.strip_suffix('>'))
698    {
699        is_nullable = true;
700        working_type = inner.to_string();
701    }
702
703    // Check for Vec<T> - extract item type
704    if let Some(inner) = working_type
705        .strip_prefix("Vec<")
706        .and_then(|s| s.strip_suffix('>'))
707    {
708        // The inner type is the items type
709        let items_type = inner.to_string();
710        return (is_nullable, Some(items_type), None);
711    }
712
713    // Check for HashMap/BTreeMap - these are objects, no specific nested type
714    // Handle both short form (HashMap<...>) and fully qualified (std::collections::HashMap<...>)
715    if working_type.starts_with("HashMap<")
716        || working_type.starts_with("BTreeMap<")
717        || working_type.contains("::HashMap<")
718        || working_type.contains("::BTreeMap<")
719    {
720        return (is_nullable, None, None);
721    }
722
723    // Check if this is a known primitive type
724    let primitives = [
725        "()", "bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64",
726        "u128", "usize", "f32", "f64", "String", "Value",
727    ];
728
729    if primitives.contains(&working_type.as_str()) {
730        return (is_nullable, None, None);
731    }
732
733    // Otherwise, this might be a nested struct type that can be looked up
734    // Only set nested_type_name if it looks like a custom type (starts with uppercase)
735    // and doesn't contain :: (which would indicate a path like std::something)
736    if !working_type.contains("::")
737        && working_type
738            .chars()
739            .next()
740            .map(|c| c.is_uppercase())
741            .unwrap_or(false)
742    {
743        return (is_nullable, None, Some(working_type));
744    }
745
746    (is_nullable, None, None)
747}
748
749/// Derive macro for capability output structs
750#[proc_macro_derive(CapabilityOutput, attributes(capability_output, field))]
751pub fn derive_capability_output(input: TokenStream) -> TokenStream {
752    let input = parse_macro_input!(input as DeriveInput);
753
754    let args = match OutputContainerArgs::from_derive_input(&input) {
755        Ok(v) => v,
756        Err(e) => return TokenStream::from(e.write_errors()),
757    };
758
759    let struct_name = &args.ident;
760    let struct_name_str = struct_name.to_string();
761
762    let fields = match args.data {
763        darling::ast::Data::Struct(fields) => fields.fields,
764        _ => {
765            return TokenStream::from(
766                quote! { compile_error!("CapabilityOutput can only be derived for structs"); },
767            );
768        }
769    };
770
771    let field_metas: Vec<_> = fields
772        .iter()
773        .map(|f| {
774            let name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
775            let type_str = type_to_string(&f.ty);
776
777            // Analyze the type for nested type information
778            let (is_nullable, items_type, nested_type) = analyze_type_for_nesting(&type_str);
779
780            let display_name_token = option_to_tokens(&f.display_name);
781            let description_token = option_to_tokens(&f.description);
782            let example_token = option_to_tokens(&f.example);
783            let items_type_token = option_to_tokens(&items_type);
784            let nested_type_token = option_to_tokens(&nested_type);
785
786            quote! {
787                runtara_dsl::agent_meta::OutputFieldMeta {
788                    name: #name,
789                    type_name: #type_str,
790                    display_name: #display_name_token,
791                    description: #description_token,
792                    example: #example_token,
793                    nullable: #is_nullable,
794                    items_type_name: #items_type_token,
795                    nested_type_name: #nested_type_token,
796                }
797            }
798        })
799        .collect();
800
801    let container_display_name = option_to_tokens(&args.display_name);
802    let container_description = option_to_tokens(&args.description);
803
804    let meta_ident = format_ident!("__OUTPUT_META_{}", struct_name);
805
806    let expanded = quote! {
807        #[allow(non_upper_case_globals)]
808        #[doc(hidden)]
809        pub static #meta_ident: runtara_dsl::agent_meta::OutputTypeMeta = runtara_dsl::agent_meta::OutputTypeMeta {
810            type_name: #struct_name_str,
811            display_name: #container_display_name,
812            description: #container_description,
813            fields: &[#(#field_metas),*],
814        };
815
816        inventory::submit! {
817            &#meta_ident
818        }
819    };
820
821    TokenStream::from(expanded)
822}
823
824// ============================================================================
825// Step Metadata Derive Macro
826// ============================================================================
827
828/// Container attributes for StepMeta derive
829#[derive(Debug, FromDeriveInput)]
830#[darling(attributes(step))]
831struct StepMetaArgs {
832    ident: syn::Ident,
833    /// Step type ID (e.g., "Conditional", "Agent")
834    #[darling(default)]
835    id: Option<String>,
836    /// Display name for UI
837    #[darling(default)]
838    display_name: Option<String>,
839    /// Description of the step type
840    #[darling(default)]
841    description: Option<String>,
842    /// Category: "control" or "execution"
843    #[darling(default)]
844    category: Option<String>,
845}
846
847/// Derive macro for step type structs
848///
849/// Generates metadata about step types that can be collected at runtime
850/// for automatic DSL schema generation.
851///
852/// # Example
853/// ```ignore
854/// #[derive(StepMeta)]
855/// #[step(
856///     id = "Conditional",
857///     display_name = "Conditional Branch",
858///     description = "Evaluates conditions and branches execution",
859///     category = "control"
860/// )]
861/// pub struct ConditionalStep {
862///     pub id: String,
863///     pub name: Option<String>,
864///     pub input_mapping: Option<InputMapping>,
865/// }
866/// ```
867#[proc_macro_derive(StepMeta, attributes(step))]
868pub fn derive_step_meta(input: TokenStream) -> TokenStream {
869    let input = parse_macro_input!(input as DeriveInput);
870
871    let args = match StepMetaArgs::from_derive_input(&input) {
872        Ok(v) => v,
873        Err(e) => return TokenStream::from(e.write_errors()),
874    };
875
876    let struct_name = &args.ident;
877    let struct_name_str = struct_name.to_string();
878
879    // Derive step ID from struct name if not provided (strip "Step" suffix)
880    let step_id = args.id.unwrap_or_else(|| {
881        struct_name_str
882            .strip_suffix("Step")
883            .unwrap_or(&struct_name_str)
884            .to_string()
885    });
886
887    // Default display name from step ID
888    let display_name = args.display_name.unwrap_or_else(|| step_id.clone());
889
890    // Default description
891    let description = args
892        .description
893        .unwrap_or_else(|| format!("{} step", step_id));
894
895    // Default category based on step type
896    let category = args.category.unwrap_or_else(|| match step_id.as_str() {
897        "Agent" | "StartScenario" => "execution".to_string(),
898        _ => "control".to_string(),
899    });
900
901    let meta_ident = format_ident!("__STEP_META_{}", struct_name);
902    let schema_fn_ident = format_ident!("__step_schema_{}", struct_name.to_string().to_lowercase());
903
904    let expanded = quote! {
905        #[doc(hidden)]
906        fn #schema_fn_ident() -> schemars::schema::RootSchema {
907            schemars::schema_for!(#struct_name)
908        }
909
910        #[allow(non_upper_case_globals)]
911        #[doc(hidden)]
912        pub static #meta_ident: runtara_dsl::agent_meta::StepTypeMeta = runtara_dsl::agent_meta::StepTypeMeta {
913            id: #step_id,
914            display_name: #display_name,
915            description: #description,
916            category: #category,
917            schema_fn: #schema_fn_ident,
918        };
919
920        inventory::submit! {
921            &#meta_ident
922        }
923    };
924
925    TokenStream::from(expanded)
926}
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931    use syn::parse_quote;
932
933    // ========================================================================
934    // Tests for unwrap_option_type
935    // ========================================================================
936
937    #[test]
938    fn test_unwrap_option_type_non_optional() {
939        let (inner, is_optional) = unwrap_option_type("String");
940        assert_eq!(inner, "String");
941        assert!(!is_optional);
942    }
943
944    #[test]
945    fn test_unwrap_option_type_simple_option() {
946        let (inner, is_optional) = unwrap_option_type("Option<String>");
947        assert_eq!(inner, "String");
948        assert!(is_optional);
949    }
950
951    #[test]
952    fn test_unwrap_option_type_primitive() {
953        let (inner, is_optional) = unwrap_option_type("Option<i32>");
954        assert_eq!(inner, "i32");
955        assert!(is_optional);
956    }
957
958    #[test]
959    fn test_unwrap_option_type_complex_inner() {
960        let (inner, is_optional) = unwrap_option_type("Option<Vec<String>>");
961        assert_eq!(inner, "Vec<String>");
962        assert!(is_optional);
963    }
964
965    #[test]
966    fn test_unwrap_option_type_non_option_generic() {
967        let (inner, is_optional) = unwrap_option_type("Vec<String>");
968        assert_eq!(inner, "Vec<String>");
969        assert!(!is_optional);
970    }
971
972    #[test]
973    fn test_unwrap_option_type_empty_string() {
974        let (inner, is_optional) = unwrap_option_type("");
975        assert_eq!(inner, "");
976        assert!(!is_optional);
977    }
978
979    // ========================================================================
980    // Tests for type_to_string
981    // ========================================================================
982
983    #[test]
984    fn test_type_to_string_simple_type() {
985        let ty: Type = parse_quote!(String);
986        assert_eq!(type_to_string(&ty), "String");
987    }
988
989    #[test]
990    fn test_type_to_string_primitive() {
991        let ty: Type = parse_quote!(i32);
992        assert_eq!(type_to_string(&ty), "i32");
993    }
994
995    #[test]
996    fn test_type_to_string_generic_single() {
997        let ty: Type = parse_quote!(Option<String>);
998        assert_eq!(type_to_string(&ty), "Option<String>");
999    }
1000
1001    #[test]
1002    fn test_type_to_string_generic_multiple() {
1003        let ty: Type = parse_quote!(HashMap<String, i32>);
1004        assert_eq!(type_to_string(&ty), "HashMap<String, i32>");
1005    }
1006
1007    #[test]
1008    fn test_type_to_string_vec() {
1009        let ty: Type = parse_quote!(Vec<u8>);
1010        assert_eq!(type_to_string(&ty), "Vec<u8>");
1011    }
1012
1013    #[test]
1014    fn test_type_to_string_nested_generics() {
1015        let ty: Type = parse_quote!(Option<Vec<String>>);
1016        assert_eq!(type_to_string(&ty), "Option<Vec<String>>");
1017    }
1018
1019    #[test]
1020    fn test_type_to_string_unit_type() {
1021        let ty: Type = parse_quote!(());
1022        assert_eq!(type_to_string(&ty), "()");
1023    }
1024
1025    #[test]
1026    fn test_type_to_string_path_type() {
1027        let ty: Type = parse_quote!(std::collections::HashMap<String, i32>);
1028        assert_eq!(
1029            type_to_string(&ty),
1030            "std::collections::HashMap<String, i32>"
1031        );
1032    }
1033
1034    #[test]
1035    fn test_type_to_string_result_type() {
1036        let ty: Type = parse_quote!(Result<String, Error>);
1037        assert_eq!(type_to_string(&ty), "Result<String, Error>");
1038    }
1039
1040    #[test]
1041    fn test_type_to_string_custom_type() {
1042        let ty: Type = parse_quote!(MyCustomInput);
1043        assert_eq!(type_to_string(&ty), "MyCustomInput");
1044    }
1045
1046    // ========================================================================
1047    // Tests for analyze_type_for_nesting
1048    // ========================================================================
1049
1050    #[test]
1051    fn test_analyze_type_for_nesting_primitive() {
1052        let (nullable, items, nested) = analyze_type_for_nesting("String");
1053        assert!(!nullable);
1054        assert!(items.is_none());
1055        assert!(nested.is_none());
1056    }
1057
1058    #[test]
1059    fn test_analyze_type_for_nesting_i32() {
1060        let (nullable, items, nested) = analyze_type_for_nesting("i32");
1061        assert!(!nullable);
1062        assert!(items.is_none());
1063        assert!(nested.is_none());
1064    }
1065
1066    #[test]
1067    fn test_analyze_type_for_nesting_option_primitive() {
1068        let (nullable, items, nested) = analyze_type_for_nesting("Option<String>");
1069        assert!(nullable);
1070        assert!(items.is_none());
1071        assert!(nested.is_none());
1072    }
1073
1074    #[test]
1075    fn test_analyze_type_for_nesting_vec() {
1076        let (nullable, items, nested) = analyze_type_for_nesting("Vec<String>");
1077        assert!(!nullable);
1078        assert_eq!(items, Some("String".to_string()));
1079        assert!(nested.is_none());
1080    }
1081
1082    #[test]
1083    fn test_analyze_type_for_nesting_option_vec() {
1084        let (nullable, items, nested) = analyze_type_for_nesting("Option<Vec<i32>>");
1085        assert!(nullable);
1086        assert_eq!(items, Some("i32".to_string()));
1087        assert!(nested.is_none());
1088    }
1089
1090    #[test]
1091    fn test_analyze_type_for_nesting_hashmap() {
1092        let (nullable, items, nested) = analyze_type_for_nesting("HashMap<String, i32>");
1093        assert!(!nullable);
1094        assert!(items.is_none());
1095        assert!(nested.is_none());
1096    }
1097
1098    #[test]
1099    fn test_analyze_type_for_nesting_btreemap() {
1100        let (nullable, items, nested) = analyze_type_for_nesting("BTreeMap<String, Value>");
1101        assert!(!nullable);
1102        assert!(items.is_none());
1103        assert!(nested.is_none());
1104    }
1105
1106    #[test]
1107    fn test_analyze_type_for_nesting_std_hashmap() {
1108        let (nullable, items, nested) =
1109            analyze_type_for_nesting("std::collections::HashMap<String, i32>");
1110        assert!(!nullable);
1111        assert!(items.is_none());
1112        assert!(nested.is_none());
1113    }
1114
1115    #[test]
1116    fn test_analyze_type_for_nesting_custom_type() {
1117        let (nullable, items, nested) = analyze_type_for_nesting("MyCustomStruct");
1118        assert!(!nullable);
1119        assert!(items.is_none());
1120        assert_eq!(nested, Some("MyCustomStruct".to_string()));
1121    }
1122
1123    #[test]
1124    fn test_analyze_type_for_nesting_option_custom() {
1125        let (nullable, items, nested) = analyze_type_for_nesting("Option<AddressInfo>");
1126        assert!(nullable);
1127        assert!(items.is_none());
1128        assert_eq!(nested, Some("AddressInfo".to_string()));
1129    }
1130
1131    #[test]
1132    fn test_analyze_type_for_nesting_unit_type() {
1133        let (nullable, items, nested) = analyze_type_for_nesting("()");
1134        assert!(!nullable);
1135        assert!(items.is_none());
1136        assert!(nested.is_none());
1137    }
1138
1139    #[test]
1140    fn test_analyze_type_for_nesting_value() {
1141        let (nullable, items, nested) = analyze_type_for_nesting("Value");
1142        assert!(!nullable);
1143        assert!(items.is_none());
1144        assert!(nested.is_none());
1145    }
1146
1147    #[test]
1148    fn test_analyze_type_for_nesting_lowercase_custom() {
1149        // Lowercase types shouldn't be treated as custom nested types
1150        let (nullable, items, nested) = analyze_type_for_nesting("lowercase_type");
1151        assert!(!nullable);
1152        assert!(items.is_none());
1153        assert!(nested.is_none());
1154    }
1155
1156    // ========================================================================
1157    // Tests for option_to_tokens
1158    // ========================================================================
1159
1160    #[test]
1161    fn test_option_to_tokens_some() {
1162        let opt = Some("test value".to_string());
1163        let tokens = option_to_tokens(&opt);
1164        let code = tokens.to_string();
1165        assert!(code.contains("Some"));
1166        assert!(code.contains("test value"));
1167    }
1168
1169    #[test]
1170    fn test_option_to_tokens_none() {
1171        let opt: Option<String> = None;
1172        let tokens = option_to_tokens(&opt);
1173        let code = tokens.to_string();
1174        assert_eq!(code, "None");
1175    }
1176
1177    #[test]
1178    fn test_option_to_tokens_empty_string() {
1179        let opt = Some("".to_string());
1180        let tokens = option_to_tokens(&opt);
1181        let code = tokens.to_string();
1182        assert!(code.contains("Some"));
1183    }
1184
1185    #[test]
1186    fn test_option_to_tokens_special_chars() {
1187        let opt = Some("value with \"quotes\" and 'apostrophes'".to_string());
1188        let tokens = option_to_tokens(&opt);
1189        let code = tokens.to_string();
1190        assert!(code.contains("Some"));
1191    }
1192
1193    // ========================================================================
1194    // Tests for extract_result_ok_type
1195    // ========================================================================
1196
1197    #[test]
1198    fn test_extract_result_ok_type_simple() {
1199        let output: syn::ReturnType = parse_quote!(-> Result<String, Error>);
1200        assert_eq!(extract_result_ok_type(&output), "String");
1201    }
1202
1203    #[test]
1204    fn test_extract_result_ok_type_custom() {
1205        let output: syn::ReturnType = parse_quote!(-> Result<MyOutput, String>);
1206        assert_eq!(extract_result_ok_type(&output), "MyOutput");
1207    }
1208
1209    #[test]
1210    fn test_extract_result_ok_type_unit() {
1211        let output: syn::ReturnType = parse_quote!(-> Result<(), Error>);
1212        assert_eq!(extract_result_ok_type(&output), "()");
1213    }
1214
1215    #[test]
1216    fn test_extract_result_ok_type_generic() {
1217        let output: syn::ReturnType = parse_quote!(-> Result<Vec<String>, Error>);
1218        assert_eq!(extract_result_ok_type(&output), "Vec<String>");
1219    }
1220
1221    #[test]
1222    fn test_extract_result_ok_type_no_return() {
1223        let output: syn::ReturnType = syn::ReturnType::Default;
1224        assert_eq!(extract_result_ok_type(&output), "Unknown");
1225    }
1226
1227    #[test]
1228    fn test_extract_result_ok_type_non_result() {
1229        let output: syn::ReturnType = parse_quote!(-> String);
1230        assert_eq!(extract_result_ok_type(&output), "Unknown");
1231    }
1232
1233    #[test]
1234    fn test_extract_result_ok_type_option() {
1235        let output: syn::ReturnType = parse_quote!(-> Result<Option<String>, Error>);
1236        assert_eq!(extract_result_ok_type(&output), "Option<String>");
1237    }
1238
1239    // ========================================================================
1240    // Tests for has_serde_default
1241    // ========================================================================
1242
1243    #[test]
1244    fn test_has_serde_default_true() {
1245        let attr: syn::Attribute = parse_quote!(#[serde(default)]);
1246        assert!(has_serde_default(&[attr]));
1247    }
1248
1249    #[test]
1250    fn test_has_serde_default_with_value() {
1251        let attr: syn::Attribute = parse_quote!(#[serde(default = "default_value")]);
1252        assert!(has_serde_default(&[attr]));
1253    }
1254
1255    #[test]
1256    fn test_has_serde_default_other_serde_attr() {
1257        let attr: syn::Attribute = parse_quote!(#[serde(rename = "other_name")]);
1258        assert!(!has_serde_default(&[attr]));
1259    }
1260
1261    #[test]
1262    fn test_has_serde_default_non_serde() {
1263        let attr: syn::Attribute = parse_quote!(#[field(display_name = "Test")]);
1264        assert!(!has_serde_default(&[attr]));
1265    }
1266
1267    #[test]
1268    fn test_has_serde_default_empty() {
1269        assert!(!has_serde_default(&[]));
1270    }
1271
1272    #[test]
1273    fn test_has_serde_default_multiple_attrs() {
1274        let attr1: syn::Attribute = parse_quote!(#[field(display_name = "Test")]);
1275        let attr2: syn::Attribute = parse_quote!(#[serde(default)]);
1276        assert!(has_serde_default(&[attr1, attr2]));
1277    }
1278
1279    #[test]
1280    fn test_has_serde_default_multiple_nested_default_first() {
1281        // When default appears first in the list, it should be detected
1282        let attr: syn::Attribute = parse_quote!(#[serde(default, rename = "foo")]);
1283        assert!(has_serde_default(&[attr]));
1284    }
1285
1286    // Note: The current implementation has a limitation where it may not detect
1287    // `default` when it appears after a key=value pair like `rename = "foo"`.
1288    // This is because parse_nested_meta stops at the first unhandled meta item.
1289    // In practice, this edge case is rare since most code puts `default` first.
1290}