Skip to main content

citum_schema_style/
macros.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Declarative macros for the Citum ecosystem.
7
8/// Generates a string-backed enum that gracefully captures unknown variants.
9#[macro_export]
10macro_rules! str_enum {
11    (
12        $(#[$meta:meta])*
13        $vis:vis enum $name:ident {
14            $(
15                $(#[$vmeta:meta])*
16                $variant:ident = $val:expr
17            ),+ $(,)?
18        }
19    ) => {
20        $(#[$meta])*
21        #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22        #[cfg_attr(feature = "bindings", derive(specta::Type))]
23        #[non_exhaustive]
24        $vis enum $name {
25            $(
26                $(#[$vmeta])*
27                #[cfg_attr(any(feature = "schema", feature = "bindings"), serde(rename = $val))]
28                $variant,
29            )+
30            #[doc = "Fallback for forward-compatibility."]
31            #[cfg_attr(feature = "schema", schemars(skip))]
32            #[cfg_attr(feature = "bindings", specta(skip))]
33            Unknown(String),
34        }
35
36        impl $name {
37            #[doc = "Returns the string value associated with this variant."]
38            #[must_use]
39            pub fn as_str(&self) -> &str {
40                match self {
41                    $( Self::$variant => $val, )+
42                    Self::Unknown(s) => s.as_str(),
43                }
44            }
45        }
46
47        impl serde::Serialize for $name {
48            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49            where
50                S: serde::Serializer,
51            {
52                serializer.serialize_str(self.as_str())
53            }
54        }
55
56        impl<'de> serde::Deserialize<'de> for $name {
57            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58            where
59                D: serde::Deserializer<'de>,
60            {
61                struct Visitor;
62                impl<'de> serde::de::Visitor<'de> for Visitor {
63                    type Value = $name;
64
65                    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
66                        formatter.write_str("a string")
67                    }
68
69                    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
70                    where
71                        E: serde::de::Error,
72                    {
73                        Ok(match value {
74                            $( $val => $name::$variant, )+
75                            _ => $name::Unknown(value.to_owned()),
76                        })
77                    }
78                }
79                deserializer.deserialize_str(Visitor)
80            }
81        }
82    }
83}
84
85/// Dispatches an operation across all variants of `TemplateComponent`.
86/// Requires `$target` to be a `TemplateComponent` and provides `$inner`
87/// to the closure/expression provided in `$action`.
88#[macro_export]
89macro_rules! dispatch_component {
90    ($target:expr, |$inner:ident| $action:expr) => {
91        match $target {
92            $crate::template::TemplateComponent::Contributor($inner) => $action,
93            $crate::template::TemplateComponent::Date($inner) => $action,
94            $crate::template::TemplateComponent::Title($inner) => $action,
95            $crate::template::TemplateComponent::Number($inner) => $action,
96            $crate::template::TemplateComponent::Variable($inner) => $action,
97            $crate::template::TemplateComponent::Group($inner) => $action,
98            $crate::template::TemplateComponent::Term($inner) => $action,
99        }
100    };
101}
102
103/// Merges fields from a target struct `source` into a mutable `target` if `source.field.is_some()`.
104/// This simplifies boilerplate in configuration merge implementations.
105#[macro_export]
106macro_rules! merge_options {
107    ($target:expr, $source:expr, $($field:ident),+ $(,)?) => {
108        $(
109            if $source.$field.is_some() {
110                $target.$field = $source.$field.clone();
111            }
112        )+
113    };
114}
115
116// AST Builder macros for tests and embedded styles.
117// These use a quasi-DSL to quickly stamp out TemplateComponents.
118
119/// Build a contributor `TemplateComponent` with optional rendering overrides.
120#[macro_export]
121macro_rules! tc_contributor {
122    ($role:ident, $form:ident $(, $key:ident = $val:expr)*) => {
123        $crate::template::TemplateComponent::Contributor(
124            $crate::template::TemplateContributor {
125                contributor: $crate::template::ContributorRole::$role,
126                form: $crate::template::ContributorForm::$form,
127                rendering: $crate::template::Rendering {
128                    $( $key: Some($val.into()), )*
129                    ..Default::default()
130                },
131                ..Default::default()
132            }
133        )
134    };
135}
136
137/// Build a date `TemplateComponent` with optional rendering overrides.
138#[macro_export]
139macro_rules! tc_date {
140    ($date_var:ident, $form:ident $(, $key:ident = $val:expr)*) => {
141        $crate::template::TemplateComponent::Date(
142            $crate::template::TemplateDate {
143                date: $crate::template::DateVariable::$date_var,
144                form: $crate::template::DateForm::$form,
145                rendering: $crate::template::Rendering {
146                    $( $key: Some($val.into()), )*
147                    ..Default::default()
148                },
149                ..Default::default()
150            }
151        )
152    };
153}
154
155/// Build a title `TemplateComponent` with optional rendering overrides.
156#[macro_export]
157macro_rules! tc_title {
158    ($title_type:ident $(, $key:ident = $val:expr)*) => {
159        $crate::template::TemplateComponent::Title(
160            $crate::template::TemplateTitle {
161                title: $crate::template::TitleType::$title_type,
162                rendering: $crate::template::Rendering {
163                    $( $key: Some($val.into()), )*
164                    ..Default::default()
165                },
166                ..Default::default()
167            }
168        )
169    };
170}
171
172/// Build a number `TemplateComponent` with optional rendering overrides.
173#[macro_export]
174macro_rules! tc_number {
175    ($num_var:ident $(, $key:ident = $val:expr)*) => {
176        $crate::template::TemplateComponent::Number(
177            $crate::template::TemplateNumber {
178                number: $crate::template::NumberVariable::$num_var,
179                rendering: $crate::template::Rendering {
180                    $( $key: Some($val.into()), )*
181                    ..Default::default()
182                },
183                ..Default::default()
184            }
185        )
186    };
187}
188
189/// Build a variable `TemplateComponent` with optional rendering overrides.
190#[macro_export]
191macro_rules! tc_variable {
192    ($var:ident $(, $key:ident = $val:expr)*) => {
193        $crate::template::TemplateComponent::Variable(
194            $crate::template::TemplateVariable {
195                variable: $crate::template::SimpleVariable::$var,
196                rendering: $crate::template::Rendering {
197                    $( $key: Some($val.into()), )*
198                    ..Default::default()
199                },
200                ..Default::default()
201            }
202        )
203    };
204}
205
206/// Build a term `TemplateComponent` with optional rendering overrides.
207#[macro_export]
208macro_rules! tc_term {
209    ($term_var:ident $(, $key:ident = $val:expr)*) => {
210        $crate::template::TemplateComponent::Term(
211            $crate::template::TemplateTerm {
212                term: $crate::locale::GeneralTerm::$term_var,
213                rendering: $crate::template::Rendering {
214                    $( $key: Some($val.into()), )*
215                    ..Default::default()
216                },
217                ..Default::default()
218            }
219        )
220    };
221}
222
223/// Build a group `TemplateComponent` with optional rendering options.
224#[macro_export]
225macro_rules! tc_group {
226    ([$($item:expr),* $(,)?] $(, $key:ident = $val:expr)*) => {
227        $crate::template::TemplateComponent::Group(
228            $crate::template::TemplateGroup {
229                group: vec![$($item),*],
230                rendering: $crate::template::Rendering {
231                    $( $key: Some($val.into()), )*
232                    ..Default::default()
233                },
234                ..Default::default()
235            }
236        )
237    };
238}
239
240// Reference builder macros for tests and fixtures.
241// These construct native Citum InputReference values without verbose struct literals.
242
243/// Builds an `InputReference::Monograph` (book) with a single structured-name author.
244#[macro_export]
245macro_rules! ref_book {
246    ($id:expr, $family:expr, $given:expr, $year:expr, $title:expr) => {
247        $crate::reference::InputReference::Monograph(::std::boxed::Box::new(
248            $crate::reference::Monograph {
249                id: Some($id.into()),
250                r#type: $crate::reference::MonographType::Book,
251                title: Some($crate::reference::Title::Single($title.to_string())),
252                author: Some($crate::reference::Contributor::StructuredName(
253                    $crate::reference::StructuredName {
254                        family: $crate::reference::MultilingualString::Simple($family.to_string()),
255                        given: $crate::reference::MultilingualString::Simple($given.to_string()),
256                        ..Default::default()
257                    },
258                )),
259                issued: $crate::reference::EdtfString($year.to_string()),
260                ..Default::default()
261            },
262        ))
263    };
264}
265
266/// Builds an `InputReference::Monograph` (book) with multiple structured-name authors.
267#[macro_export]
268macro_rules! ref_book_authors {
269    ($id:expr, [$(($family:expr, $given:expr)),* $(,)?], $year:expr, $title:expr) => {{
270        let _authors: Vec<$crate::reference::Contributor> = vec![
271            $(
272                $crate::reference::Contributor::StructuredName(
273                    $crate::reference::StructuredName {
274                        family: $crate::reference::MultilingualString::Simple(
275                            $family.to_string(),
276                        ),
277                        given: $crate::reference::MultilingualString::Simple($given.to_string()),
278                        ..Default::default()
279                    },
280                ),
281            )*
282        ];
283        $crate::reference::InputReference::Monograph(::std::boxed::Box::new(
284            $crate::reference::Monograph {
285                id: Some($id.into()),
286                r#type: $crate::reference::MonographType::Book,
287                title: Some($crate::reference::Title::Single($title.to_string())),
288                author: Some($crate::reference::Contributor::ContributorList(
289                    $crate::reference::ContributorList(_authors),
290                )),
291                issued: $crate::reference::EdtfString($year.to_string()),
292                ..Default::default()
293            },
294        ))
295    }};
296}
297
298/// Builds an `InputReference::SerialComponent` (journal article) with a single author.
299#[macro_export]
300macro_rules! ref_article {
301    ($id:expr, $family:expr, $given:expr, $year:expr, $title:expr) => {
302        $crate::reference::InputReference::SerialComponent(::std::boxed::Box::new(
303            $crate::reference::SerialComponent {
304                id: Some($id.into()),
305                r#type: $crate::reference::SerialComponentType::Article,
306                title: Some($crate::reference::Title::Single($title.to_string())),
307                author: Some($crate::reference::Contributor::StructuredName(
308                    $crate::reference::StructuredName {
309                        family: $crate::reference::MultilingualString::Simple($family.to_string()),
310                        given: $crate::reference::MultilingualString::Simple($given.to_string()),
311                        ..Default::default()
312                    },
313                )),
314                issued: $crate::reference::EdtfString($year.to_string()),
315                container: Some($crate::reference::WorkRelation::Embedded(
316                    ::std::boxed::Box::new($crate::reference::InputReference::Serial(
317                        ::std::boxed::Box::new($crate::reference::Serial {
318                            r#type: $crate::reference::SerialType::AcademicJournal,
319                            title: Some($crate::reference::Title::Single(String::new())),
320                            ..Default::default()
321                        }),
322                    )),
323                )),
324                ..Default::default()
325            },
326        ))
327    };
328}
329
330/// Builds an `InputReference::SerialComponent` (journal article) with multiple authors.
331#[macro_export]
332macro_rules! ref_article_authors {
333    ($id:expr, [$(($family:expr, $given:expr)),* $(,)?], $year:expr, $title:expr) => {{
334        let _authors: Vec<$crate::reference::Contributor> = vec![
335            $(
336                $crate::reference::Contributor::StructuredName(
337                    $crate::reference::StructuredName {
338                        family: $crate::reference::MultilingualString::Simple(
339                            $family.to_string(),
340                        ),
341                        given: $crate::reference::MultilingualString::Simple($given.to_string()),
342                        ..Default::default()
343                    },
344                ),
345            )*
346        ];
347        $crate::reference::InputReference::SerialComponent(::std::boxed::Box::new(
348            $crate::reference::SerialComponent {
349                id: Some($id.into()),
350                r#type: $crate::reference::SerialComponentType::Article,
351                title: Some($crate::reference::Title::Single($title.to_string())),
352                author: Some($crate::reference::Contributor::ContributorList(
353                    $crate::reference::ContributorList(_authors),
354                )),
355                issued: $crate::reference::EdtfString($year.to_string()),
356                ..Default::default()
357            },
358        ))
359    }};
360}
361
362/// Builds a `CitationLocator` value.
363#[macro_export]
364macro_rules! citation_locator {
365    ($label:ident, $value:expr) => {
366        $crate::citation::CitationLocator::single(
367            $crate::citation::LocatorType::$label,
368            $value,
369        )
370    };
371    ($l1:ident => $v1:expr, $l2:ident => $v2:expr $(, $lrest:ident => $vrest:expr)* $(,)?) => {
372        $crate::citation::CitationLocator::compound(vec![
373            $crate::citation::LocatorSegment::new(
374                $crate::citation::LocatorType::$l1,
375                $v1,
376            ),
377            $crate::citation::LocatorSegment::new(
378                $crate::citation::LocatorType::$l2,
379                $v2,
380            ),
381            $(
382                $crate::citation::LocatorSegment::new(
383                    $crate::citation::LocatorType::$lrest,
384                    $vrest,
385                )
386            ),*
387        ]).expect("compound locator macro requires at least two segments")
388    };
389}
390
391/// Builds a `CitationItem` with optional named fields.
392#[macro_export]
393macro_rules! citation_item {
394    ($id:expr $(, $key:ident = $val:expr)*) => {{
395        #[allow(unused_mut)]
396        let mut _item = $crate::citation::CitationItem {
397            id: $id.to_string(),
398            ..Default::default()
399        };
400        $( citation_item!(@set _item, $key, $val); )*
401        _item
402    }};
403    (@set $item:ident, locator, $val:expr) => { $item.locator = Some($val); };
404    (@set $item:ident, prefix, $val:expr) => { $item.prefix = Some($val.to_string()); };
405    (@set $item:ident, suffix, $val:expr) => { $item.suffix = Some($val.to_string()); };
406}
407
408/// Builds a `Citation` from a list of `CitationItem` expressions with optional named fields.
409#[macro_export]
410macro_rules! citation {
411    ([$($item:expr),* $(,)?] $(, $key:ident = $val:expr)* $(,)?) => {
412        $crate::citation::Citation {
413            items: vec![$($item),*],
414            $($key: $val,)*
415            ..Default::default()
416        }
417    };
418}
419
420/// Builds a `Citation` with one `CitationItem`.
421#[macro_export]
422macro_rules! cite {
423    ($id:expr) => {
424        $crate::citation::Citation {
425            items: vec![$crate::citation::CitationItem {
426                id: $id.to_string(),
427                ..Default::default()
428            }],
429            ..Default::default()
430        }
431    };
432    ($id:expr, $key:ident = $val:expr) => {
433        $crate::citation::Citation {
434            items: vec![$crate::citation::CitationItem {
435                id: $id.to_string(),
436                ..Default::default()
437            }],
438            $key: $val,
439            ..Default::default()
440        }
441    };
442}
443
444/// Builds an `IndexMap<String, InputReference>` from key-value pairs.
445#[macro_export]
446macro_rules! bib_map {
447    ($($key:expr => $val:expr),* $(,)?) => {{
448        #[allow(unused_mut)]
449        let mut _map = indexmap::IndexMap::new();
450        $( _map.insert($key.to_string(), $val); )*
451        _map
452    }};
453}