rain-engine-macros 0.1.0

Proc macros for RainEngine manifests
Documentation
//! Procedural macros for RainEngine skill manifests.

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    DeriveInput, LitInt, LitStr, Meta, Token, parse::Parser, parse_macro_input,
    punctuated::Punctuated,
};

#[proc_macro_derive(SkillManifest, attributes(skill))]
pub fn derive_skill_manifest(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let ident = input.ident;

    let mut name = None::<LitStr>;
    let mut description = None::<LitStr>;
    let mut timeout_ms = None::<LitInt>;
    let mut max_memory_bytes = None::<LitInt>;
    let mut max_fuel = None::<LitInt>;
    let mut approval_required = false;
    let mut scopes = Vec::<LitStr>::new();
    let mut capabilities = Vec::<LitStr>::new();

    for attr in &input.attrs {
        if !attr.path().is_ident("skill") {
            continue;
        }
        let metas = attr
            .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
            .expect("invalid #[skill(...)] attribute");
        for meta in metas {
            match meta {
                Meta::NameValue(value) if value.path.is_ident("name") => {
                    if let syn::Expr::Lit(expr) = value.value
                        && let syn::Lit::Str(lit) = expr.lit
                    {
                        name = Some(lit);
                    }
                }
                Meta::NameValue(value) if value.path.is_ident("description") => {
                    if let syn::Expr::Lit(expr) = value.value
                        && let syn::Lit::Str(lit) = expr.lit
                    {
                        description = Some(lit);
                    }
                }
                Meta::NameValue(value) if value.path.is_ident("timeout_ms") => {
                    if let syn::Expr::Lit(expr) = value.value
                        && let syn::Lit::Int(lit) = expr.lit
                    {
                        timeout_ms = Some(lit);
                    }
                }
                Meta::NameValue(value) if value.path.is_ident("max_memory_bytes") => {
                    if let syn::Expr::Lit(expr) = value.value
                        && let syn::Lit::Int(lit) = expr.lit
                    {
                        max_memory_bytes = Some(lit);
                    }
                }
                Meta::NameValue(value) if value.path.is_ident("max_fuel") => {
                    if let syn::Expr::Lit(expr) = value.value
                        && let syn::Lit::Int(lit) = expr.lit
                    {
                        max_fuel = Some(lit);
                    }
                }
                Meta::NameValue(value) if value.path.is_ident("approval_required") => {
                    if let syn::Expr::Lit(expr) = value.value
                        && let syn::Lit::Bool(lit) = expr.lit
                    {
                        approval_required = lit.value;
                    }
                }
                Meta::List(list) if list.path.is_ident("scopes") => {
                    let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
                    scopes.extend(parser.parse2(list.tokens).expect("invalid scopes"));
                }
                Meta::List(list) if list.path.is_ident("capabilities") => {
                    let parser = Punctuated::<LitStr, Token![,]>::parse_terminated;
                    capabilities.extend(parser.parse2(list.tokens).expect("invalid capabilities"));
                }
                _ => {}
            }
        }
    }

    let name = name.expect("skill name is required");
    let description = description.expect("skill description is required");
    let timeout_ms = timeout_ms.unwrap_or_else(|| LitInt::new("5000", name.span()));
    let max_memory_bytes = max_memory_bytes.unwrap_or_else(|| LitInt::new("8388608", name.span()));
    let max_fuel_tokens = if let Some(max_fuel) = max_fuel {
        quote! { Some(#max_fuel) }
    } else {
        quote! { None }
    };

    let scope_tokens = scopes
        .into_iter()
        .map(|scope| quote! { #scope.to_string() });
    let capability_tokens = capabilities.into_iter().map(parse_capability);

    TokenStream::from(quote! {
        impl rain_engine_core::SkillManifestDescriptor for #ident {
            fn skill_manifest() -> rain_engine_core::SkillManifest {
                let schema = schemars::schema_for!(#ident);
                rain_engine_core::SkillManifest {
                    name: #name.to_string(),
                    description: #description.to_string(),
                    input_schema: serde_json::to_value(schema).expect("schema serializes"),
                    required_scopes: vec![#(#scope_tokens),*],
                    capability_grants: vec![#(#capability_tokens),*],
                        resource_policy: rain_engine_core::ResourcePolicy {
                            timeout_ms: #timeout_ms,
                            max_memory_bytes: #max_memory_bytes,
                            max_fuel: #max_fuel_tokens,
                            priority_class: 0,
                            retry_policy: rain_engine_core::RetryPolicy::default(),
                            dry_run_supported: false,
                        },
                        approval_required: #approval_required,
                        circuit_breaker_threshold: 0.5,
                    }
                }
            }
    })
}

fn parse_capability(value: LitStr) -> proc_macro2::TokenStream {
    let raw = value.value();
    if raw == "log" {
        quote! { rain_engine_core::SkillCapability::StructuredLog }
    } else if let Some(namespace) = raw.strip_prefix("kv:") {
        quote! {
            rain_engine_core::SkillCapability::KeyValueRead {
                namespaces: vec![#namespace.to_string()],
            }
        }
    } else if let Some(host) = raw.strip_prefix("http:") {
        quote! {
            rain_engine_core::SkillCapability::HttpOutbound {
                allow_hosts: vec![#host.to_string()],
            }
        }
    } else {
        panic!("unsupported capability `{raw}`");
    }
}