Skip to main content

rain_engine_macros/
lib.rs

1//! Procedural macros for RainEngine skill manifests.
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6    DeriveInput, LitInt, LitStr, Meta, Token, parse::Parser, parse_macro_input,
7    punctuated::Punctuated,
8};
9
10#[proc_macro_derive(SkillManifest, attributes(skill))]
11pub fn derive_skill_manifest(input: TokenStream) -> TokenStream {
12    let input = parse_macro_input!(input as DeriveInput);
13    let ident = input.ident;
14
15    let mut name = None::<LitStr>;
16    let mut description = None::<LitStr>;
17    let mut timeout_ms = None::<LitInt>;
18    let mut max_memory_bytes = None::<LitInt>;
19    let mut max_fuel = None::<LitInt>;
20    let mut approval_required = false;
21    let mut scopes = Vec::<LitStr>::new();
22    let mut capabilities = Vec::<LitStr>::new();
23
24    for attr in &input.attrs {
25        if !attr.path().is_ident("skill") {
26            continue;
27        }
28        let metas = attr
29            .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
30            .expect("invalid #[skill(...)] attribute");
31        for meta in metas {
32            match meta {
33                Meta::NameValue(value) if value.path.is_ident("name") => {
34                    if let syn::Expr::Lit(expr) = value.value
35                        && let syn::Lit::Str(lit) = expr.lit
36                    {
37                        name = Some(lit);
38                    }
39                }
40                Meta::NameValue(value) if value.path.is_ident("description") => {
41                    if let syn::Expr::Lit(expr) = value.value
42                        && let syn::Lit::Str(lit) = expr.lit
43                    {
44                        description = Some(lit);
45                    }
46                }
47                Meta::NameValue(value) if value.path.is_ident("timeout_ms") => {
48                    if let syn::Expr::Lit(expr) = value.value
49                        && let syn::Lit::Int(lit) = expr.lit
50                    {
51                        timeout_ms = Some(lit);
52                    }
53                }
54                Meta::NameValue(value) if value.path.is_ident("max_memory_bytes") => {
55                    if let syn::Expr::Lit(expr) = value.value
56                        && let syn::Lit::Int(lit) = expr.lit
57                    {
58                        max_memory_bytes = Some(lit);
59                    }
60                }
61                Meta::NameValue(value) if value.path.is_ident("max_fuel") => {
62                    if let syn::Expr::Lit(expr) = value.value
63                        && let syn::Lit::Int(lit) = expr.lit
64                    {
65                        max_fuel = Some(lit);
66                    }
67                }
68                Meta::NameValue(value) if value.path.is_ident("approval_required") => {
69                    if let syn::Expr::Lit(expr) = value.value
70                        && let syn::Lit::Bool(lit) = expr.lit
71                    {
72                        approval_required = lit.value;
73                    }
74                }
75                Meta::List(list) if list.path.is_ident("scopes") => {
76                    let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
77                    scopes.extend(parser.parse2(list.tokens).expect("invalid scopes"));
78                }
79                Meta::List(list) if list.path.is_ident("capabilities") => {
80                    let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
81                    capabilities.extend(parser.parse2(list.tokens).expect("invalid capabilities"));
82                }
83                _ => {}
84            }
85        }
86    }
87
88    let name = name.expect("skill name is required");
89    let description = description.expect("skill description is required");
90    let timeout_ms = timeout_ms.unwrap_or_else(|| LitInt::new("5000", name.span()));
91    let max_memory_bytes = max_memory_bytes.unwrap_or_else(|| LitInt::new("8388608", name.span()));
92    let max_fuel_tokens = if let Some(max_fuel) = max_fuel {
93        quote! { Some(#max_fuel) }
94    } else {
95        quote! { None }
96    };
97
98    let scope_tokens = scopes
99        .into_iter()
100        .map(|scope| quote! { #scope.to_string() });
101    let capability_tokens = capabilities.into_iter().map(parse_capability);
102
103    TokenStream::from(quote! {
104        impl rain_engine_core::SkillManifestDescriptor for #ident {
105            fn skill_manifest() -> rain_engine_core::SkillManifest {
106                let schema = schemars::schema_for!(#ident);
107                rain_engine_core::SkillManifest {
108                    name: #name.to_string(),
109                    description: #description.to_string(),
110                    input_schema: serde_json::to_value(schema).expect("schema serializes"),
111                    required_scopes: vec![#(#scope_tokens),*],
112                    capability_grants: vec![#(#capability_tokens),*],
113                        resource_policy: rain_engine_core::ResourcePolicy {
114                            timeout_ms: #timeout_ms,
115                            max_memory_bytes: #max_memory_bytes,
116                            max_fuel: #max_fuel_tokens,
117                            priority_class: 0,
118                            retry_policy: rain_engine_core::RetryPolicy::default(),
119                            dry_run_supported: false,
120                        },
121                        approval_required: #approval_required,
122                        circuit_breaker_threshold: 0.5,
123                    }
124                }
125            }
126    })
127}
128
129fn parse_capability(value: LitStr) -> proc_macro2::TokenStream {
130    let raw = value.value();
131    if raw == "log" {
132        quote! { rain_engine_core::SkillCapability::StructuredLog }
133    } else if let Some(namespace) = raw.strip_prefix("kv:") {
134        quote! {
135            rain_engine_core::SkillCapability::KeyValueRead {
136                namespaces: vec![#namespace.to_string()],
137            }
138        }
139    } else if let Some(host) = raw.strip_prefix("http:") {
140        quote! {
141            rain_engine_core::SkillCapability::HttpOutbound {
142                allow_hosts: vec![#host.to_string()],
143            }
144        }
145    } else {
146        panic!("unsupported capability `{raw}`");
147    }
148}