rain_engine_macros/
lib.rs1use 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}