use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{
parse_macro_input, parse_quote, Attribute, Block, Expr, ExprLit, Fields, FnArg,
GenericArgument, Ident, ImplItem, ItemFn, ItemImpl, ItemStruct, Lit, LitInt, LitStr, Meta,
PatType, Path, PathArguments, ReturnType, Token, Type,
};
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Injectable(_attr: TokenStream, item: TokenStream) -> TokenStream {
let st = parse_macro_input!(item as ItemStruct);
let name = st.ident.clone();
let name_str = name.to_string();
let mut deps_tys: Vec<Type> = Vec::new();
let mut ctor_field_inits: Vec<TokenStream2> = Vec::new();
match &st.fields {
Fields::Named(named) => {
for f in &named.named {
let fname = f.ident.as_ref().unwrap();
if let Some(inner) = inject_inner_ty(&f.ty) {
deps_tys.push(inner.clone());
ctor_field_inits.push(quote! {
#fname: ::arcly_http::__macro_support::Inject::__from_static(
__r.get::<#inner>(),
)
});
} else {
let fty = &f.ty;
ctor_field_inits.push(quote! {
#fname: <#fty as ::core::default::Default>::default()
});
}
}
}
Fields::Unit => {
}
Fields::Unnamed(_) => {
return syn::Error::new(
st.fields.span(),
"#[Injectable] does not yet support tuple structs — use a named-field struct",
)
.to_compile_error()
.into();
}
}
let deps_ty_paths: Vec<TokenStream2> = deps_tys.iter().map(|t| quote!(#t)).collect();
let desc_name = format_ident!("__ARCLY_PROVIDER_{}", name_str.to_uppercase());
let ctor_body = match &st.fields {
Fields::Named(_) => quote! { #name { #( #ctor_field_inits ),* } },
Fields::Unit => quote! { #name },
Fields::Unnamed(_) => unreachable!(),
};
quote! {
#st
impl #name {
#[doc(hidden)]
pub fn __arcly_build(__r: &::arcly_http::__macro_support::Resolver<'_>) -> Self {
#ctor_body
}
}
#[allow(non_upper_case_globals)]
static #desc_name: ::arcly_http::__macro_support::ProviderDescriptor =
::arcly_http::__macro_support::ProviderDescriptor {
name: #name_str,
type_id_fn: || ::core::any::TypeId::of::<#name>(),
deps_fn: || ::std::vec![
#( ::core::any::TypeId::of::<#deps_ty_paths>() ),*
],
build: |__r| ::std::sync::Arc::new(#name::__arcly_build(__r)),
};
impl #name {
#[doc(hidden)]
pub const fn __arcly_descriptor() -> &'static ::arcly_http::__macro_support::ProviderDescriptor {
&#desc_name
}
}
}
.into()
}
fn inject_inner_ty(ty: &Type) -> Option<&Type> {
let Type::Path(tp) = ty else { return None };
let seg = tp.path.segments.last()?;
if seg.ident != "Inject" {
return None;
}
first_generic(&seg.arguments)
}
struct ModuleArgs {
providers: Vec<Path>,
controllers: Vec<Path>,
imports: Vec<Path>,
gateways: Vec<Path>,
}
impl Parse for ModuleArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut out = ModuleArgs {
providers: vec![],
controllers: vec![],
imports: vec![],
gateways: vec![],
};
while !input.is_empty() {
let key: Ident = input.parse()?;
let content;
syn::parenthesized!(content in input);
let list: Punctuated<Path, Token![,]> =
content.parse_terminated(Path::parse, Token![,])?;
match key.to_string().as_str() {
"providers" => out.providers.extend(list),
"controllers" => out.controllers.extend(list),
"imports" => out.imports.extend(list),
"gateways" => out.gateways.extend(list),
other => return Err(syn::Error::new(
key.span(),
format!("unknown Module key `{other}` (expected providers/controllers/imports/gateways)"),
)),
}
let _ = input.parse::<Token![,]>();
}
Ok(out)
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Module(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as ModuleArgs);
let st = parse_macro_input!(item as ItemStruct);
let st_name = &st.ident;
let mod_name_str = st_name.to_string();
let provider_refs: Vec<TokenStream2> = args
.providers
.iter()
.map(|p| {
quote! { <#p>::__arcly_descriptor() }
})
.collect();
let controller_names: Vec<TokenStream2> = args
.controllers
.iter()
.map(|p| {
let n = p
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
quote! { #n }
})
.collect();
let gateway_names: Vec<TokenStream2> = args
.gateways
.iter()
.map(|p| {
let n = p
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
quote! { #n }
})
.collect();
let import_fns: Vec<TokenStream2> = args
.imports
.iter()
.map(|p| {
quote! { (<#p as ::arcly_http::__macro_support::Module>::descriptor) }
})
.collect();
let static_name = format_ident!("__ARCLY_MODULE_{}", st_name.to_string().to_uppercase());
quote! {
#st
#[allow(non_upper_case_globals)]
static #static_name: ::arcly_http::__macro_support::ModuleDescriptor =
::arcly_http::__macro_support::ModuleDescriptor {
name: #mod_name_str,
providers: &[ #( #provider_refs ),* ],
controllers: &[ #( #controller_names ),* ],
imports: &[ #( #import_fns ),* ],
gateways: &[ #( #gateway_names ),* ],
};
impl ::arcly_http::__macro_support::Module for #st_name {
fn descriptor() -> &'static ::arcly_http::__macro_support::ModuleDescriptor {
&#static_name
}
}
::arcly_http::inventory::submit! {
&#static_name
}
}
.into()
}
struct RouteArgs {
path: LitStr,
guards: Vec<Expr>,
tags: Vec<LitStr>,
security: Vec<LitStr>,
summary: Option<LitStr>,
description: Option<LitStr>,
operation_id: Option<LitStr>,
status: Option<LitInt>,
deprecated: bool,
}
impl Parse for RouteArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path: LitStr = input.parse()?;
let mut out = RouteArgs {
path,
guards: vec![],
tags: vec![],
security: vec![],
summary: None,
description: None,
operation_id: None,
status: None,
deprecated: false,
};
while input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
if input.is_empty() {
break;
}
let key: Ident = input.parse()?;
let k = key.to_string();
if k == "deprecated" {
out.deprecated = true;
continue;
}
let content;
syn::parenthesized!(content in input);
match k.as_str() {
"guards" => {
let list: Punctuated<Expr, Token![,]> = content.parse_terminated(Expr::parse, Token![,])?;
out.guards.extend(list);
}
"tags" => {
let list: Punctuated<LitStr, Token![,]> = content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
out.tags.extend(list);
}
"security" => {
let list: Punctuated<LitStr, Token![,]> = content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
out.security.extend(list);
}
"summary" => out.summary = Some(content.parse()?),
"description" => out.description = Some(content.parse()?),
"operation_id" => out.operation_id = Some(content.parse()?),
"status" => out.status = Some(content.parse()?),
other => return Err(syn::Error::new(
key.span(),
format!("unknown route key `{other}` (expected guards/tags/security/summary/description/operation_id/status/deprecated)"),
)),
}
}
Ok(out)
}
}
struct ControllerArgs {
prefix: LitStr,
tags: Vec<LitStr>,
}
impl Parse for ControllerArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let prefix: LitStr = input.parse()?;
let mut tags: Vec<LitStr> = vec![];
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
if !input.is_empty() {
let key: Ident = input.parse()?;
if key != "tags" {
return Err(syn::Error::new(key.span(), "expected `tags(...)`"));
}
let content;
syn::parenthesized!(content in input);
let list: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
tags.extend(list);
}
}
Ok(Self { prefix, tags })
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Controller(attr: TokenStream, item: TokenStream) -> TokenStream {
if let Ok(imp) = syn::parse::<ItemImpl>(item.clone()) {
return controller_on_impl(attr, imp);
}
item
}
fn controller_on_impl(attr: TokenStream, mut imp: ItemImpl) -> TokenStream {
let ControllerArgs {
prefix,
tags: ctrl_tags,
} = parse_macro_input!(attr as ControllerArgs);
let mut api_version = String::new();
let mut sunset = String::new();
{
let mut keep: Vec<Attribute> = Vec::with_capacity(imp.attrs.len());
for a in imp.attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match id.as_str() {
"Version" => {
if let Ok(v) = a.parse_args::<LitStr>() {
api_version = v.value().trim_matches('/').to_owned();
}
}
"Deprecated" => {
let _ = a.parse_nested_meta(|meta| {
if meta.path.is_ident("sunset") {
let v: LitStr = meta.value()?.parse()?;
sunset = v.value();
}
Ok(())
});
}
_ => keep.push(a),
}
}
imp.attrs = keep;
}
let raw_prefix = prefix.value();
let prefix_str = if api_version.is_empty() {
raw_prefix
} else {
format!("/{api_version}{raw_prefix}")
};
let self_ty = (*imp.self_ty).clone();
let controller_name = match &self_ty {
Type::Path(tp) => tp
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default(),
_ => String::new(),
};
let mut route_registrations: Vec<TokenStream2> = Vec::new();
let mut errors: Vec<syn::Error> = Vec::new();
for item in imp.items.iter_mut() {
let ImplItem::Fn(m) = item else { continue };
let mut route_attr_idx: Option<usize> = None;
let mut route_method: Option<&'static str> = None;
let mut interceptor_attr_idxs: Vec<usize> = Vec::new();
for (i, a) in m.attrs.iter().enumerate() {
let ident = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match ident.as_str() {
"Get" | "Post" | "Put" | "Delete" | "Patch" => {
route_attr_idx = Some(i);
route_method = Some(match ident.as_str() {
"Get" => "GET",
"Post" => "POST",
"Put" => "PUT",
"Delete" => "DELETE",
"Patch" => "PATCH",
_ => unreachable!(),
});
}
"UseInterceptors" => interceptor_attr_idxs.push(i),
_ => {}
}
}
let Some(idx) = route_attr_idx else { continue };
let route_method = route_method.unwrap();
let route_attr = m.attrs[idx].clone();
let route_args: RouteArgs = match route_attr.parse_args() {
Ok(a) => a,
Err(e) => {
errors.push(e);
continue;
}
};
let mut interceptor_paths: Vec<Path> = Vec::new();
for i in &interceptor_attr_idxs {
let a = &m.attrs[*i];
match a.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated) {
Ok(list) => interceptor_paths.extend(list),
Err(e) => errors.push(e),
}
}
let local_path = route_args.path.value();
let full = join_paths(&prefix_str, &local_path);
let merged_tags: Vec<LitStr> = ctrl_tags
.iter()
.cloned()
.chain(route_args.tags.iter().cloned())
.collect();
let mut keep: Vec<Attribute> = Vec::with_capacity(m.attrs.len());
for (i, a) in m.attrs.iter().enumerate() {
if i == idx || interceptor_attr_idxs.contains(&i) {
continue;
}
keep.push(a.clone());
}
m.attrs = keep;
let (cache_ttl_secs, cache_key) = match harvest_cache_attrs(&mut m.attrs) {
Ok(p) => p,
Err(e) => {
errors.push(e);
continue;
}
};
let audit = match harvest_audit_attr(&mut m.attrs) {
Ok(a) => a,
Err(e) => {
errors.push(e);
continue;
}
};
let timeout_ms = match harvest_timeout_attr(&mut m.attrs) {
Ok(t) => t,
Err(e) => {
errors.push(e);
continue;
}
};
let transactional = harvest_transactional_attr(&mut m.attrs);
let idempotent_ttl = match harvest_idempotent_attr(&mut m.attrs) {
Ok(t) => t,
Err(e) => {
errors.push(e);
continue;
}
};
let policies = match harvest_policies_attr(&mut m.attrs) {
Ok(p) => p,
Err(e) => {
errors.push(e);
continue;
}
};
let mask_fields = match harvest_mask_attr(&mut m.attrs) {
Ok(f) => f,
Err(e) => {
errors.push(e);
continue;
}
};
let reg = match build_method_route_registration(
&self_ty,
m,
route_method,
full,
&route_args,
&merged_tags,
&interceptor_paths,
cache_ttl_secs,
&cache_key,
&controller_name,
&audit,
timeout_ms,
&api_version,
&sunset,
transactional,
idempotent_ttl,
&policies,
&mask_fields,
) {
Ok(ts) => ts,
Err(e) => {
errors.push(e);
continue;
}
};
route_registrations.push(reg);
for input in m.sig.inputs.iter_mut() {
if let FnArg::Typed(pt) = input {
pt.attrs.retain(|a| {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
!matches!(id.as_str(), "Param" | "Query" | "Body" | "Header")
});
}
}
}
if let Some(err) = errors.into_iter().reduce(|mut a, b| {
a.combine(b);
a
}) {
return err.to_compile_error().into();
}
quote! {
#imp
#( #route_registrations )*
}
.into()
}
fn harvest_cache_attrs(attrs: &mut Vec<Attribute>) -> syn::Result<(u64, String)> {
let mut ttl_secs: u64 = 0;
let mut key: String = String::new();
let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
for a in attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match id.as_str() {
"CacheTTL" => {
let n: LitInt = a.parse_args()?;
ttl_secs = n.base10_parse()?;
}
"CacheKey" => {
let s: LitStr = a.parse_args()?;
key = s.value();
}
_ => {
keep.push(a);
}
}
}
*attrs = keep;
Ok((ttl_secs, key))
}
fn harvest_audit_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<(String, String)>> {
let mut found: Option<(String, String)> = None;
let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
for a in attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "AuditLog" {
let mut action = String::new();
let mut resource = String::new();
a.parse_nested_meta(|meta| {
if meta.path.is_ident("action") {
let v: LitStr = meta.value()?.parse()?;
action = v.value();
} else if meta.path.is_ident("resource") {
let v: LitStr = meta.value()?.parse()?;
resource = v.value();
} else {
return Err(meta.error("expected `action = \"…\"` or `resource = \"…\"`"));
}
Ok(())
})?;
if action.is_empty() {
return Err(syn::Error::new_spanned(
&a,
"#[AuditLog] requires action = \"…\"",
));
}
found = Some((action, resource));
} else {
keep.push(a);
}
}
*attrs = keep;
Ok(found)
}
fn harvest_timeout_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<u64>> {
let mut found: Option<u64> = None;
let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
for a in attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "Timeout" {
let lit: LitStr = a.parse_args()?;
let raw = lit.value();
let ms = parse_duration_str_ms(&raw).ok_or_else(|| {
syn::Error::new_spanned(&a, "expected a duration like \"250ms\", \"2s\", or \"1m\"")
})?;
found = Some(ms);
} else {
keep.push(a);
}
}
*attrs = keep;
Ok(found)
}
fn parse_duration_str_ms(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(v) = s.strip_suffix("ms") {
return v.trim().parse().ok();
}
if let Some(v) = s.strip_suffix('s') {
return v.trim().parse::<u64>().ok().map(|n| n * 1_000);
}
if let Some(v) = s.strip_suffix('h') {
return v.trim().parse::<u64>().ok().map(|n| n * 3_600_000);
}
if let Some(v) = s.strip_suffix('m') {
return v.trim().parse::<u64>().ok().map(|n| n * 60_000);
}
s.parse().ok()
}
fn harvest_transactional_attr(attrs: &mut Vec<Attribute>) -> bool {
let before = attrs.len();
attrs.retain(|a| {
a.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default()
!= "Transactional"
});
attrs.len() != before
}
fn harvest_idempotent_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Option<u64>> {
let mut found: Option<u64> = None;
let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
for a in attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "Idempotent" {
let mut ttl_secs: u64 = 24 * 3600;
a.parse_nested_meta(|meta| {
if meta.path.is_ident("ttl") {
let v: LitStr = meta.value()?.parse()?;
let ms = parse_duration_str_ms(&v.value())
.ok_or_else(|| meta.error("expected a duration like \"30m\", \"24h\""))?;
ttl_secs = (ms / 1000).max(1);
}
Ok(())
})?;
found = Some(ttl_secs);
} else {
keep.push(a);
}
}
*attrs = keep;
Ok(found)
}
fn harvest_policies_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Vec<LitStr>> {
let mut found: Vec<LitStr> = Vec::new();
let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
for a in attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "RequirePolicies" {
let list = a.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?;
found.extend(list);
} else {
keep.push(a);
}
}
*attrs = keep;
Ok(found)
}
fn harvest_mask_attr(attrs: &mut Vec<Attribute>) -> syn::Result<Vec<LitStr>> {
let mut found: Vec<LitStr> = Vec::new();
let mut keep: Vec<Attribute> = Vec::with_capacity(attrs.len());
for a in attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "MaskFields" {
let list = a.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?;
found.extend(list);
} else {
keep.push(a);
}
}
*attrs = keep;
Ok(found)
}
fn join_paths(prefix: &str, local: &str) -> String {
let p = prefix.trim_end_matches('/');
let l = if local.starts_with('/') {
local
} else {
return format!("{p}/{local}");
};
if p.is_empty() {
l.to_owned()
} else {
format!("{p}{l}")
}
}
fn route_idents(controller: &str, fn_name: &str) -> (Ident, Ident, Ident) {
let (thunk, desc, spec) = if controller.is_empty() {
(
format!("__arcly_thunk_{fn_name}"),
format!("__ARCLY_ROUTE_{}", fn_name.to_uppercase()),
format!("__ARCLY_SPEC_{}", fn_name.to_uppercase()),
)
} else {
(
format!("__arcly_thunk_{controller}_{fn_name}"),
format!(
"__ARCLY_ROUTE_{}_{}",
controller.to_uppercase(),
fn_name.to_uppercase()
),
format!(
"__ARCLY_SPEC_{}_{}",
controller.to_uppercase(),
fn_name.to_uppercase()
),
)
};
(
format_ident!("{}", thunk),
format_ident!("{}", desc),
format_ident!("{}", spec),
)
}
#[allow(clippy::too_many_arguments)]
fn build_method_route_registration(
self_ty: &Type,
m: &syn::ImplItemFn,
method: &'static str,
full_path: String,
args: &RouteArgs,
tags: &[LitStr],
interceptors: &[Path],
cache_ttl_secs: u64,
cache_key: &str,
controller_name: &str,
audit: &Option<(String, String)>,
timeout_ms: Option<u64>,
api_version: &str,
sunset: &str,
transactional: bool,
idempotent_ttl: Option<u64>,
policies: &[LitStr],
mask_fields: &[LitStr],
) -> syn::Result<TokenStream2> {
let fn_name = m.sig.ident.clone();
let (thunk_name, desc_name, spec_name) = route_idents(controller_name, &fn_name.to_string());
let method_ident = Ident::new(method, Span::call_site());
let doc = collect_doc_comments(&m.attrs);
let mut extract_stmts: Vec<TokenStream2> = Vec::new();
let mut call_args: Vec<TokenStream2> = Vec::new();
let mut spec_params: Vec<TokenStream2> = Vec::new();
let mut has_body = false;
let mut body_ty: Option<Type> = None;
let mut query_ty: Option<Type> = None;
for (i, input) in m.sig.inputs.iter().enumerate() {
let FnArg::Typed(pt) = input else {
return Err(syn::Error::new(
input.span(),
"controller methods must not take `self` — use Inject<T> fields instead",
));
};
let var = format_ident!("__arg_{i}");
let (kind, ty) = classify_arg(pt)?;
let stmt = emit_extractor(
&kind,
&ty,
&var,
&mut spec_params,
&mut has_body,
&mut body_ty,
&mut query_ty,
);
extract_stmts.push(stmt);
call_args.push(quote! { #var });
}
let mut guard_stmts: Vec<TokenStream2> = args
.guards
.iter()
.map(|g| {
quote! {
<_ as ::arcly_http::__macro_support::Guard>::check(&#g, &ctx)?;
}
})
.collect();
if !policies.is_empty() {
let action_lits: Vec<TokenStream2> = policies.iter().map(|p| quote!(#p)).collect();
guard_stmts.push(quote! {
::arcly_http::__macro_support::check_policies(
&ctx, &[ #( #action_lits ),* ], ::arcly_http::serde_json::Value::Null,
)?;
});
}
let run_body = if transactional {
quote! {
let __tx_ctx = ctx.clone();
::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
::arcly_http::__macro_support::run_transactional(&__tx_ctx, async move {
#( #guard_stmts )*
#( #extract_stmts )*
<#self_ty>::#fn_name( #( #call_args ),* ).await
}).await
)
}
} else {
quote! {
#( #guard_stmts )*
#( #extract_stmts )*
::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
<#self_ty>::#fn_name( #( #call_args ),* ).await
)
}
};
let inner = quote! {
let __run = async move {
#run_body
};
match __run.await {
::core::result::Result::Ok(v) => {
::arcly_http::__axum::response::IntoResponse::into_response(v)
}
::core::result::Result::Err(e) => {
::arcly_http::__axum::response::IntoResponse::into_response(e)
}
}
};
let inner = match timeout_ms {
Some(ms) => {
let route_lit = LitStr::new(&full_path, Span::call_site());
quote! {
::arcly_http::__macro_support::run_with_timeout(
#ms, #route_lit, async move { #inner },
).await
}
}
None => inner,
};
let inner = match audit {
Some((action, resource)) => {
let action_lit = LitStr::new(action, Span::call_site());
let resource_lit = LitStr::new(resource, Span::call_site());
quote! {
let __audit_ctx = ctx.clone();
let __resp: ::arcly_http::__axum::response::Response = { #inner };
::arcly_http::__macro_support::emit_route_audit(
&__audit_ctx, #action_lit, #resource_lit, __resp.status().as_u16(),
);
__resp
}
}
None => inner,
};
let inner = if mask_fields.is_empty() {
inner
} else {
let mask_lits: Vec<TokenStream2> = mask_fields.iter().map(|f| quote!(#f)).collect();
quote! {
let __mask_ctx = ctx.clone();
let __resp: ::arcly_http::__axum::response::Response = { #inner };
::arcly_http::__macro_support::mask_response(
&__mask_ctx, &[ #( #mask_lits ),* ], __resp,
).await
}
};
let inner = match idempotent_ttl {
Some(ttl) => {
let route_lit = LitStr::new(&full_path, Span::call_site());
quote! {
let __idem_ctx = ctx.clone();
::arcly_http::__macro_support::run_idempotent(
&__idem_ctx, #ttl, #route_lit, async move { #inner },
).await
}
}
None => inner,
};
let thunk_body = wrap_interceptors(inner, interceptors);
let fn_str = fn_name.to_string();
let summary_str = args
.summary
.as_ref()
.map(|s| s.value())
.unwrap_or_else(|| fn_str.clone());
let operation_id = args
.operation_id
.as_ref()
.map(|s| s.value())
.unwrap_or_else(|| fn_str.clone());
let description_str = args.description.as_ref().map(|s| s.value()).unwrap_or(doc);
let deprecated = args.deprecated;
let tag_lits: Vec<TokenStream2> = tags.iter().map(|t| quote!(#t)).collect();
let sec_lits: Vec<TokenStream2> = args.security.iter().map(|s| quote!(#s)).collect();
let status_expr = match &args.status {
Some(n) => quote! { ::core::option::Option::Some(#n as u16) },
None => quote! { ::core::option::Option::None },
};
let body_schema_expr = schema_expr(&body_ty);
let query_schema_expr = schema_expr(&query_ty);
let response_schema_expr = schema_expr(&extract_response_ty(&m.sig.output));
let full_path_lit = LitStr::new(&full_path, Span::call_site());
let spec_idem_ttl = idempotent_ttl.unwrap_or(0);
let spec_policy_lits: Vec<TokenStream2> = policies.iter().map(|p| quote!(#p)).collect();
let (spec_audit_action, spec_audit_resource) = match audit {
Some((a, r)) => (a.clone(), r.clone()),
None => (String::new(), String::new()),
};
let spec_timeout_ms = timeout_ms.unwrap_or(0);
let spec_mask_lits: Vec<TokenStream2> = mask_fields.iter().map(|f| quote!(#f)).collect();
Ok(quote! {
fn #thunk_name(ctx: ::arcly_http::__macro_support::RequestContext)
-> ::arcly_http::futures::future::BoxFuture<'static, ::arcly_http::__axum::response::Response>
{
::arcly_http::futures::FutureExt::boxed(async move { #thunk_body })
}
#[allow(non_upper_case_globals)]
static #spec_name: ::arcly_http::__macro_support::RouteSpec =
::arcly_http::__macro_support::RouteSpec {
summary: #summary_str,
description: #description_str,
operation_id: #operation_id,
tags: &[ #( #tag_lits ),* ],
security: &[ #( #sec_lits ),* ],
status_code: #status_expr,
deprecated: #deprecated,
params: &[ #( #spec_params ),* ],
has_body: #has_body,
body_schema: #body_schema_expr,
query_schema: #query_schema_expr,
response_schema: #response_schema_expr,
cache_ttl_secs: #cache_ttl_secs,
cache_key: #cache_key,
api_version: #api_version,
sunset: #sunset,
idempotent_ttl_secs: #spec_idem_ttl,
policies: &[ #( #spec_policy_lits ),* ],
audit_action: #spec_audit_action,
audit_resource: #spec_audit_resource,
timeout_ms: #spec_timeout_ms,
transactional: #transactional,
mask_fields: &[ #( #spec_mask_lits ),* ],
};
#[allow(non_upper_case_globals)]
static #desc_name: ::arcly_http::__macro_support::RouteDescriptor =
::arcly_http::__macro_support::RouteDescriptor {
method: ::arcly_http::__macro_support::HttpMethod::#method_ident,
path: #full_path_lit,
handler: #thunk_name,
spec: &#spec_name,
controller: #controller_name,
};
::arcly_http::inventory::submit! {
&#desc_name
}
})
}
fn schema_expr(ty: &Option<Type>) -> TokenStream2 {
match ty {
Some(t) => quote! { ::core::option::Option::Some(|| ::arcly_http::__schema_for::<#t>()) },
None => quote! { ::core::option::Option::None },
}
}
fn wrap_interceptors(inner: TokenStream2, interceptors: &[Path]) -> TokenStream2 {
match interceptors.len() {
0 => return inner,
1 => {
let icp = &interceptors[0];
return quote! {
{
static __ICP: #icp = #icp;
let __inner = ::arcly_http::__macro_support::NextHandler::new(
move |ctx: ::arcly_http::__macro_support::RequestContext|
::arcly_http::futures::FutureExt::boxed(async move {
let ctx = ctx;
#inner
})
);
<#icp as ::arcly_http::__macro_support::Interceptor>::around(&__ICP, ctx, __inner).await
}
};
}
_ => {}
}
let mut current = quote! {
::arcly_http::__macro_support::NextHandler::new(
move |ctx: ::arcly_http::__macro_support::RequestContext|
::arcly_http::futures::FutureExt::boxed(async move {
let ctx = ctx;
#inner
})
)
};
for icp in interceptors.iter().rev() {
current = quote! {
{
static __ICP: #icp = #icp;
let __inner = #current;
::arcly_http::__macro_support::NextHandler::new(
move |ctx: ::arcly_http::__macro_support::RequestContext| {
<#icp as ::arcly_http::__macro_support::Interceptor>::around(&__ICP, ctx, __inner.__clone_for_chain())
},
)
}
};
}
quote! { #current.run(ctx).await }
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Get(a: TokenStream, i: TokenStream) -> TokenStream {
route_free_fn(a, i, "GET")
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Post(a: TokenStream, i: TokenStream) -> TokenStream {
route_free_fn(a, i, "POST")
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Put(a: TokenStream, i: TokenStream) -> TokenStream {
route_free_fn(a, i, "PUT")
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Delete(a: TokenStream, i: TokenStream) -> TokenStream {
route_free_fn(a, i, "DELETE")
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Patch(a: TokenStream, i: TokenStream) -> TokenStream {
route_free_fn(a, i, "PATCH")
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn CacheTTL(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn AuditLog(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Timeout(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Version(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Deprecated(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Transactional(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Idempotent(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn RequirePolicies(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn EventPattern(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn EventConsumer(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut imp = parse_macro_input!(item as ItemImpl);
let self_ty = (*imp.self_ty).clone();
let consumer_name = match &self_ty {
Type::Path(tp) => tp
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default(),
_ => String::new(),
};
let mut registrations: Vec<TokenStream2> = Vec::new();
let mut errors: Vec<syn::Error> = Vec::new();
for item in imp.items.iter_mut() {
let ImplItem::Fn(m) = item else { continue };
let mut topic: Option<LitStr> = None;
let mut keep: Vec<Attribute> = Vec::with_capacity(m.attrs.len());
for a in m.attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "EventPattern" {
match a.parse_args::<LitStr>() {
Ok(t) => topic = Some(t),
Err(e) => errors.push(e),
}
} else {
keep.push(a);
}
}
m.attrs = keep;
let Some(topic) = topic else { continue };
let fn_name = m.sig.ident.clone();
let thunk = format_ident!("__arcly_event_{}_{}", consumer_name, fn_name);
let desc = format_ident!("__ARCLY_EVENT_DESC_{}_{}", consumer_name, fn_name);
let consumer_lit = LitStr::new(&consumer_name, Span::call_site());
registrations.push(quote! {
#[allow(non_snake_case)]
fn #thunk(ctx: ::arcly_http::__macro_support::EventContext)
-> ::arcly_http::futures::future::BoxFuture<'static, ::core::result::Result<(), ::std::string::String>>
{
::arcly_http::futures::FutureExt::boxed(<#self_ty>::#fn_name(ctx))
}
#[allow(non_upper_case_globals)]
static #desc: ::arcly_http::__macro_support::EventHandlerDescriptor =
::arcly_http::__macro_support::EventHandlerDescriptor {
topic: #topic,
consumer: #consumer_lit,
handler: #thunk,
};
::arcly_http::inventory::submit! { &#desc }
});
}
if let Some(err) = errors.into_iter().reduce(|mut a, b| {
a.combine(b);
a
}) {
return err.to_compile_error().into();
}
quote! {
#imp
#( #registrations )*
}
.into()
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn MaskFields(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn CacheKey(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn UseInterceptors(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
fn route_free_fn(attr: TokenStream, item: TokenStream, method: &'static str) -> TokenStream {
let args = parse_macro_input!(attr as RouteArgs);
let mut f = parse_macro_input!(item as ItemFn);
let mut interceptors: Vec<Path> = Vec::new();
let mut keep_attrs: Vec<Attribute> = Vec::with_capacity(f.attrs.len());
for a in f.attrs.drain(..) {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if id == "UseInterceptors" {
match a.parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated) {
Ok(list) => interceptors.extend(list),
Err(e) => return e.to_compile_error().into(),
}
} else {
keep_attrs.push(a);
}
}
f.attrs = keep_attrs;
let (cache_ttl_secs, cache_key): (u64, String) = match harvest_cache_attrs(&mut f.attrs) {
Ok(p) => p,
Err(e) => return e.to_compile_error().into(),
};
let path_lit = args.path.clone();
let full_path = path_lit.value();
let fn_name = f.sig.ident.clone();
let (thunk_name, desc_name, spec_name) = route_idents("", &fn_name.to_string());
let method_ident = Ident::new(method, Span::call_site());
let doc = collect_doc_comments(&f.attrs);
let mut extract_stmts: Vec<TokenStream2> = Vec::new();
let mut call_args: Vec<TokenStream2> = Vec::new();
let mut errors: Vec<syn::Error> = Vec::new();
let mut spec_params: Vec<TokenStream2> = Vec::new();
let mut has_body = false;
let mut body_ty: Option<Type> = None;
let mut query_ty: Option<Type> = None;
for (i, input) in f.sig.inputs.iter_mut().enumerate() {
let FnArg::Typed(pt) = input else {
errors.push(syn::Error::new(input.span(), "handler must not take self"));
continue;
};
let var = format_ident!("__arg_{i}");
match classify_arg(pt) {
Ok((kind, ty)) => {
let stmt = emit_extractor(
&kind,
&ty,
&var,
&mut spec_params,
&mut has_body,
&mut body_ty,
&mut query_ty,
);
extract_stmts.push(stmt);
call_args.push(quote! { #var });
}
Err(e) => errors.push(e),
}
pt.attrs.retain(|a| {
let id = a
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
!matches!(id.as_str(), "Param" | "Query" | "Body" | "Header")
});
}
if let Some(err) = errors.into_iter().reduce(|mut a, b| {
a.combine(b);
a
}) {
return err.to_compile_error().into();
}
let guard_stmts: Vec<TokenStream2> = args
.guards
.iter()
.map(|g| {
quote! {
<_ as ::arcly_http::__macro_support::Guard>::check(&#g, &ctx)?;
}
})
.collect();
let inner = quote! {
let __run = async move {
#( #guard_stmts )*
#( #extract_stmts )*
::core::result::Result::<_, ::arcly_http::__macro_support::Error>::Ok(
#fn_name( #( #call_args ),* ).await
)
};
match __run.await {
::core::result::Result::Ok(v) => {
::arcly_http::__axum::response::IntoResponse::into_response(v)
}
::core::result::Result::Err(e) => {
::arcly_http::__axum::response::IntoResponse::into_response(e)
}
}
};
let thunk_body = wrap_interceptors(inner, &interceptors);
let fn_str = fn_name.to_string();
let summary_str = args
.summary
.as_ref()
.map(|s| s.value())
.unwrap_or_else(|| fn_str.clone());
let operation_id = args
.operation_id
.as_ref()
.map(|s| s.value())
.unwrap_or_else(|| fn_str.clone());
let description_str = args.description.as_ref().map(|s| s.value()).unwrap_or(doc);
let deprecated = args.deprecated;
let tag_lits: Vec<TokenStream2> = args.tags.iter().map(|t| quote!(#t)).collect();
let sec_lits: Vec<TokenStream2> = args.security.iter().map(|s| quote!(#s)).collect();
let status_expr = match &args.status {
Some(n) => quote! { ::core::option::Option::Some(#n as u16) },
None => quote! { ::core::option::Option::None },
};
let body_schema_expr = schema_expr(&body_ty);
let query_schema_expr = schema_expr(&query_ty);
let response_schema_expr = schema_expr(&extract_response_ty(&f.sig.output));
let full_path_lit = LitStr::new(&full_path, Span::call_site());
quote! {
#f
fn #thunk_name(ctx: ::arcly_http::__macro_support::RequestContext)
-> ::arcly_http::futures::future::BoxFuture<'static, ::arcly_http::__axum::response::Response>
{
::arcly_http::futures::FutureExt::boxed(async move { #thunk_body })
}
#[allow(non_upper_case_globals)]
static #spec_name: ::arcly_http::__macro_support::RouteSpec =
::arcly_http::__macro_support::RouteSpec {
summary: #summary_str,
description: #description_str,
operation_id: #operation_id,
tags: &[ #( #tag_lits ),* ],
security: &[ #( #sec_lits ),* ],
status_code: #status_expr,
deprecated: #deprecated,
params: &[ #( #spec_params ),* ],
has_body: #has_body,
body_schema: #body_schema_expr,
query_schema: #query_schema_expr,
response_schema: #response_schema_expr,
cache_ttl_secs: #cache_ttl_secs,
cache_key: #cache_key,
api_version: "",
sunset: "",
idempotent_ttl_secs: 0,
policies: &[],
audit_action: "",
audit_resource: "",
timeout_ms: 0,
transactional: false,
mask_fields: &[],
};
#[allow(non_upper_case_globals)]
static #desc_name: ::arcly_http::__macro_support::RouteDescriptor =
::arcly_http::__macro_support::RouteDescriptor {
method: ::arcly_http::__macro_support::HttpMethod::#method_ident,
path: #full_path_lit,
handler: #thunk_name,
spec: &#spec_name,
controller: "",
};
::arcly_http::inventory::submit! {
&#desc_name
}
}
.into()
}
enum ParamKind {
Param(LitStr),
Query,
Body,
Header(LitStr),
Ctx,
FromContext,
}
fn classify_arg(arg: &PatType) -> syn::Result<(ParamKind, Type)> {
for attr in &arg.attrs {
let ident = attr
.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
match ident.as_str() {
"Param" => {
let name: LitStr = attr.parse_args()?;
return Ok((ParamKind::Param(name), (*arg.ty).clone()));
}
"Query" => return Ok((ParamKind::Query, (*arg.ty).clone())),
"Body" => return Ok((ParamKind::Body, (*arg.ty).clone())),
"Header" => {
let name: LitStr = attr.parse_args()?;
return Ok((ParamKind::Header(name), (*arg.ty).clone()));
}
_ => {}
}
}
let ty_ref = &*arg.ty;
let ty_str = quote!(#ty_ref).to_string();
let ty = (*arg.ty).clone();
let kind = if ty_str.contains("RequestContext") {
ParamKind::Ctx
} else {
ParamKind::FromContext
};
Ok((kind, ty))
}
fn emit_extractor(
kind: &ParamKind,
ty: &Type,
var: &Ident,
spec_params: &mut Vec<TokenStream2>,
has_body: &mut bool,
body_ty: &mut Option<Type>,
query_ty: &mut Option<Type>,
) -> TokenStream2 {
match kind {
ParamKind::Param(name) => {
spec_params.push(quote! {
::arcly_http::__macro_support::ParamSpec {
name: #name,
loc: ::arcly_http::__macro_support::ParamLoc::Path,
required: true,
schema: || ::arcly_http::__schema_for::<#ty>(),
}
});
quote! { let #var: #ty = ::arcly_http::__macro_support::extract_param(&ctx, #name)?; }
}
ParamKind::Query => {
*query_ty = Some(ty.clone());
quote! { let #var: #ty = ::arcly_http::__macro_support::extract_query_validated(&ctx)?; }
}
ParamKind::Body => {
*has_body = true;
*body_ty = Some(ty.clone());
quote! { let #var: #ty = ::arcly_http::__macro_support::extract_body_validated(&ctx)?; }
}
ParamKind::Header(name) => {
spec_params.push(quote! {
::arcly_http::__macro_support::ParamSpec {
name: #name,
loc: ::arcly_http::__macro_support::ParamLoc::Header,
required: true,
schema: || ::arcly_http::__schema_for::<#ty>(),
}
});
quote! { let #var: #ty = ::arcly_http::__macro_support::extract_header(&ctx, #name)?.to_owned(); }
}
ParamKind::Ctx => quote! { let #var: #ty = ctx.clone(); },
ParamKind::FromContext => quote! { let #var: #ty = <#ty>::from_ctx(&ctx); },
}
}
fn extract_response_ty(ret: &ReturnType) -> Option<Type> {
let ty = match ret {
ReturnType::Default => return None,
ReturnType::Type(_, ty) => &**ty,
};
inner_payload_ty(ty).cloned()
}
fn inner_payload_ty(ty: &Type) -> Option<&Type> {
let Type::Path(tp) = ty else { return None };
let seg = tp.path.segments.last()?;
match seg.ident.to_string().as_str() {
"Json" | "Created" | "Accepted" => first_generic(&seg.arguments),
"Result" => {
let ok = first_generic(&seg.arguments)?;
inner_payload_ty(ok)
}
"NoContent" => None,
_ => None,
}
}
fn first_generic(args: &PathArguments) -> Option<&Type> {
let PathArguments::AngleBracketed(ab) = args else {
return None;
};
ab.args.iter().find_map(|a| match a {
GenericArgument::Type(t) => Some(t),
_ => None,
})
}
fn collect_doc_comments(attrs: &[Attribute]) -> String {
let mut out = String::new();
for a in attrs {
if !a.path().is_ident("doc") {
continue;
}
if let Meta::NameValue(nv) = &a.meta {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = &nv.value
{
let line = s.value();
if !out.is_empty() {
out.push('\n');
}
out.push_str(line.trim_start());
}
}
}
out
}
struct GatewayArgs {
path: LitStr,
#[allow(dead_code)]
tags: Vec<LitStr>,
}
impl Parse for GatewayArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path: LitStr = input.parse()?;
let mut tags: Vec<LitStr> = vec![];
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
if !input.is_empty() {
let key: Ident = input.parse()?;
if key != "tags" {
return Err(syn::Error::new(key.span(), "expected `tags(...)`"));
}
let content;
syn::parenthesized!(content in input);
let list: Punctuated<LitStr, Token![,]> =
content.parse_terminated(|s| s.parse::<LitStr>(), Token![,])?;
tags.extend(list);
}
}
Ok(Self { path, tags })
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Gateway(attr: TokenStream, item: TokenStream) -> TokenStream {
match syn::parse::<ItemImpl>(item) {
Ok(imp) => gateway_on_impl(attr, imp),
Err(_) => syn::Error::new(
Span::call_site(),
"#[Gateway(\"/path\")] must be placed on the gateway's `impl` block \
(the struct uses #[Injectable]; lifecycle goes in `impl ArclyGateway`)",
)
.to_compile_error()
.into(),
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Subscribe(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
fn gateway_on_impl(attr: TokenStream, mut imp: ItemImpl) -> TokenStream {
let GatewayArgs { path, .. } = parse_macro_input!(attr as GatewayArgs);
let self_ty = (*imp.self_ty).clone();
let name = match &self_ty {
Type::Path(tp) => tp
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default(),
_ => String::new(),
};
let mut dispatch_inserts: Vec<TokenStream2> = Vec::new();
let mut errors: Vec<syn::Error> = Vec::new();
for item in imp.items.iter_mut() {
let ImplItem::Fn(m) = item else { continue };
let sub_idx = m.attrs.iter().position(|a| {
a.path()
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default()
== "Subscribe"
});
let Some(idx) = sub_idx else { continue };
let event: LitStr = match m.attrs[idx].parse_args() {
Ok(e) => e,
Err(e) => {
errors.push(e);
continue;
}
};
m.attrs.remove(idx);
let fn_name = m.sig.ident.clone();
match build_subscribe_insert(&self_ty, m, &event, &fn_name) {
Ok(ts) => dispatch_inserts.push(ts),
Err(e) => errors.push(e),
}
}
if let Some(err) = errors.into_iter().reduce(|mut a, b| {
a.combine(b);
a
}) {
return err.to_compile_error().into();
}
let path_lit = LitStr::new(&path.value(), Span::call_site());
let name_lit = LitStr::new(&name, Span::call_site());
let build_ident = format_ident!("__arcly_build_gateway_{}", name.to_uppercase());
let desc_ident = format_ident!("__ARCLY_GATEWAY_{}", name.to_uppercase());
quote! {
#imp
#[doc(hidden)]
#[allow(non_snake_case)]
fn #build_ident(__container: &'static ::arcly_http::__macro_support::FrozenDiContainer)
-> &'static ::arcly_http::__macro_support::GatewayRuntime
{
let __gw: &'static #self_ty = ::std::boxed::Box::leak(::std::boxed::Box::new(
<#self_ty>::__arcly_build(&__container.resolver())
));
let mut __dispatch: ::std::collections::HashMap<
&'static str,
::arcly_http::__macro_support::MessageHandler,
> = ::std::collections::HashMap::new();
#( #dispatch_inserts )*
::std::boxed::Box::leak(::std::boxed::Box::new(
::arcly_http::__macro_support::GatewayRuntime {
path: #path_lit,
on_connect: ::std::boxed::Box::new(move |__c: ::arcly_http::__macro_support::WsClient| {
let __gw = __gw;
::arcly_http::futures::FutureExt::boxed(async move {
<#self_ty as ::arcly_http::__macro_support::ArclyGateway>::on_connect(__gw, __c).await
})
}),
on_disconnect: ::std::boxed::Box::new(move |__c: ::arcly_http::__macro_support::WsClient| {
let __gw = __gw;
::arcly_http::futures::FutureExt::boxed(async move {
<#self_ty as ::arcly_http::__macro_support::ArclyGateway>::on_disconnect(__gw, __c).await
})
}),
dispatch: __dispatch,
}
))
}
#[allow(non_upper_case_globals)]
static #desc_ident: ::arcly_http::__macro_support::GatewayDescriptor =
::arcly_http::__macro_support::GatewayDescriptor {
name: #name_lit,
path: #path_lit,
build: #build_ident,
};
::arcly_http::inventory::submit! { &#desc_ident }
}
.into()
}
fn build_subscribe_insert(
self_ty: &Type,
m: &syn::ImplItemFn,
event: &LitStr,
fn_name: &Ident,
) -> syn::Result<TokenStream2> {
let mut extract_stmts: Vec<TokenStream2> = Vec::new();
let mut call_args: Vec<TokenStream2> = Vec::new();
for (i, input) in m.sig.inputs.iter().enumerate() {
let pt = match input {
FnArg::Receiver(_) => continue, FnArg::Typed(pt) => pt,
};
let ty = (*pt.ty).clone();
let var = format_ident!("__sub_arg_{i}");
if type_last_ident_is(&ty, "WsClient") {
extract_stmts.push(
quote! { let #var: ::arcly_http::__macro_support::WsClient = __client.clone(); },
);
call_args.push(quote! { #var });
} else if let Some(inner) = json_inner_ty(&ty) {
extract_stmts.push(quote! {
let #var: ::arcly_http::__macro_support::Json<#inner> = ::arcly_http::__macro_support::Json(
::arcly_http::serde_json::from_str::<#inner>(&__data)
.map_err(|_| ::arcly_http::__macro_support::Error::BadRequest("invalid websocket payload"))?
);
});
call_args.push(quote! { #var });
} else {
return Err(syn::Error::new(
pt.span(),
"#[Subscribe] handler params must be `WsClient` or `Json<T>`",
));
}
}
Ok(quote! {
{
let __gw = __gw;
let __handler: ::arcly_http::__macro_support::MessageHandler = ::std::sync::Arc::new(
move |__client: ::arcly_http::__macro_support::WsClient, __data: ::std::sync::Arc<str>| {
let __gw = __gw;
::arcly_http::futures::FutureExt::boxed(async move {
#( #extract_stmts )*
<#self_ty>::#fn_name(__gw, #( #call_args ),*).await
})
}
);
__dispatch.insert(#event, __handler);
}
})
}
fn type_last_ident_is(ty: &Type, name: &str) -> bool {
matches!(ty, Type::Path(tp)
if tp.path.segments.last().map(|s| s.ident == name).unwrap_or(false))
}
fn json_inner_ty(ty: &Type) -> Option<Type> {
let Type::Path(tp) = ty else { return None };
let seg = tp.path.segments.last()?;
if seg.ident != "Json" {
return None;
}
first_generic(&seg.arguments).cloned()
}
struct BreakerArgs {
threshold: u32,
cooldown_millis: u64,
}
impl Parse for BreakerArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut threshold: Option<u32> = None;
let mut cooldown_millis: Option<u64> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
let _: Token![=] = input.parse()?;
match key.to_string().as_str() {
"threshold" => {
let n: LitInt = input.parse()?;
threshold = Some(n.base10_parse()?);
}
"cooldown" => {
let s: LitStr = input.parse()?;
cooldown_millis = Some(parse_duration_ms(&s)?);
}
other => {
return Err(syn::Error::new(
key.span(),
format!("unknown circuit_breaker key `{other}`"),
))
}
}
let _ = input.parse::<Token![,]>();
}
Ok(Self {
threshold: threshold
.ok_or_else(|| syn::Error::new(input.span(), "missing `threshold = N`"))?,
cooldown_millis: cooldown_millis
.ok_or_else(|| syn::Error::new(input.span(), "missing `cooldown = \"…\"`"))?,
})
}
}
fn parse_duration_ms(s: &LitStr) -> syn::Result<u64> {
let raw = s.value();
let r = raw.trim();
let (num_s, unit) = match r.rfind(|c: char| c.is_ascii_digit()) {
Some(i) => (&r[..=i], &r[i + 1..]),
None => return Err(syn::Error::new(s.span(), "invalid duration")),
};
let n: u64 = num_s
.parse()
.map_err(|_| syn::Error::new(s.span(), "invalid duration number"))?;
let mult = match unit.trim() {
"ms" => 1,
"s" | "" => 1_000,
"m" => 60_000,
"h" => 3_600_000,
other => {
return Err(syn::Error::new(
s.span(),
format!("unknown duration unit `{other}`"),
))
}
};
Ok(n * mult)
}
#[proc_macro_attribute]
pub fn circuit_breaker(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as BreakerArgs);
let mut f = parse_macro_input!(item as syn::ImplItemFn);
let threshold = args.threshold;
let cooldown_ms = args.cooldown_millis;
let breaker_name = format_ident!("__BREAKER_{}", f.sig.ident.to_string().to_uppercase());
let original_body = f.block.clone();
let new_block: Block = parse_quote! {{
static #breaker_name: ::arcly_http::__macro_support::CircuitBreaker =
::arcly_http::__macro_support::CircuitBreaker::const_new(#threshold, #cooldown_ms);
match #breaker_name.execute(|| async move #original_body).await {
::core::result::Result::Ok(inner) => inner,
::core::result::Result::Err(_open) => ::core::result::Result::Err(
<_ as ::core::convert::From<::arcly_http::__macro_support::BreakerOpen>>::from(_open),
),
}
}};
f.block = new_block;
quote! { #f }.into()
}
struct EncryptFieldsArgs {
key: LitStr,
fields: Vec<LitStr>,
}
impl Parse for EncryptFieldsArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut key: Option<LitStr> = None;
let mut fields: Vec<LitStr> = Vec::new();
while !input.is_empty() {
let ident: Ident = input.parse()?;
match ident.to_string().as_str() {
"key" => {
input.parse::<Token![=]>()?;
key = Some(input.parse()?);
}
"fields" => {
let inner;
syn::parenthesized!(inner in input);
let lits: Punctuated<LitStr, Token![,]> =
inner.parse_terminated(|p: ParseStream| p.parse::<LitStr>(), Token![,])?;
fields.extend(lits);
}
other => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown EncryptFields argument `{other}` (expected `key` or `fields`)"
),
))
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
let key = key.ok_or_else(|| {
syn::Error::new(Span::call_site(), "EncryptFields requires `key = \"...\"`")
})?;
if fields.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"EncryptFields requires at least one entry in `fields(...)`",
));
}
Ok(Self { key, fields })
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn EncryptFields(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as EncryptFieldsArgs);
let st = parse_macro_input!(item as ItemStruct);
let name = &st.ident;
let (impl_g, ty_g, where_c) = st.generics.split_for_impl();
let key = &args.key;
let fields = &args.fields;
quote! {
#st
impl #impl_g ::arcly_http::__macro_support::EncryptRecord for #name #ty_g #where_c {
const ENCRYPT_FIELDS: &'static [&'static str] = &[ #( #fields ),* ];
const KEY_ID: &'static str = #key;
}
}
.into()
}