Skip to main content

galeon_engine_macros/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use quote::{format_ident, quote};
6use syn::spanned::Spanned;
7use syn::{Token, parse::Parser, punctuated::Punctuated};
8
9/// Resolve the `galeon-engine` crate path as used by the consumer.
10/// Handles renames like `engine = { package = "galeon-engine" }`.
11fn engine_crate() -> proc_macro2::TokenStream {
12    match crate_name("galeon-engine").expect("galeon-engine must be in Cargo.toml") {
13        FoundCrate::Itself => quote!(crate),
14        FoundCrate::Name(name) => {
15            let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
16            quote!(#ident)
17        }
18    }
19}
20
21/// Derive macro that implements the `Component` trait for a struct.
22///
23/// This enables the type to be stored in ECS sparse-set component storage,
24/// keyed by `TypeId`.
25#[proc_macro_derive(Component)]
26pub fn derive_component(input: TokenStream) -> TokenStream {
27    let input = syn::parse_macro_input!(input as syn::DeriveInput);
28    let name = &input.ident;
29    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
30
31    let expanded = quote! {
32        impl #impl_generics galeon_engine::component::Component for #name #ty_generics #where_clause {}
33    };
34
35    TokenStream::from(expanded)
36}
37
38// ---------------------------------------------------------------------------
39// Protocol attribute macros
40// ---------------------------------------------------------------------------
41
42/// Validate that the input is a named or unit struct (not enum, union, or tuple struct).
43fn validate_struct(item: &syn::Item) -> Result<&syn::ItemStruct, syn::Error> {
44    match item {
45        syn::Item::Struct(s) => {
46            if let syn::Fields::Unnamed(_) = &s.fields {
47                Err(syn::Error::new(
48                    s.fields.span(),
49                    "galeon protocol macros do not support tuple structs",
50                ))
51            } else {
52                Ok(s)
53            }
54        }
55        syn::Item::Enum(e) => Err(syn::Error::new(
56            e.enum_token.span(),
57            "galeon protocol macros do not support enums",
58        )),
59        _ => Err(syn::Error::new(
60            proc_macro2::Span::call_site(),
61            "galeon protocol macros can only be applied to structs",
62        )),
63    }
64}
65
66/// Extract the first `#[doc = "..."]` attribute value as a doc string.
67fn extract_doc(attrs: &[syn::Attribute]) -> String {
68    for attr in attrs {
69        if attr.path().is_ident("doc")
70            && let syn::Meta::NameValue(nv) = &attr.meta
71            && let syn::Expr::Lit(syn::ExprLit {
72                lit: syn::Lit::Str(s),
73                ..
74            }) = &nv.value
75        {
76            return s.value().trim().to_string();
77        }
78    }
79    String::new()
80}
81
82/// Convert a type to its string representation for manifest field metadata.
83///
84/// Collapses runs of whitespace to single spaces but preserves them so
85/// composite types like `Vec < UnitView >` render as `Vec<UnitView>`.
86fn type_to_string(ty: &syn::Type) -> String {
87    let raw = quote!(#ty).to_string();
88    raw.replace(" < ", "<")
89        .replace("< ", "<")
90        .replace(" >", ">")
91        .replace(" ,", ",")
92        .replace(" ::", "::")
93        .replace(":: ", "::")
94}
95
96/// Parse optional `surface = "..."` or `surfaces = ["...", "..."]` arguments.
97fn parse_surfaces(attr: TokenStream) -> Result<Vec<String>, syn::Error> {
98    if attr.is_empty() {
99        return Ok(Vec::new());
100    }
101
102    let parser = Punctuated::<syn::MetaNameValue, Token![,]>::parse_terminated;
103    let args = parser.parse(attr)?;
104    let mut surfaces = Vec::new();
105    let mut saw_surface = false;
106    let mut saw_surfaces = false;
107
108    for arg in args {
109        if arg.path.is_ident("surface") {
110            if saw_surfaces {
111                return Err(syn::Error::new(
112                    arg.path.span(),
113                    "use either `surface = \"...\"` or `surfaces = [..]`, not both",
114                ));
115            }
116            if saw_surface {
117                return Err(syn::Error::new(
118                    arg.path.span(),
119                    "`surface` may only be specified once",
120                ));
121            }
122            let syn::Expr::Lit(syn::ExprLit {
123                lit: syn::Lit::Str(surface),
124                ..
125            }) = arg.value
126            else {
127                return Err(syn::Error::new(
128                    arg.path.span(),
129                    "`surface` must be a string literal",
130                ));
131            };
132            let val = surface.value();
133            if val.is_empty() {
134                return Err(syn::Error::new(
135                    surface.span(),
136                    "surface name must not be empty",
137                ));
138            }
139            saw_surface = true;
140            surfaces.push(val);
141            continue;
142        }
143
144        if arg.path.is_ident("surfaces") {
145            if saw_surface {
146                return Err(syn::Error::new(
147                    arg.path.span(),
148                    "use either `surface = \"...\"` or `surfaces = [..]`, not both",
149                ));
150            }
151            if saw_surfaces {
152                return Err(syn::Error::new(
153                    arg.path.span(),
154                    "`surfaces` may only be specified once",
155                ));
156            }
157            let syn::Expr::Array(array) = arg.value else {
158                return Err(syn::Error::new(
159                    arg.path.span(),
160                    "`surfaces` must be an array of string literals",
161                ));
162            };
163            for elem in array.elems {
164                let syn::Expr::Lit(syn::ExprLit {
165                    lit: syn::Lit::Str(surface),
166                    ..
167                }) = elem
168                else {
169                    return Err(syn::Error::new(
170                        arg.path.span(),
171                        "`surfaces` must be an array of string literals",
172                    ));
173                };
174                let surface_val = surface.value();
175                if surface_val.is_empty() {
176                    return Err(syn::Error::new(
177                        surface.span(),
178                        "surface name must not be empty",
179                    ));
180                }
181                surfaces.push(surface_val);
182            }
183            saw_surfaces = true;
184            continue;
185        }
186
187        return Err(syn::Error::new(
188            arg.path.span(),
189            "unsupported protocol attribute argument; expected `surface` or `surfaces`",
190        ));
191    }
192
193    surfaces.sort();
194    surfaces.dedup();
195    Ok(surfaces)
196}
197
198/// Shared implementation for protocol attribute macros.
199fn protocol_attr(
200    attr: TokenStream,
201    input: TokenStream,
202    marker_trait: &str,
203    kind_variant: &str,
204    extra_derives: &[&str],
205) -> TokenStream {
206    let item: syn::Item = match syn::parse(input) {
207        Ok(item) => item,
208        Err(e) => return e.to_compile_error().into(),
209    };
210
211    let s = match validate_struct(&item) {
212        Ok(s) => s,
213        Err(e) => return e.to_compile_error().into(),
214    };
215
216    let name = &s.ident;
217    let name_str = name.to_string();
218    let (impl_generics, ty_generics, where_clause) = s.generics.split_for_impl();
219
220    // Resolve the engine crate path, handling renames.
221    let krate = engine_crate();
222
223    let marker_trait_ident = syn::Ident::new(marker_trait, proc_macro2::Span::call_site());
224    let kind_variant_ident = syn::Ident::new(kind_variant, proc_macro2::Span::call_site());
225    let surfaces = match parse_surfaces(attr) {
226        Ok(surfaces) => surfaces,
227        Err(e) => return e.to_compile_error().into(),
228    };
229    let surface_literals: Vec<syn::LitStr> = surfaces
230        .iter()
231        .map(|surface| syn::LitStr::new(surface, proc_macro2::Span::call_site()))
232        .collect();
233
234    let extra: Vec<syn::Path> = extra_derives
235        .iter()
236        .map(|d| syn::parse_str(d).expect("valid derive path"))
237        .collect();
238
239    // Build serde crate path string for #[serde(crate = "...")] attribute.
240    let serde_crate_path = format!("{}::serde", krate);
241
242    // Extract field metadata for manifest generation.
243    let doc_str = extract_doc(&s.attrs);
244    let field_entries: Vec<proc_macro2::TokenStream> = match &s.fields {
245        syn::Fields::Named(fields) => fields
246            .named
247            .iter()
248            .map(|f| {
249                let fname = f.ident.as_ref().unwrap().to_string();
250                let ftype = type_to_string(&f.ty);
251                quote! {
252                    #krate::manifest::FieldEntry {
253                        name: #fname,
254                        ty: #ftype,
255                    }
256                }
257            })
258            .collect(),
259        _ => Vec::new(), // Unit struct — no fields.
260    };
261
262    let expanded = quote! {
263        #[derive(#krate::serde::Serialize, #krate::serde::Deserialize, #(#extra),*)]
264        #[serde(crate = #serde_crate_path)]
265        #item
266
267        impl #impl_generics #krate::protocol::#marker_trait_ident for #name #ty_generics #where_clause {}
268
269        impl #impl_generics #krate::protocol::ProtocolMeta for #name #ty_generics #where_clause {
270            fn name() -> &'static str {
271                #name_str
272            }
273            fn kind() -> #krate::protocol::ProtocolKind {
274                #krate::protocol::ProtocolKind::#kind_variant_ident
275            }
276        }
277
278        #krate::inventory::submit! {
279            #krate::manifest::ProtocolRegistration {
280                name: #name_str,
281                kind: #krate::protocol::ProtocolKind::#kind_variant_ident,
282                fields: &[#(#field_entries),*],
283                doc: #doc_str,
284                surfaces: &[#(#surface_literals),*],
285            }
286        }
287    };
288
289    expanded.into()
290}
291
292/// Marks a struct as a protocol **command** (state-changing request).
293///
294/// Derives `Serialize`, `Deserialize`, implements [`Command`] and [`ProtocolMeta`].
295///
296/// # Example
297///
298/// ```ignore
299/// #[galeon_engine::command]
300/// pub struct SpawnUnit {
301///     pub unit_id: u64,
302///     pub location_id: u64,
303/// }
304/// ```
305#[proc_macro_attribute]
306pub fn command(attr: TokenStream, input: TokenStream) -> TokenStream {
307    protocol_attr(attr, input, "Command", "Command", &[])
308}
309
310/// Marks a struct as a protocol **query** (read-only request).
311///
312/// Derives `Serialize`, `Deserialize`, implements [`ProtocolQuery`] and [`ProtocolMeta`].
313///
314/// # Example
315///
316/// ```ignore
317/// #[galeon_engine::query]
318/// pub struct GetWorldSnapshot;
319/// ```
320#[proc_macro_attribute]
321pub fn query(attr: TokenStream, input: TokenStream) -> TokenStream {
322    protocol_attr(attr, input, "ProtocolQuery", "Query", &[])
323}
324
325/// Marks a struct as a protocol **event** (authoritative fact).
326///
327/// Derives `Serialize`, `Deserialize`, implements [`Event`] and [`ProtocolMeta`].
328///
329/// # Example
330///
331/// ```ignore
332/// #[galeon_engine::event]
333/// pub struct UnitDestroyed {
334///     pub unit_id: u64,
335///     pub destroyed_at: u64,
336/// }
337/// ```
338#[proc_macro_attribute]
339pub fn event(attr: TokenStream, input: TokenStream) -> TokenStream {
340    protocol_attr(attr, input, "Event", "Event", &[])
341}
342
343/// Marks a struct as a protocol **DTO** (boundary-facing data structure).
344///
345/// Derives `Serialize`, `Deserialize`, `Clone`, implements [`Dto`] and [`ProtocolMeta`].
346///
347/// # Example
348///
349/// ```ignore
350/// #[galeon_engine::dto]
351/// pub struct WorldSnapshot {
352///     pub ships: Vec<UnitView>,
353/// }
354/// ```
355#[proc_macro_attribute]
356pub fn dto(attr: TokenStream, input: TokenStream) -> TokenStream {
357    protocol_attr(attr, input, "Dto", "Dto", &["Clone"])
358}
359
360// ---------------------------------------------------------------------------
361// Handler attribute macro
362// ---------------------------------------------------------------------------
363
364/// Extract `Ok` and `Err` type syntax from a `Result<R, E>` return type.
365fn extract_result_pair(ty: &syn::Type) -> Result<(syn::Type, syn::Type), syn::Error> {
366    if let syn::Type::Path(type_path) = ty
367        && let Some(segment) = type_path.path.segments.last()
368        && segment.ident == "Result"
369        && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
370    {
371        let type_args: Vec<_> = args.args.iter().collect();
372        if type_args.len() == 2 {
373            let ok_type = match type_args[0] {
374                syn::GenericArgument::Type(t) => t.clone(),
375                _ => {
376                    return Err(syn::Error::new_spanned(
377                        type_args[0],
378                        "expected type parameter for Result Ok type",
379                    ));
380                }
381            };
382            let err_type = match type_args[1] {
383                syn::GenericArgument::Type(t) => t.clone(),
384                _ => {
385                    return Err(syn::Error::new_spanned(
386                        type_args[1],
387                        "expected type parameter for Result Err type",
388                    ));
389                }
390            };
391            return Ok((ok_type, err_type));
392        }
393    }
394
395    Err(syn::Error::new_spanned(
396        ty,
397        "#[handler] functions must return `Result<R, E>`",
398    ))
399}
400
401/// Shared implementation for the `#[handler]` attribute macro.
402fn handler_attr(_attr: TokenStream, input: TokenStream) -> TokenStream {
403    let func: syn::ItemFn = match syn::parse(input) {
404        Ok(f) => f,
405        Err(e) => return e.to_compile_error().into(),
406    };
407
408    // --- Validation ---
409
410    // Must be `pub`.
411    if !matches!(func.vis, syn::Visibility::Public(_)) {
412        return syn::Error::new(func.sig.ident.span(), "#[handler] functions must be `pub`")
413            .to_compile_error()
414            .into();
415    }
416
417    // Must not be generic.
418    if !func.sig.generics.params.is_empty() {
419        return syn::Error::new(
420            func.sig.generics.params.first().unwrap().span(),
421            "#[handler] functions must not be generic — each handler must have concrete request/response types",
422        )
423        .to_compile_error()
424        .into();
425    }
426
427    // Must not be `async`.
428    if let Some(async_token) = func.sig.asyncness {
429        return syn::Error::new(
430            async_token.span(),
431            "#[handler] functions must be synchronous (not `async`)",
432        )
433        .to_compile_error()
434        .into();
435    }
436
437    // Must have at least one parameter (the request type).
438    if func.sig.inputs.is_empty() {
439        return syn::Error::new(
440            func.sig.paren_token.span.join(),
441            "#[handler] functions must have at least one parameter (the request type)",
442        )
443        .to_compile_error()
444        .into();
445    }
446
447    // First parameter must not be `self`.
448    let first_param = func.sig.inputs.first().unwrap();
449    let (request_type, request_type_str) = match first_param {
450        syn::FnArg::Typed(pat_type) => (pat_type.ty.clone(), type_to_string(&pat_type.ty)),
451        syn::FnArg::Receiver(r) => {
452            return syn::Error::new(
453                r.self_token.span(),
454                "#[handler] functions must not have `self` as the first parameter",
455            )
456            .to_compile_error()
457            .into();
458        }
459    };
460
461    // Collect extra parameter types (everything after the request param).
462    let extra_param_types: Vec<Box<syn::Type>> = func
463        .sig
464        .inputs
465        .iter()
466        .skip(1)
467        .filter_map(|arg| match arg {
468            syn::FnArg::Typed(pat_type) => Some(pat_type.ty.clone()),
469            _ => None,
470        })
471        .collect();
472
473    // Return type must be `Result<R, E>`.
474    let return_type = match &func.sig.output {
475        syn::ReturnType::Default => {
476            return syn::Error::new(
477                func.sig.paren_token.span.close(),
478                "#[handler] functions must return `Result<R, E>`",
479            )
480            .to_compile_error()
481            .into();
482        }
483        syn::ReturnType::Type(_, ty) => ty,
484    };
485
486    let (response_type, err_type) = match extract_result_pair(return_type) {
487        Ok(types) => types,
488        Err(e) => return e.to_compile_error().into(),
489    };
490    let response_type_str = type_to_string(&response_type);
491    let error_type_str = type_to_string(&err_type);
492
493    // --- Emit metadata + IntoHandler compatibility assertion ---
494
495    let krate = engine_crate();
496    let name_str = func.sig.ident.to_string();
497    let fn_ident = &func.sig.ident;
498
499    // Build the IntoHandler Params tuple from extra param types.
500    // Zero extras → (), one extra → (P0,), two extras → (P0, P1,), ...
501    let params_tuple = if extra_param_types.is_empty() {
502        quote!(())
503    } else {
504        let types = &extra_param_types;
505        quote!((#(#types,)*))
506    };
507
508    let json_shim_ident = format_ident!("{}__galeon_axum_json", fn_ident);
509
510    let expanded = quote! {
511        #func
512
513        /// Autogenerated by `#[handler]` for `galeon generate routes` axum glue.
514        #[doc(hidden)]
515        #[allow(non_snake_case)]
516        pub fn #json_shim_ident(
517            json: &str,
518            world: &mut #krate::World,
519        ) -> Result<#krate::serde_json::Value, String> {
520            let mut h = #krate::handler_function::IntoHandler::<
521                #request_type,
522                #response_type,
523                #params_tuple,
524            >::into_handler(#fn_ident, #name_str);
525            #krate::run_json_handler_value(&mut *h, json, world)
526        }
527
528        #krate::inventory::submit! {
529            #krate::manifest::HandlerRegistration {
530                name: #name_str,
531                module_path: module_path!(),
532                request_type: #request_type_str,
533                response_type: #response_type_str,
534                error_type: #error_type_str,
535            }
536        }
537
538        // Hidden compile-time assertion: the handler function must be
539        // compatible with IntoHandler. This catches non-SystemParam extra
540        // parameters at compile time rather than letting them silently pass.
541        const _: () = {
542            fn _assert_into_handler<F, Req, Resp, Params>(
543                _f: F,
544            ) where
545                F: #krate::handler_function::IntoHandler<Req, Resp, Params>,
546            {}
547
548            fn _check() {
549                _assert_into_handler::<_, #request_type, _, #params_tuple>(#fn_ident);
550            }
551        };
552    };
553
554    expanded.into()
555}
556
557/// Marks a function as a Galeon **handler** (filesystem-routed API endpoint).
558///
559/// Registers handler metadata via [`inventory`] for code generation.
560/// The function must be:
561/// - `pub` (visible to generated router glue)
562/// - Synchronous (not `async`)
563/// - First parameter is the request type
564/// - Returns `Result<R, E>`
565///
566/// Additional parameters beyond the first are reserved for future ECS
567/// `SystemParam` injection (#163).
568///
569/// # Example
570///
571/// ```ignore
572/// #[galeon_engine::handler]
573/// pub fn dispatch_fleet(cmd: DispatchFleetCmd) -> Result<FleetStatus, FleetError> {
574///     todo!()
575/// }
576/// ```
577#[proc_macro_attribute]
578pub fn handler(attr: TokenStream, input: TokenStream) -> TokenStream {
579    handler_attr(attr, input)
580}