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#[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
45fn 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
65fn 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
81fn 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
101fn 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
115fn 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}