Skip to main content

affn_derive/
lib.rs

1//! # affn-derive
2//!
3//! Derive macros for the `affn` crate, providing `#[derive(ReferenceFrame)]`
4//! and `#[derive(ReferenceCenter)]` for convenient frame and center definitions.
5//!
6//! ## Usage
7//!
8//! These derives are re-exported from `affn`, so you typically use them as:
9//!
10//! ```rust,ignore
11//! use affn::{ReferenceFrame, ReferenceCenter};
12//!
13//! #[derive(Debug, Copy, Clone, ReferenceFrame)]
14//! struct MyFrame;
15//!
16//! #[derive(Debug, Copy, Clone, ReferenceCenter)]
17//! struct MyCenter;
18//! ```
19//!
20//! ## Attributes
21//!
22//! ### `#[derive(ReferenceFrame)]`
23//!
24//! - `#[frame(name = "CustomName")]` - Override the frame name (defaults to struct name)
25//! - `#[frame(polar = "dec", azimuth = "ra")]` - Also implement `SphericalNaming` with custom names
26//! - `#[frame(distance = "altitude")]` - Override distance name (defaults to "distance")
27//! - `#[frame(inherent)]` - Generate inherent methods on `Direction<F>` and `Position<C,F,U>`.
28//!   Only valid when the frame is defined in the same crate as `Direction`/`Position`.
29//! - `#[frame(ellipsoid = "Wgs84")]` - Also implement `HasEllipsoid` for the frame.
30//!
31//! ### `#[derive(ReferenceCenter)]`
32//!
33//! - `#[center(name = "CustomName")]` - Override the center name (defaults to struct name)
34//! - `#[center(params = MyParamsType)]` - Specify the `Params` associated type (defaults to `()`)
35//! - `#[center(affine = false)]` - Skip implementing `AffineCenter` marker trait
36
37use proc_macro::TokenStream;
38use proc_macro2::TokenStream as TokenStream2;
39use quote::quote;
40use syn::{parse_macro_input, DeriveInput, Expr, Lit, Meta, Type};
41
42// =============================================================================
43// ReferenceFrame derive
44// =============================================================================
45
46/// Derive macro for implementing [`ReferenceFrame`](affn::frames::ReferenceFrame).
47///
48/// # Example
49///
50/// ```rust,ignore
51/// use affn::ReferenceFrame;
52///
53/// #[derive(Debug, Copy, Clone, ReferenceFrame)]
54/// struct ICRS;
55///
56/// assert_eq!(ICRS::frame_name(), "ICRS");
57/// ```
58///
59/// ## Custom Name
60///
61/// ```rust,ignore
62/// #[derive(Debug, Copy, Clone, ReferenceFrame)]
63/// #[frame(name = "International Celestial Reference System")]
64/// struct ICRS;
65///
66/// assert_eq!(ICRS::frame_name(), "International Celestial Reference System");
67/// ```
68///
69/// ## SphericalNaming
70///
71/// When `polar` and `azimuth` attributes are provided, the macro also implements
72/// [`SphericalNaming`](affn::frames::SphericalNaming):
73///
74/// ```rust,ignore
75/// #[derive(Debug, Copy, Clone, ReferenceFrame)]
76/// #[frame(polar = "dec", azimuth = "ra")]
77/// struct ICRS;
78///
79/// assert_eq!(ICRS::polar_name(), "dec");
80/// assert_eq!(ICRS::azimuth_name(), "ra");
81/// assert_eq!(ICRS::distance_name(), "distance"); // default
82/// ```
83///
84/// With custom distance name:
85///
86/// ```rust,ignore
87/// #[derive(Debug, Copy, Clone, ReferenceFrame)]
88/// #[frame(polar = "lat", azimuth = "lon", distance = "altitude")]
89/// struct ITRF;
90/// ```
91#[proc_macro_derive(ReferenceFrame, attributes(frame))]
92pub fn derive_reference_frame(input: TokenStream) -> TokenStream {
93    let input = parse_macro_input!(input as DeriveInput);
94    match derive_reference_frame_impl(input) {
95        Ok(tokens) => tokens.into(),
96        Err(err) => err.to_compile_error().into(),
97    }
98}
99
100/// Attributes parsed from `#[frame(...)]`.
101#[derive(Default)]
102struct FrameAttributes {
103    /// Custom frame name (defaults to struct name).
104    name: Option<String>,
105    /// Polar angle name for SphericalNaming (e.g., "dec", "lat", "alt").
106    polar: Option<String>,
107    /// Azimuthal angle name for SphericalNaming (e.g., "ra", "lon", "az").
108    azimuth: Option<String>,
109    /// Distance name for SphericalNaming (defaults to "distance").
110    distance: Option<String>,
111    /// Whether to generate inherent impls on Direction<F> and Position<C,F,U>.
112    /// Only valid when the frame is defined in the same crate as Direction/Position.
113    inherent: bool,
114    /// Ellipsoid type name for HasEllipsoid impl (e.g., "Wgs84").
115    ellipsoid: Option<syn::Ident>,
116}
117
118fn derive_reference_frame_impl(input: DeriveInput) -> syn::Result<TokenStream2> {
119    let name = &input.ident;
120
121    // Parse #[frame(...)] attributes
122    let attrs = parse_frame_attributes(&input)?;
123
124    let name_expr = match &attrs.name {
125        Some(custom_name) => quote! { #custom_name },
126        None => {
127            let name_str = name.to_string();
128            quote! { #name_str }
129        }
130    };
131
132    // Generate SphericalNaming impl (always) + inherent methods (when `inherent` flag set)
133    let spherical_impl = match (&attrs.polar, &attrs.azimuth) {
134        (Some(polar), Some(azimuth)) => {
135            let distance = attrs.distance.as_deref().unwrap_or("distance");
136
137            let polar_ident = syn::Ident::new(polar, proc_macro2::Span::call_site());
138            let azimuth_ident = syn::Ident::new(azimuth, proc_macro2::Span::call_site());
139
140            // SphericalNaming is always generated (trait impl, not inherent)
141            let naming_impl = quote! {
142                impl ::affn::frames::SphericalNaming for #name {
143                    fn polar_name() -> &'static str {
144                        #polar
145                    }
146                    fn azimuth_name() -> &'static str {
147                        #azimuth
148                    }
149                    fn distance_name() -> &'static str {
150                        #distance
151                    }
152                }
153            };
154
155            // Inherent impls: only generated when `inherent` flag is set.
156            // These require Direction/Position to be in the same crate as the frame.
157            let inherent_impl = if attrs.inherent {
158                // Determine constructor parameter order:
159                // IAU convention: polar first for alt/az, azimuth first for everything else
160                let polar_first = polar == "alt";
161
162                let (first_param, second_param) = if polar_first {
163                    (&polar_ident, &azimuth_ident)
164                } else {
165                    (&azimuth_ident, &polar_ident)
166                };
167
168                // new_raw always takes (polar, azimuth)
169                let (polar_arg, azimuth_arg) = (&polar_ident, &azimuth_ident);
170
171                let polar_doc = format!("Returns the {} angle in degrees.", polar);
172                let azimuth_doc = format!("Returns the {} angle in degrees.", azimuth);
173                let dir_new_doc = format!(
174                    "Creates a new direction from {} and {} (canonicalized).",
175                    first_param, second_param
176                );
177                let pos_new_doc = format!(
178                    "Creates a new position from {}, {}, and distance (canonicalized).",
179                    first_param, second_param
180                );
181
182                // Ellipsoidal getters: only for frames with an associated
183                // ellipsoid.  We do NOT generate a constructor —
184                // `ellipsoidal::Position` already has its own `new()`.
185                let ellipsoidal_getters = if attrs.ellipsoid.is_some() {
186                    let distance_ident = syn::Ident::new(distance, proc_macro2::Span::call_site());
187                    let distance_doc = format!(
188                        "Returns the {} (height above the reference ellipsoid).",
189                        distance
190                    );
191
192                    quote! {
193                        // ── EllipsoidalPosition<C, F, U>: inherent named getters ──
194
195                        impl<C, U> ::affn::ellipsoidal::Position<C, #name, U>
196                        where
197                            C: ::affn::centers::ReferenceCenter,
198                            U: ::qtty::LengthUnit,
199                        {
200                            #[doc = #polar_doc]
201                            #[inline]
202                            pub fn #polar_ident(&self) -> ::qtty::Degrees {
203                                self.lat
204                            }
205
206                            #[doc = #azimuth_doc]
207                            #[inline]
208                            pub fn #azimuth_ident(&self) -> ::qtty::Degrees {
209                                self.lon
210                            }
211
212                            #[doc = #distance_doc]
213                            #[inline]
214                            pub fn #distance_ident(&self) -> ::qtty::Quantity<U> {
215                                self.height
216                            }
217                        }
218                    }
219                } else {
220                    quote! {}
221                };
222
223                quote! {
224                    // ── Direction<F>: inherent named constructor + getters ──
225
226                    impl ::affn::spherical::Direction<#name> {
227                        #[doc = #dir_new_doc]
228                        #[inline]
229                        pub fn new(
230                            #first_param: ::qtty::Degrees,
231                            #second_param: ::qtty::Degrees,
232                        ) -> Self {
233                            Self::new_raw(
234                                #polar_arg .wrap_quarter_fold(),
235                                #azimuth_arg .normalize(),
236                            )
237                        }
238
239                        #[doc = #polar_doc]
240                        #[inline]
241                        pub fn #polar_ident(&self) -> ::qtty::Degrees {
242                            self.polar
243                        }
244
245                        #[doc = #azimuth_doc]
246                        #[inline]
247                        pub fn #azimuth_ident(&self) -> ::qtty::Degrees {
248                            self.azimuth
249                        }
250                    }
251
252                    // ── Position<C, F, U>: inherent named getters (any center) ──
253
254                    impl<C, U> ::affn::spherical::Position<C, #name, U>
255                    where
256                        C: ::affn::centers::ReferenceCenter,
257                        U: ::qtty::LengthUnit,
258                    {
259                        #[doc = #polar_doc]
260                        #[inline]
261                        pub fn #polar_ident(&self) -> ::qtty::Degrees {
262                            self.polar
263                        }
264
265                        #[doc = #azimuth_doc]
266                        #[inline]
267                        pub fn #azimuth_ident(&self) -> ::qtty::Degrees {
268                            self.azimuth
269                        }
270                    }
271
272                    // ── Position<C, F, U>: named constructor (only Params = ()) ──
273
274                    impl<C, U> ::affn::spherical::Position<C, #name, U>
275                    where
276                        C: ::affn::centers::ReferenceCenter<Params = ()>,
277                        U: ::qtty::LengthUnit,
278                    {
279                        #[doc = #pos_new_doc]
280                        #[inline]
281                        pub fn new<T: Into<::qtty::Quantity<U>>>(
282                            #first_param: ::qtty::Degrees,
283                            #second_param: ::qtty::Degrees,
284                            distance: T,
285                        ) -> Self {
286                            Self::new_raw(
287                                #polar_arg .wrap_quarter_fold(),
288                                #azimuth_arg .normalize(),
289                                distance.into(),
290                            )
291                        }
292                    }
293
294                    #ellipsoidal_getters
295                }
296            } else {
297                quote! {}
298            };
299
300            quote! {
301                #naming_impl
302                #inherent_impl
303            }
304        }
305        (Some(_), None) => {
306            return Err(syn::Error::new_spanned(
307                &input.ident,
308                "`polar` attribute requires `azimuth` to also be specified",
309            ));
310        }
311        (None, Some(_)) => {
312            return Err(syn::Error::new_spanned(
313                &input.ident,
314                "`azimuth` attribute requires `polar` to also be specified",
315            ));
316        }
317        (None, None) => quote! {},
318    };
319
320    // Generate HasEllipsoid impl when `ellipsoid = "..."` is specified
321    let ellipsoid_impl = match &attrs.ellipsoid {
322        Some(ellipsoid_ident) => quote! {
323            impl ::affn::ellipsoid::HasEllipsoid for #name {
324                type Ellipsoid = ::affn::ellipsoid::#ellipsoid_ident;
325            }
326        },
327        None => quote! {},
328    };
329
330    let expanded = quote! {
331        impl ::affn::frames::ReferenceFrame for #name {
332            fn frame_name() -> &'static str {
333                #name_expr
334            }
335        }
336
337        #spherical_impl
338        #ellipsoid_impl
339    };
340
341    Ok(expanded)
342}
343
344fn parse_frame_attributes(input: &DeriveInput) -> syn::Result<FrameAttributes> {
345    let mut attrs = FrameAttributes::default();
346
347    for attr in &input.attrs {
348        if attr.path().is_ident("frame") {
349            let nested = attr.parse_args_with(
350                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
351            )?;
352
353            for meta in nested {
354                match &meta {
355                    Meta::Path(path) if path.is_ident("inherent") => {
356                        attrs.inherent = true;
357                    }
358                    Meta::NameValue(nv) => {
359                        let value_str = extract_string_literal(&nv.value)?;
360
361                        if nv.path.is_ident("name") {
362                            attrs.name = Some(value_str);
363                        } else if nv.path.is_ident("polar") {
364                            attrs.polar = Some(value_str);
365                        } else if nv.path.is_ident("azimuth") {
366                            attrs.azimuth = Some(value_str);
367                        } else if nv.path.is_ident("distance") {
368                            attrs.distance = Some(value_str);
369                        } else if nv.path.is_ident("ellipsoid") {
370                            attrs.ellipsoid =
371                                Some(syn::Ident::new(&value_str, proc_macro2::Span::call_site()));
372                        }
373                    }
374                    _ => {}
375                }
376            }
377        }
378    }
379
380    Ok(attrs)
381}
382
383/// Extract a string literal from an expression, or return an error.
384fn extract_string_literal(expr: &Expr) -> syn::Result<String> {
385    if let Expr::Lit(expr_lit) = expr {
386        if let Lit::Str(lit_str) = &expr_lit.lit {
387            return Ok(lit_str.value());
388        }
389    }
390    Err(syn::Error::new_spanned(expr, "expected string literal"))
391}
392
393// =============================================================================
394// ReferenceCenter derive
395// =============================================================================
396
397/// Derive macro for implementing [`ReferenceCenter`](affn::centers::ReferenceCenter).
398///
399/// By default, this also implements [`AffineCenter`](affn::centers::AffineCenter).
400///
401/// # Example
402///
403/// ```rust,ignore
404/// use affn::ReferenceCenter;
405///
406/// #[derive(Debug, Copy, Clone, ReferenceCenter)]
407/// struct Heliocentric;
408///
409/// assert_eq!(Heliocentric::center_name(), "Heliocentric");
410/// ```
411///
412/// ## Custom Parameters
413///
414/// ```rust,ignore
415/// use affn::ReferenceCenter;
416///
417/// #[derive(Clone, Debug, Default, PartialEq)]
418/// struct ObserverLocation {
419///     lat: f64,
420///     lon: f64,
421/// }
422///
423/// #[derive(Debug, Copy, Clone, ReferenceCenter)]
424/// #[center(params = ObserverLocation)]
425/// struct Topocentric;
426/// ```
427///
428/// ## Skip AffineCenter
429///
430/// ```rust,ignore
431/// #[derive(Debug, Copy, Clone, ReferenceCenter)]
432/// #[center(affine = false)]
433/// struct NonAffineCenter;
434/// ```
435#[proc_macro_derive(ReferenceCenter, attributes(center))]
436pub fn derive_reference_center(input: TokenStream) -> TokenStream {
437    let input = parse_macro_input!(input as DeriveInput);
438    match derive_reference_center_impl(input) {
439        Ok(tokens) => tokens.into(),
440        Err(err) => err.to_compile_error().into(),
441    }
442}
443
444struct CenterAttributes {
445    name: Option<String>,
446    params: Option<Type>,
447    affine: bool,
448}
449
450impl Default for CenterAttributes {
451    fn default() -> Self {
452        Self {
453            name: None,
454            params: None,
455            affine: true,
456        }
457    }
458}
459
460fn derive_reference_center_impl(input: DeriveInput) -> syn::Result<TokenStream2> {
461    let name = &input.ident;
462
463    // Parse #[center(...)] attributes
464    let attrs = parse_center_attributes(&input)?;
465
466    let name_expr = match attrs.name {
467        Some(custom_name) => quote! { #custom_name },
468        None => {
469            let name_str = name.to_string();
470            quote! { #name_str }
471        }
472    };
473
474    let params_type = match attrs.params {
475        Some(ty) => quote! { #ty },
476        None => quote! { () },
477    };
478
479    let affine_impl = if attrs.affine {
480        quote! {
481            impl ::affn::centers::AffineCenter for #name {}
482        }
483    } else {
484        quote! {}
485    };
486
487    let expanded = quote! {
488        impl ::affn::centers::ReferenceCenter for #name {
489            type Params = #params_type;
490
491            fn center_name() -> &'static str {
492                #name_expr
493            }
494        }
495
496        #affine_impl
497    };
498
499    Ok(expanded)
500}
501
502fn parse_center_attributes(input: &DeriveInput) -> syn::Result<CenterAttributes> {
503    let mut attrs = CenterAttributes::default();
504
505    for attr in &input.attrs {
506        if attr.path().is_ident("center") {
507            let nested = attr.parse_args_with(
508                syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
509            )?;
510
511            for meta in nested {
512                match meta {
513                    Meta::NameValue(nv) => {
514                        if nv.path.is_ident("name") {
515                            if let Expr::Lit(expr_lit) = &nv.value {
516                                if let Lit::Str(lit_str) = &expr_lit.lit {
517                                    attrs.name = Some(lit_str.value());
518                                    continue;
519                                }
520                            }
521                            return Err(syn::Error::new_spanned(
522                                &nv.value,
523                                "expected string literal for `name`",
524                            ));
525                        } else if nv.path.is_ident("params") {
526                            // Parse as a type path
527                            if let Expr::Path(expr_path) = &nv.value {
528                                attrs.params = Some(Type::Path(syn::TypePath {
529                                    qself: None,
530                                    path: expr_path.path.clone(),
531                                }));
532                                continue;
533                            }
534                            return Err(syn::Error::new_spanned(
535                                &nv.value,
536                                "expected type for `params`",
537                            ));
538                        } else if nv.path.is_ident("affine") {
539                            if let Expr::Lit(expr_lit) = &nv.value {
540                                if let Lit::Bool(lit_bool) = &expr_lit.lit {
541                                    attrs.affine = lit_bool.value();
542                                    continue;
543                                }
544                            }
545                            return Err(syn::Error::new_spanned(
546                                &nv.value,
547                                "expected boolean for `affine`",
548                            ));
549                        }
550                    }
551                    _ => {}
552                }
553            }
554        }
555    }
556
557    Ok(attrs)
558}