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}