#![forbid(
dead_code,
invalid_value,
overflowing_literals,
unconditional_recursion,
unreachable_pub,
unused_allocation,
unsafe_code
)]
#![deny(
bad_style,
clippy::allow_attributes,
deprecated,
meta_variable_misuse,
non_ascii_idents,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
rust_2021_compatibility,
trivial_casts,
trivial_numeric_casts,
unreachable_code,
unused_assignments,
unused_attributes,
unused_extern_crates,
unused_imports,
unused_must_use,
unused_mut,
unused_parens,
unused_qualifications,
unused_results,
unused_variables
)]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
Data, DeriveInput, Error, Field, Fields, Meta, Path, Type, parse_macro_input,
punctuated::Punctuated,
};
#[proc_macro_derive(Command, attributes(stream))]
pub fn command(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_command(&input) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn expand_command(input: &DeriveInput) -> syn::Result<TokenStream2> {
let ident = &input.ident;
let fields = extract_named_fields(input)?;
let stream_exprs = collect_stream_fields(fields)?;
if stream_exprs.is_empty() {
return Err(Error::new_spanned(
ident,
"EventCore: #[derive(Command)] requires at least one #[stream] StreamId field; add #[stream] to your StreamId member",
));
}
Ok(quote! {
impl ::eventcore::CommandStreams for #ident {
fn stream_declarations(&self) -> ::eventcore::StreamDeclarations {
::eventcore::StreamDeclarations::try_from_streams(vec![
#( #stream_exprs ),*
])
.expect("valid stream declarations generated by #[derive(Command)]")
}
}
})
}
fn extract_named_fields(input: &DeriveInput) -> syn::Result<&Punctuated<Field, syn::token::Comma>> {
let Data::Struct(data_struct) = &input.data else {
return Err(Error::new_spanned(
&input.ident,
"EventCore: #[derive(Command)] only supports structs with named fields",
));
};
let Fields::Named(fields) = &data_struct.fields else {
return Err(Error::new_spanned(
&input.ident,
"EventCore: #[derive(Command)] only supports structs with named fields",
));
};
Ok(&fields.named)
}
fn collect_stream_fields(
fields: &Punctuated<Field, syn::token::Comma>,
) -> syn::Result<Vec<TokenStream2>> {
let mut stream_exprs = Vec::new();
for field in fields {
if has_stream_marker(field)? {
stream_exprs.push(stream_expression(field)?);
}
}
Ok(stream_exprs)
}
fn has_stream_marker(field: &Field) -> syn::Result<bool> {
let mut marked = false;
for attr in &field.attrs {
if attr.path().is_ident("stream") {
if !matches!(&attr.meta, Meta::Path(_)) {
return Err(Error::new_spanned(
attr,
"EventCore: #[stream] does not accept parameters",
));
}
marked = true;
}
}
Ok(marked)
}
fn stream_expression(field: &Field) -> syn::Result<TokenStream2> {
let Some(field_ident) = &field.ident else {
return Err(Error::new_spanned(
field,
"EventCore: #[derive(Command)] only supports structs with named fields",
));
};
ensure_stream_id_type(field)?;
Ok(quote! { self.#field_ident.clone() })
}
fn ensure_stream_id_type(field: &Field) -> syn::Result<()> {
match &field.ty {
Type::Path(type_path) if is_eventcore_stream_id(&type_path.path) => Ok(()),
_ => Err(Error::new_spanned(
field,
"EventCore: #[stream] fields must have type eventcore::StreamId",
)),
}
}
fn is_eventcore_stream_id(path: &Path) -> bool {
let Some(last) = path.segments.last() else {
return false;
};
if last.ident != "StreamId" {
return false;
}
path.segments
.iter()
.take(path.segments.len().saturating_sub(1))
.all(|segment| {
matches!(
segment.ident.to_string().as_str(),
"eventcore" | "crate" | "self" | "super"
)
})
}