use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{
Token,
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
spanned::Spanned,
};
mod command;
mod item;
mod message_events;
mod partial_matches;
mod query;
mod relationship;
mod report;
mod saga;
mod setter;
mod view;
pub(crate) fn is_myko_crate() -> bool {
std::env::var("CARGO_PKG_NAME")
.map(|name| name == "myko")
.unwrap_or(false)
}
pub(crate) fn myko_path() -> syn::Path {
if is_myko_crate() {
syn::Path::from(syn::Ident::new("crate", Span::call_site()))
} else {
syn::Path::from(syn::Ident::new("myko", Span::call_site()))
}
}
pub(crate) struct DeriveCtx {
pub krate: syn::Path,
pub serde_path: proc_macro2::TokenStream,
pub serde_crate_attr: Option<String>,
pub partially_path: proc_macro2::TokenStream,
pub partially_crate_attr: Option<String>,
}
impl DeriveCtx {
pub fn new() -> Self {
let krate = myko_path();
if is_myko_crate() {
Self {
krate,
serde_path: quote!(serde),
serde_crate_attr: None,
partially_path: quote!(partially),
partially_crate_attr: None,
}
} else {
let serde_crate_str = "myko::serde".to_string();
let partially_crate_str = "myko::partially".to_string();
Self {
krate,
serde_path: quote!(myko::serde),
serde_crate_attr: Some(serde_crate_str),
partially_path: quote!(myko::partially),
partially_crate_attr: Some(partially_crate_str),
}
}
}
pub fn serde_attr(&self, rest: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
match &self.serde_crate_attr {
Some(crate_str) => {
if rest.is_empty() {
quote!(#[serde(crate = #crate_str)])
} else {
quote!(#[serde(crate = #crate_str, #rest)])
}
}
None => {
if rest.is_empty() {
quote!()
} else {
quote!(#[serde(#rest)])
}
}
}
}
}
pub(crate) fn take_manual_cache_key_attr(input_struct: &mut syn::ItemStruct) -> bool {
let mut found = take_marker_attr(input_struct, "myko_manual_cache_key");
input_struct.attrs.retain(|attr| {
let is_doc_marker = attr.path().is_ident("doc")
&& attr
.meta
.require_name_value()
.ok()
.and_then(|nv| match &nv.value {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => Some(s.value() == "__myko_manual_cache_key"),
_ => None,
},
_ => None,
})
.unwrap_or(false);
found |= is_doc_marker;
!is_doc_marker
});
found
}
pub(crate) fn take_non_hash_cache_key_attr(input_struct: &mut syn::ItemStruct) -> bool {
let mut found = take_marker_attr(input_struct, "myko_non_hash_cache_key");
input_struct.attrs.retain(|attr| {
let is_doc_marker = attr.path().is_ident("doc")
&& attr
.meta
.require_name_value()
.ok()
.and_then(|nv| match &nv.value {
syn::Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => Some(s.value() == "__myko_non_hash_cache_key"),
_ => None,
},
_ => None,
})
.unwrap_or(false);
found |= is_doc_marker;
!is_doc_marker
});
found
}
fn take_marker_attr(input_struct: &mut syn::ItemStruct, attr_name: &str) -> bool {
let mut found = false;
input_struct.attrs.retain(|attr| {
let matches = attr.path().is_ident(attr_name);
found |= matches;
!matches
});
found
}
#[proc_macro_derive(TsNoop, attributes(ts))]
pub fn ts_noop_derive(_input: TokenStream) -> TokenStream {
TokenStream::new()
}
pub(crate) fn gate_ts_attrs(_attrs: &mut [syn::Attribute]) {}
#[proc_macro_attribute]
pub fn myko_manual_cache_key(_attr: TokenStream, input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as syn::ItemStruct);
quote! {
#[doc = "__myko_manual_cache_key"]
#item
}
.into()
}
#[proc_macro_attribute]
pub fn myko_non_hash_cache_key(_attr: TokenStream, input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as syn::ItemStruct);
quote! {
#[doc = "__myko_non_hash_cache_key"]
#item
}
.into()
}
#[proc_macro_derive(PartialMatches)]
pub fn derive_partial_matches(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
partial_matches::derive_partial_matches_impl(input).into()
}
#[proc_macro_attribute]
pub fn myko_item(attr: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as item::ItemArgs);
let input = parse_macro_input!(input as syn::ItemStruct);
item::myko_item_impl(args, input).into()
}
#[proc_macro_attribute]
pub fn myko_query(attr: TokenStream, input: TokenStream) -> TokenStream {
let query_item_type = parse_macro_input!(attr as syn::Path);
let input = parse_macro_input!(input as syn::ItemStruct);
query::myko_query_impl(query_item_type, input).into()
}
#[proc_macro_attribute]
pub fn myko_view(attr: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::ItemStruct);
if attr.is_empty() {
return syn::Error::new(
input.ident.span(),
"#[myko_view] requires an item type: #[myko_view(ViewItemType)]",
)
.to_compile_error()
.into();
}
let args = parse_macro_input!(attr as view::ViewArgs);
view::myko_view_impl(args, input).into()
}
#[proc_macro_attribute]
pub fn myko_view_item(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::ItemStruct);
view::myko_view_item_impl(input).into()
}
#[proc_macro_attribute]
pub fn myko_report(attr: TokenStream, input: TokenStream) -> TokenStream {
let report_output_type = parse_macro_input!(attr as syn::Path);
let input = parse_macro_input!(input as syn::ItemStruct);
report::myko_report_impl(report_output_type, input).into()
}
#[proc_macro_attribute]
pub fn myko_command(attr: TokenStream, input: TokenStream) -> TokenStream {
let options = if attr.is_empty() {
command::CommandOptions {
result_type: None,
custom_serialize: false,
}
} else {
parse_macro_input!(attr as CommandArgs).into()
};
let input = parse_macro_input!(input as syn::ItemStruct);
command::myko_command_impl(options, input).into()
}
struct CommandArgs {
result_type: Option<syn::Path>,
custom_serialize: bool,
}
impl From<CommandArgs> for command::CommandOptions {
fn from(value: CommandArgs) -> Self {
Self {
result_type: value.result_type,
custom_serialize: value.custom_serialize,
}
}
}
impl Parse for CommandArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args = Punctuated::<syn::Path, Token![,]>::parse_terminated(input)?;
let mut result_type = None;
let mut custom_serialize = false;
for path in args {
if path.is_ident("custom_serialize") {
if custom_serialize {
return Err(syn::Error::new(
path.span(),
"duplicate custom_serialize flag",
));
}
custom_serialize = true;
continue;
}
if result_type.is_some() {
return Err(syn::Error::new(
path.span(),
"expected at most one result type",
));
}
result_type = Some(path);
}
Ok(Self {
result_type,
custom_serialize,
})
}
}
#[proc_macro_derive(MessageEvents)]
pub fn derive_message_events(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
message_events::derive_message_events_impl(input).into()
}
#[proc_macro_attribute]
pub fn myko_saga(attr: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::ItemStruct);
saga::myko_saga_impl(attr.into(), input).into()
}
#[proc_macro_attribute]
pub fn myko_report_output(_attr: TokenStream, input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as syn::ItemStruct);
let name = &input.ident;
let ctx = DeriveCtx::new();
let krate = &ctx.krate;
let serde_path = &ctx.serde_path;
let serde_rename_attr = ctx.serde_attr(quote!(rename_all = "camelCase"));
gate_ts_attrs(&mut input.attrs);
for field in input.fields.iter_mut() {
gate_ts_attrs(&mut field.attrs);
}
let expanded = quote! {
#[derive(Debug, Clone, PartialEq, #serde_path::Serialize, #serde_path::Deserialize, #krate::TS)]
#serde_rename_attr
#input
#krate::register_ts_export!(#name);
};
expanded.into()
}
#[proc_macro_attribute]
pub fn myko_subtype(attr: TokenStream, input: TokenStream) -> TokenStream {
let extra_derives = parse_macro_input!(attr as SubtypeArgs).extra_derives;
let item: syn::Item = parse_macro_input!(input as syn::Item);
myko_subtype_expand(extra_derives, item).into()
}
struct SubtypeArgs {
extra_derives: Vec<syn::Path>,
}
impl syn::parse::Parse for SubtypeArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(Self {
extra_derives: Vec::new(),
});
}
let meta: syn::Meta = input.parse()?;
if let syn::Meta::List(list) = meta {
if list.path.is_ident("derive") {
let punct: syn::punctuated::Punctuated<syn::Path, syn::Token![,]> =
list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
return Ok(Self {
extra_derives: punct.into_iter().collect(),
});
}
return Err(syn::Error::new_spanned(
&list.path,
"expected `derive(...)` argument",
));
}
Err(syn::Error::new_spanned(
meta,
"expected `derive(...)` argument",
))
}
}
fn myko_subtype_expand(
extra_derives: Vec<syn::Path>,
mut item: syn::Item,
) -> proc_macro2::TokenStream {
let ctx = DeriveCtx::new();
let krate = &ctx.krate;
let serde_path = &ctx.serde_path;
let (name, has_rename_all, is_struct) = match &mut item {
syn::Item::Struct(s) => {
gate_ts_attrs(&mut s.attrs);
for field in s.fields.iter_mut() {
gate_ts_attrs(&mut field.attrs);
}
(s.ident.clone(), attrs_have_serde_rename_all(&s.attrs), true)
}
syn::Item::Enum(e) => {
gate_ts_attrs(&mut e.attrs);
for variant in e.variants.iter_mut() {
gate_ts_attrs(&mut variant.attrs);
for field in variant.fields.iter_mut() {
gate_ts_attrs(&mut field.attrs);
}
}
(
e.ident.clone(),
attrs_have_serde_rename_all(&e.attrs),
false,
)
}
other => {
return syn::Error::new_spanned(
other,
"#[myko_subtype] only supports `struct` and `enum` items",
)
.to_compile_error();
}
};
let extra_derive_tokens = if extra_derives.is_empty() {
quote!()
} else {
quote!(, #(#extra_derives),*)
};
let serde_rename_attr = if is_struct && !has_rename_all {
ctx.serde_attr(quote!(rename_all = "camelCase"))
} else {
quote!()
};
quote! {
#[derive(Debug, Clone, PartialEq, #serde_path::Serialize, #serde_path::Deserialize, #krate::TS #extra_derive_tokens)]
#[ts(export)]
#serde_rename_attr
#item
#krate::register_ts_export!(#name);
}
}
fn attrs_have_serde_rename_all(attrs: &[syn::Attribute]) -> bool {
use quote::ToTokens;
attrs.iter().any(|a| {
a.path().is_ident("serde") && a.to_token_stream().to_string().contains("rename_all")
})
}