Skip to main content

eventcore_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::quote;
4use syn::{
5    Data, DeriveInput, Error, Field, Fields, Meta, Path, Type, parse_macro_input,
6    punctuated::Punctuated,
7};
8
9/// Macro entry point that generates `CommandStreams` implementations for
10/// structs whose `StreamId` fields are annotated with `#[stream]`.
11#[proc_macro_derive(Command, attributes(stream))]
12pub fn command(input: TokenStream) -> TokenStream {
13    let input = parse_macro_input!(input as DeriveInput);
14
15    match expand_command(&input) {
16        Ok(tokens) => tokens.into(),
17        Err(error) => error.to_compile_error().into(),
18    }
19}
20
21fn expand_command(input: &DeriveInput) -> syn::Result<TokenStream2> {
22    let ident = &input.ident;
23    let fields = extract_named_fields(input)?;
24    let stream_exprs = collect_stream_fields(fields)?;
25
26    if stream_exprs.is_empty() {
27        return Err(Error::new_spanned(
28            ident,
29            "EventCore: #[derive(Command)] requires at least one #[stream] StreamId field; add #[stream] to your StreamId member",
30        ));
31    }
32
33    Ok(quote! {
34        impl ::eventcore::CommandStreams for #ident {
35            fn stream_declarations(&self) -> ::eventcore::StreamDeclarations {
36                ::eventcore::StreamDeclarations::try_from_streams(vec![
37                    #( #stream_exprs ),*
38                ])
39                .expect("valid stream declarations generated by #[derive(Command)]")
40            }
41        }
42    })
43}
44
45/// Ensures the derive target is a struct with named fields and returns them for
46/// further validation.
47fn extract_named_fields(input: &DeriveInput) -> syn::Result<&Punctuated<Field, syn::token::Comma>> {
48    let Data::Struct(data_struct) = &input.data else {
49        return Err(Error::new_spanned(
50            &input.ident,
51            "EventCore: #[derive(Command)] can only be applied to structs, not enums or unions",
52        ));
53    };
54
55    let Fields::Named(fields) = &data_struct.fields else {
56        return Err(Error::new_spanned(
57            &input.ident,
58            "EventCore: #[derive(Command)] requires named fields; tuple structs are not supported",
59        ));
60    };
61
62    Ok(&fields.named)
63}
64
65/// Walks the struct fields, validating #[stream] usages and producing the
66/// expressions used in the generated CommandStreams impl.
67fn collect_stream_fields(
68    fields: &Punctuated<Field, syn::token::Comma>,
69) -> syn::Result<Vec<TokenStream2>> {
70    let mut stream_exprs = Vec::new();
71
72    for field in fields {
73        if has_stream_marker(field)? {
74            stream_exprs.push(stream_expression(field)?);
75        }
76    }
77
78    Ok(stream_exprs)
79}
80
81/// Returns true when the field carries a valid #[stream] attribute.
82fn has_stream_marker(field: &Field) -> syn::Result<bool> {
83    let mut marked = false;
84
85    for attr in &field.attrs {
86        if attr.path().is_ident("stream") {
87            if !matches!(&attr.meta, Meta::Path(_)) {
88                return Err(Error::new_spanned(
89                    attr,
90                    "EventCore: #[stream] does not accept parameters",
91                ));
92            }
93
94            marked = true;
95        }
96    }
97
98    Ok(marked)
99}
100
101/// Produces the `self.field.clone()` expression for a validated stream field.
102fn stream_expression(field: &Field) -> syn::Result<TokenStream2> {
103    let Some(field_ident) = &field.ident else {
104        return Err(Error::new_spanned(
105            field,
106            "EventCore: #[derive(Command)] requires named fields",
107        ));
108    };
109
110    ensure_stream_id_type(field)?;
111
112    Ok(quote! { self.#field_ident.clone() })
113}
114
115/// Confirms the field type resolves to StreamId (allowing qualified paths).
116fn ensure_stream_id_type(field: &Field) -> syn::Result<()> {
117    match &field.ty {
118        Type::Path(type_path) if is_eventcore_stream_id(&type_path.path) => Ok(()),
119        _ => Err(Error::new_spanned(
120            field,
121            "EventCore: #[stream] fields must have type eventcore::StreamId",
122        )),
123    }
124}
125
126fn is_eventcore_stream_id(path: &Path) -> bool {
127    let Some(last) = path.segments.last() else {
128        return false;
129    };
130
131    if last.ident != "StreamId" {
132        return false;
133    }
134
135    path.segments
136        .iter()
137        .take(path.segments.len().saturating_sub(1))
138        .all(|segment| {
139            matches!(
140                segment.ident.to_string().as_str(),
141                "eventcore" | "crate" | "self" | "super"
142            )
143        })
144}