#![deny(clippy::disallowed_methods)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::undocumented_unsafe_blocks)]
#![deny(unsafe_op_in_unsafe_fn)]
#![allow(non_camel_case_types, nonstandard_style)]
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{
Data::Struct,
DataStruct, DeriveInput, Fields,
Fields::Named,
FieldsNamed, Ident, ItemFn, LitStr, Token,
parse::{Parse, ParseStream},
parse_macro_input
};
mod util;
use util::*;
#[proc_macro_attribute]
pub fn run_app(_: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let fn_block = match input_fn.block.stmts.first() {
Some(stmt) => stmt,
None => {
return syn::Error::new_spanned(
&input_fn.sig.ident,
"run_app requires at least one statement in the function body"
)
.to_compile_error()
.into();
}
};
quote! {
async fn run<Context: Clone + Send + 'static>(
listener: TcpListener,
db_pool: PgPool,
graph_pool: neo4rs::Graph,
base_url: String,
hmac_secret: SecretString,
redis_uri: SecretString,
custom_context: Context,
allowed_origin: String
) -> Result<Server, anyhow::Error> {
let redis_store = app::redis_session(redis_uri).await?;
let server = HttpServer::new(move || {
ActixWebApp::new()
.wrap(TracingLogger::default())
.wrap(app::session_middleware(
hmac_secret.clone(),
redis_store.clone(),
))
.wrap(app::cors_middleware(allowed_origin.clone()))
.app_data(web::Data::new(ApplicationBaseUrl(base_url.clone())))
.app_data(web::Data::new(HmacSecret(hmac_secret.clone())))
.app_data(web::Data::new(db_pool.clone()))
.app_data(web::Data::new(graph_pool.clone()))
.app_data(web::Data::new(custom_context.clone()))
.service(mae::health::health)
.service(mae::health::health_pg)
.service(mae::health::health_neo)
.#fn_block
})
.listen(listener)?
.run();
Ok(server)
}
}
.into()
}
#[doc(hidden)]
struct Args {
ctx: Ident,
schema: LitStr,
_comma: Token![,]
}
#[doc(hidden)]
impl Parse for Args {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self { ctx: input.parse()?, _comma: input.parse()?, schema: input.parse()? })
}
}
#[proc_macro_attribute]
pub fn schema(args: TokenStream, input: TokenStream) -> TokenStream {
let Args { ctx, schema, .. } = parse_macro_input!(args as Args);
let ast = parse_macro_input!(input as DeriveInput);
let repo_ident = &ast.ident;
let repo_attrs = &ast.attrs;
let fields = match ast.data {
Struct(DataStruct { fields: Named(FieldsNamed { ref named, .. }), .. }) => named,
_ => {
return syn::Error::new_spanned(
repo_ident,
"schema only works for structs with named fields"
)
.to_compile_error()
.into();
}
};
let params = fields.iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
let attrs = &f.attrs;
quote! {
#(#attrs)*
pub #name: #ty
}
});
let repo = quote! {
#(#repo_attrs)*
#[derive(mae_macros::MaeRepo, Debug, sqlx::FromRow, serde::Serialize, serde::Deserialize, Clone)]
pub struct #repo_ident {
#[locked]
pub id: i32,
#[insert_only]
pub sys_client: i32,
pub status: mae::repo::default::DomainStatus,
#(#params,)*
pub comment: Option<String>,
#[sqlx(json)]
pub tags: serde_json::Value,
#[sqlx(json)]
pub sys_detail: serde_json::Value,
#[locked]
pub created_by: i32,
#[locked]
pub updated_by: i32,
#[locked]
pub created_at: chrono::DateTime<chrono::Utc>,
#[locked]
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl mae::repo::__private__::Build<#ctx, InsertRow, UpdateRow, Field, PatchField> for #repo_ident {
fn schema() -> String {
#schema.to_string()
}
}
};
repo.into()
}
#[proc_macro_attribute]
pub fn schema_root(args: TokenStream, input: TokenStream) -> TokenStream {
let Args { ctx, schema, .. } = parse_macro_input!(args as Args);
let ast = parse_macro_input!(input as DeriveInput);
let repo_ident = &ast.ident;
let repo_attrs = &ast.attrs;
let fields = match ast.data {
Struct(DataStruct { fields: Named(FieldsNamed { ref named, .. }), .. }) => named,
_ => {
return syn::Error::new_spanned(
repo_ident,
"schema_root only works for structs with named fields"
)
.to_compile_error()
.into();
}
};
let params = fields.iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
let attrs = &f.attrs;
quote! {
#(#attrs)*
pub #name: #ty
}
});
let repo = quote! {
#(#repo_attrs)*
#[derive(mae_macros::MaeRepo, Debug, sqlx::FromRow, serde::Serialize, serde::Deserialize, Clone)]
pub struct #repo_ident {
#[locked]
pub id: i32,
pub status: mae::repo::default::DomainStatus,
#(#params,)*
pub comment: Option<String>,
#[sqlx(json)]
pub tags: serde_json::Value,
#[sqlx(json)]
pub sys_detail: serde_json::Value,
#[locked]
pub created_by: i32,
#[locked]
pub updated_by: i32,
#[locked]
pub created_at: chrono::DateTime<chrono::Utc>,
#[locked]
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl mae::repo::__private__::Build<#ctx, InsertRow, UpdateRow, Field, PatchField> for #repo_ident {
fn schema() -> String {
#schema.to_string()
}
}
};
repo.into()
}
#[doc(hidden)]
#[proc_macro_derive(MaeRepo, attributes(from_context, insert_only, update_only, locked))]
pub fn derive_mae_repo(item: TokenStream) -> TokenStream {
let ast = parse_macro_input!(item as DeriveInput);
let _ = match &ast.data {
Struct(DataStruct { fields: Fields::Named(fields), .. }) => &fields.named,
_ => {
return syn::Error::new_spanned(
&ast.ident,
"MaeRepo derive expects a struct with named fields"
)
.to_compile_error()
.into();
}
};
let (insert_row, _) = to_row(&ast, vec!["locked".into(), "update_only".into()]);
let (update_row, _) = to_row(&ast, vec!["locked".into(), "insert_only".into()]);
let (repo_typed, _) = to_patches(&ast);
let (repo_variant, _) = to_fields(&ast);
quote! {
#repo_variant
#insert_row
#update_row
#repo_typed
}
.into()
}
struct MaeTestArgs {
docker: bool,
teardown: Option<syn::ExprPath>
}
impl Parse for MaeTestArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut docker = false;
let mut teardown = None;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
match ident.to_string().as_str() {
"docker" => docker = true,
"teardown" => {
input.parse::<Token![=]>()?;
teardown = Some(input.parse::<syn::ExprPath>()?);
}
other => {
return Err(syn::Error::new_spanned(
ident,
format!(
"unknown #[mae_test] argument: `{other}`; expected `docker` or `teardown = <path>`"
)
));
}
}
if input.peek(Token![,]) {
let _: Token![,] = input.parse()?;
}
}
Ok(Self { docker, teardown })
}
}
#[proc_macro_attribute]
pub fn mae_test(attr: TokenStream, item: TokenStream) -> TokenStream {
let MaeTestArgs { docker, teardown } = parse_macro_input!(attr as MaeTestArgs);
let mut f = match syn::parse::<syn::ItemFn>(item) {
Ok(f) => f,
Err(_) => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"#[mae_test] can only be applied to a function"
)
.to_compile_error()
.into();
}
};
if !f.sig.inputs.is_empty() {
return syn::Error::new_spanned(
&f.sig.inputs,
"#[mae_test] test functions must not take arguments"
)
.to_compile_error()
.into();
}
let orig_block = *f.block;
let body_s = quote::quote!(#orig_block).to_string();
let forbidden = [
".expect", ".unwrap", "assert!", "assert_eq!", "assert_ne!" ];
if forbidden.iter().any(|pat| body_s.contains(pat)) {
return syn::Error::new_spanned(
&orig_block,
"#[mae_test] forbids assert*/unwrap/expect in test bodies; use must::* helpers or return Result and use `?`",
)
.to_compile_error()
.into();
}
let ret_ty: syn::Type = match &f.sig.output {
syn::ReturnType::Default => syn::parse_quote!(()),
syn::ReturnType::Type(_, ty) => (**ty).clone()
};
let docker_gate = if docker {
let early_return: proc_macro2::TokenStream = match &f.sig.output {
syn::ReturnType::Default => quote! { return; },
syn::ReturnType::Type(..) => {
quote! { return ::core::result::Result::Ok(::core::default::Default::default()); }
}
};
quote! {
if ::std::option_env!("MAE_TESTCONTAINERS") != ::core::option::Option::Some("1") {
#early_return
}
}
} else {
quote! {}
};
let teardown_call = match teardown {
Some(ref td_path) => quote! {
let __teardown_result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
__mae_rt.block_on(async move {
#td_path().await;
})
}));
},
None => quote! {
let __teardown_result: ::std::result::Result<(), Box<dyn ::std::any::Any + Send>> = Ok(());
}
};
f.sig.asyncness = None;
f.attrs.insert(0, syn::parse_quote!(#[test]));
*f.block = syn::parse_quote!({
#[allow(clippy::disallowed_methods, clippy::expect_used)]
fn __mae_run_test() -> #ret_ty {
#docker_gate
let __mae_rt = match ::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt,) => rt,
Err(e,) => panic!("failed to build tokio runtime for #[mae_test]: {e}"),
};
let __user_result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
__mae_rt.block_on(async move {
(async move #orig_block).await
})
}));
#teardown_call
match (__user_result, __teardown_result) {
(Ok(__ret), Ok(())) => __ret,
(Err(__panic), Ok(())) => ::std::panic::resume_unwind(__panic),
(Ok(_), Err(__panic)) => ::std::panic::resume_unwind(__panic),
(Err(__panic), Err(_teardown_panic)) => ::std::panic::resume_unwind(__panic),
}
}
__mae_run_test()
});
TokenStream::from(quote::quote!(#f))
}