1use proc_macro::TokenStream;
37use proc_macro2::TokenStream as TokenStream2;
38use quote::quote;
39use syn::{
40 Data, DeriveInput, Error, Field, Fields, Meta, Path, Type, parse_macro_input,
41 punctuated::Punctuated,
42};
43
44#[proc_macro_derive(Command, attributes(stream))]
54pub fn command(input: TokenStream) -> TokenStream {
55 let input = parse_macro_input!(input as DeriveInput);
56
57 match expand_command(&input) {
58 Ok(tokens) => tokens.into(),
59 Err(error) => error.to_compile_error().into(),
60 }
61}
62
63fn expand_command(input: &DeriveInput) -> syn::Result<TokenStream2> {
64 let ident = &input.ident;
65 let fields = extract_named_fields(input)?;
66 let stream_exprs = collect_stream_fields(fields)?;
67
68 if stream_exprs.is_empty() {
69 return Err(Error::new_spanned(
70 ident,
71 "EventCore: #[derive(Command)] requires at least one #[stream] StreamId field; add #[stream] to your StreamId member",
72 ));
73 }
74
75 Ok(quote! {
76 impl ::eventcore::CommandStreams for #ident {
77 fn stream_declarations(&self) -> ::eventcore::StreamDeclarations {
78 ::eventcore::StreamDeclarations::try_from_streams(vec![
79 #( #stream_exprs ),*
80 ])
81 .expect("valid stream declarations generated by #[derive(Command)]")
82 }
83 }
84 })
85}
86
87fn extract_named_fields(input: &DeriveInput) -> syn::Result<&Punctuated<Field, syn::token::Comma>> {
90 let Data::Struct(data_struct) = &input.data else {
91 return Err(Error::new_spanned(
92 &input.ident,
93 "EventCore: #[derive(Command)] can only be applied to structs, not enums or unions",
94 ));
95 };
96
97 let Fields::Named(fields) = &data_struct.fields else {
98 return Err(Error::new_spanned(
99 &input.ident,
100 "EventCore: #[derive(Command)] requires named fields; tuple structs are not supported",
101 ));
102 };
103
104 Ok(&fields.named)
105}
106
107fn collect_stream_fields(
110 fields: &Punctuated<Field, syn::token::Comma>,
111) -> syn::Result<Vec<TokenStream2>> {
112 let mut stream_exprs = Vec::new();
113
114 for field in fields {
115 if has_stream_marker(field)? {
116 stream_exprs.push(stream_expression(field)?);
117 }
118 }
119
120 Ok(stream_exprs)
121}
122
123fn has_stream_marker(field: &Field) -> syn::Result<bool> {
125 let mut marked = false;
126
127 for attr in &field.attrs {
128 if attr.path().is_ident("stream") {
129 if !matches!(&attr.meta, Meta::Path(_)) {
130 return Err(Error::new_spanned(
131 attr,
132 "EventCore: #[stream] does not accept parameters",
133 ));
134 }
135
136 marked = true;
137 }
138 }
139
140 Ok(marked)
141}
142
143fn stream_expression(field: &Field) -> syn::Result<TokenStream2> {
145 let Some(field_ident) = &field.ident else {
146 return Err(Error::new_spanned(
147 field,
148 "EventCore: #[derive(Command)] requires named fields",
149 ));
150 };
151
152 ensure_stream_id_type(field)?;
153
154 Ok(quote! { self.#field_ident.clone() })
155}
156
157fn ensure_stream_id_type(field: &Field) -> syn::Result<()> {
159 match &field.ty {
160 Type::Path(type_path) if is_eventcore_stream_id(&type_path.path) => Ok(()),
161 _ => Err(Error::new_spanned(
162 field,
163 "EventCore: #[stream] fields must have type eventcore::StreamId",
164 )),
165 }
166}
167
168fn is_eventcore_stream_id(path: &Path) -> bool {
169 let Some(last) = path.segments.last() else {
170 return false;
171 };
172
173 if last.ident != "StreamId" {
174 return false;
175 }
176
177 path.segments
178 .iter()
179 .take(path.segments.len().saturating_sub(1))
180 .all(|segment| {
181 matches!(
182 segment.ident.to_string().as_str(),
183 "eventcore" | "crate" | "self" | "super"
184 )
185 })
186}