use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, Ident, ExprArray, Token, LitStr, LitBool};
use syn::parse::{Parse, ParseStream};
struct ModuleArgs {
secrets: Vec<syn::Expr>,
rotate: bool,
endpoints: Vec<syn::Expr>,
health: Option<syn::LitStr>,
metrics: bool,
version: Option<syn::LitStr>,
state_fn: Option<syn::Expr>,
channels: Option<syn::Expr>,
security_level: Option<syn::Expr>,
auth_required: Option<syn::LitBool>,
}
impl Parse for ModuleArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut secrets = Vec::new();
let mut rotate = false;
let mut endpoints = Vec::new();
let mut health = None;
let mut metrics = false;
let mut version = None;
let mut state_fn = None;
let mut channels = None;
let mut security_level = None;
let mut auth_required = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
if key == "secrets" {
let arr: ExprArray = input.parse()?;
secrets = arr.elems.into_iter().collect();
} else if key == "rotate" {
let lit: LitBool = input.parse()?;
rotate = lit.value;
} else if key == "endpoints" {
let arr: ExprArray = input.parse()?;
endpoints = arr.elems.into_iter().collect();
} else if key == "health" || key == "health_path" {
let lit: LitStr = input.parse()?;
health = Some(lit);
} else if key == "metrics" {
let lit: LitBool = input.parse()?;
metrics = lit.value;
} else if key == "version" {
let lit: LitStr = input.parse()?;
version = Some(lit);
} else if key == "state" {
let expr: syn::Expr = input.parse()?;
state_fn = Some(expr);
} else if key == "channels" {
let expr: syn::Expr = input.parse()?;
channels = Some(expr);
} else if key == "security_level" {
let expr: syn::Expr = input.parse()?;
security_level = Some(expr);
} else if key == "auth_required" {
let lit: LitBool = input.parse()?;
auth_required = Some(lit);
} else {
let _skip: syn::Expr = input.parse()?;
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(ModuleArgs {
secrets,
rotate,
endpoints,
health,
metrics,
version,
state_fn,
channels,
security_level,
auth_required,
})
}
}
#[proc_macro_attribute]
pub fn module(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as ModuleArgs);
let func = parse_macro_input!(item as ItemFn);
module_impl(args, func).into()
}
fn module_impl(args: ModuleArgs, func: ItemFn) -> proc_macro2::TokenStream {
let func_name = &func.sig.ident;
let secrets = &args.secrets;
let rotate = args.rotate;
let endpoints = &args.endpoints;
let health_path = match &args.health {
Some(lit) => quote! { #lit },
None => quote! { "/health" },
};
let metrics_enabled = args.metrics;
let version_prefix = match &args.version {
Some(lit) => quote! { Some(#lit.to_string()) },
None => quote! { None },
};
let state_fn_tokens = if let Some(expr) = args.state_fn {
quote! { #expr }
} else {
quote! { |_, _| Default::default() }
};
let expanded = quote! {
#func
#[tokio::main]
async fn main() -> ::pywatt_sdk::Result<()> {
::pywatt_sdk::core::logging::init_module();
let init = ::pywatt_sdk::read_init().await?;
let client = ::pywatt_sdk::security::secrets::get_module_secret_client(&init.orchestrator_api, &init.module_id).await?;
let secret_keys: &[&str] = &[#(#secrets),*];
let secrets = if !secret_keys.is_empty() {
match ::pywatt_sdk::security::secrets::get_secrets(&client, secret_keys).await {
Ok(s) => s,
Err(e) => return Err(::pywatt_sdk::Error::Secret(e)),
}
} else {
Vec::new()
};
if #rotate {
let _ = ::pywatt_sdk::security::secrets::subscribe_secret_rotations(
client.clone(),
secret_keys.iter().map(|s| s.to_string()).collect(),
|_, _new_secret| { }
);
}
let user_state = {
let secrets_typed: Vec<::secrecy::SecretString> = secrets;
#state_fn_tokens(&init, secrets_typed)
};
let app_state = ::pywatt_sdk::AppState::new(
init.module_id.clone(),
init.orchestrator_api.clone(),
client.clone(),
user_state,
);
let mut router = #func_name(app_state.clone()).await;
router = router.layer(::axum::Extension(app_state.clone()));
let path: &str = #health_path;
router = router.route(
path,
::axum::routing::get(|| async {
let info = ::serde_json::json!({
"status": "OK",
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
});
::axum::Json(info)
}),
);
if #metrics_enabled {
router = router.route(
"/metrics",
::axum::routing::get(|| async {
"# No metrics implemented yet\n"
}),
);
}
let mut eps: Vec<::pywatt_sdk::AnnouncedEndpoint> = vec![#(#endpoints),*];
if let Some(prefix) = #version_prefix {
for ep in &mut eps {
if ep.path != #health_path {
ep.path = format!("/{}{}", prefix, ep.path);
}
}
}
eps.push(::pywatt_sdk::AnnouncedEndpoint {
path: #health_path.to_string(),
methods: vec!["GET".to_string()],
auth: None,
});
if #metrics_enabled {
eps.push(::pywatt_sdk::AnnouncedEndpoint {
path: "/metrics".to_string(),
methods: vec!["GET".to_string()],
auth: None,
});
}
let announce = ::pywatt_sdk::ModuleAnnounce {
listen: match &init.listen {
::pywatt_sdk::communication::ipc_types::ListenAddress::Tcp(addr) => addr.to_string(),
_ => "127.0.0.1:0".to_string(),
},
endpoints: eps,
};
::pywatt_sdk::send_announce(&announce)?;
match init.listen {
::pywatt_sdk::communication::ipc_types::ListenAddress::Tcp(addr) => {
let listener = ::tokio::net::TcpListener::bind(addr).await?;
::axum::serve(listener, router.into_make_service()).await?;
}
_ => {
::tracing::error!("unsupported listen address");
return Err(::pywatt_sdk::Error::Config(
"Only TCP listen is supported".into()
));
}
}
Ok(())
}
};
expanded
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn it_compiles() {
let input = quote! {
#[pywatt_sdk::module]
async fn foo(state: AppState<()>) -> Router { Router::new() }
};
let attr = proc_macro2::TokenStream::new(); let args = syn::parse2::<ModuleArgs>(attr).unwrap();
let func = syn::parse2::<ItemFn>(input.clone()).unwrap();
let _ = module_impl(args, func);
}
}