Skip to main content

sofie_macros/
lib.rs

1//! Derives emit trait impls and descriptions only; behaviour lives in the libraries.
2
3use proc_macro::TokenStream;
4use syn::{DeriveInput, parse_macro_input};
5
6mod aggregate;
7mod case;
8#[cfg(feature = "children")]
9mod children;
10#[cfg(feature = "config")]
11mod config;
12mod constraints;
13#[cfg(feature = "contracts")]
14mod contracts;
15/// The rule `case` copies, for the test that holds the copy to it.
16#[cfg(test)]
17#[path = "../../sofie-core/src/case.rs"]
18mod core_case;
19mod crate_path;
20mod enumeration;
21mod event;
22mod every;
23#[cfg(feature = "factories")]
24mod factory;
25#[cfg(feature = "fixtures")]
26mod fixture;
27mod input;
28#[cfg(feature = "joins")]
29mod joins;
30#[cfg(feature = "retention")]
31mod pruned;
32mod read_model;
33#[cfg(feature = "remote-contracts")]
34mod remote;
35mod sensitive;
36mod serialize;
37mod shape;
38#[cfg(feature = "span")]
39mod span;
40#[cfg(feature = "tasks")]
41mod task;
42mod value;
43
44/// `#[owner]` marks the `Id<…>` field naming the owner; on the struct, a record that is its
45/// own owner, or with `(through = parent)` one owned by its parent's owner.
46/// `#[aggregate(behaviors(Slugged(from = name)))]` adds behaviors to `Versioned` and
47/// `Timestamps`. Field constraints are those of `#[derive(Input)]`. A `#[sensitive]` field shows
48/// as `***` in `Debug` and logs; it and one of a sensitive type stay out of JSON and search.
49#[proc_macro_derive(
50    Aggregate,
51    attributes(
52        sofie, owner, aggregate, min, max, length, range, required, unique, expose, mirror,
53        sensitive
54    )
55)]
56pub fn derive_aggregate(item: TokenStream) -> TokenStream {
57    expand(item, aggregate::derive)
58}
59
60/// Needs an `id: Id<Self>` field; stored in its own table beside the aggregate's.
61#[cfg(feature = "children")]
62#[proc_macro_derive(Child, attributes(sofie, min, max, length, range, required))]
63pub fn derive_child(item: TokenStream) -> TokenStream {
64    expand(item, children::derive)
65}
66
67/// `#[sensitive]` masks it in `Debug`. As a handler's input it is read from the body field named
68/// after the type: `cancel(order: Order, cancel_reason: CancelReason)` takes
69/// `{ "cancel_reason": … }`, and `Option<CancelReason>` takes it or nothing.
70#[proc_macro_derive(Value, attributes(sofie, sensitive))]
71pub fn derive_value(item: TokenStream) -> TokenStream {
72    expand(item, value::derive)
73}
74
75/// Read from its module's `<MODULE>_<FIELD>` keys; a field of another struct deriving it reads
76/// `<MODULE>_<FIELD>_*`.
77#[cfg(feature = "config")]
78#[proc_macro_derive(Config, attributes(sofie))]
79pub fn derive_config(item: TokenStream) -> TokenStream {
80    expand(item, config::derive)
81}
82
83/// `#[to(…)]` lists a variant's transitions.
84#[proc_macro_derive(Enum, attributes(sofie, to, tone))]
85pub fn derive_enum(item: TokenStream) -> TokenStream {
86    expand(item, enumeration::derive)
87}
88
89/// `#[read(from = Aggregate)]`; `#[read(via = Base::COLUMN, field = Other::COLUMN)]` reads a
90/// column of the record the base's id column names. A field reading a sensitive one needs
91/// `#[sensitive]`.
92#[proc_macro_derive(ReadModel, attributes(sofie, read, sensitive))]
93pub fn derive_read_model(item: TokenStream) -> TokenStream {
94    expand(item, read_model::derive)
95}
96
97#[proc_macro_derive(Event, attributes(sofie))]
98pub fn derive_event(item: TokenStream) -> TokenStream {
99    expand(item, event::derive)
100}
101
102/// `#[min(n)]`/`#[max(n)]` bound text characters or a number's value; `#[length(min, max)]`,
103/// `#[range(min, max)]`, `#[required]` (not blank) and `#[unique]` (held by the database only).
104/// `#[input(from = Order)]` inherits `Order`'s field constraints, `#[unconstrained]` drops
105/// them; `#[input(validate)]` runs `Validate` after them.
106#[proc_macro_derive(
107    Input,
108    attributes(sofie, input, min, max, length, range, required, unique, unconstrained)
109)]
110pub fn derive_input(item: TokenStream) -> TokenStream {
111    expand(item, input::derive)
112}
113
114/// Takes no arguments: naming the event again could only disagree with the argument's type.
115#[proc_macro_attribute]
116pub fn on(arguments: TokenStream, item: TokenStream) -> TokenStream {
117    if arguments.is_empty() {
118        return item;
119    }
120    let error = syn::Error::new_spanned(
121        proc_macro2::TokenStream::from(arguments),
122        "write a bare `#[on]`: the listener's first argument's type is the event",
123    );
124    [error.into_compile_error().into(), item]
125        .into_iter()
126        .collect()
127}
128
129/// Its arguments are never recorded.
130#[cfg(feature = "span")]
131#[proc_macro_attribute]
132pub fn span(arguments: TokenStream, item: TokenStream) -> TokenStream {
133    span::attribute(&arguments.into(), item.into()).into()
134}
135
136/// Read by `sofie gen`, which registers the job with the worker.
137#[proc_macro_attribute]
138pub fn job(_arguments: TokenStream, item: TokenStream) -> TokenStream {
139    item
140}
141
142/// Read by `sofie gen`, which registers the task as `<module>.<function>`.
143#[cfg(feature = "tasks")]
144#[proc_macro_attribute]
145pub fn task(arguments: TokenStream, item: TokenStream) -> TokenStream {
146    task::attribute(&arguments.into(), item.into()).into()
147}
148
149/// Each `async fn` returns a boxed future so the trait works as `dyn`.
150#[cfg(feature = "contracts")]
151#[proc_macro_attribute]
152pub fn contract(_arguments: TokenStream, item: TokenStream) -> TokenStream {
153    contracts::attribute(item.into(), contracts::Boundary::Contract).into()
154}
155
156/// Calls run in `<vendor>.<method>` spans; see `sofie::client`.
157#[cfg(feature = "gateways")]
158#[proc_macro_attribute]
159pub fn gateway(_arguments: TokenStream, item: TokenStream) -> TokenStream {
160    contracts::attribute(item.into(), contracts::Boundary::Gateway).into()
161}
162
163/// Interval as `30s`, `15m`, `1h`, `1d`; `each = query` runs it per listed record, one job each.
164/// Or a time on a zone's clock: `daily_at = "03:00", zone = "Europe/Istanbul"`, and with
165/// `weekly_on = "monday"` on that day only.
166///
167/// ```
168/// # async fn late() {}
169/// #[sofie_macros::every("1h", each = late)]
170/// pub async fn remind() {}
171/// ```
172///
173/// ```compile_fail
174/// #[sofie_macros::every("1h", each = no_such_query)]
175/// pub async fn remind() {}
176/// ```
177#[proc_macro_attribute]
178pub fn every(arguments: TokenStream, item: TokenStream) -> TokenStream {
179    every::attribute(arguments.into(), item.into()).into()
180}
181
182/// Written `#[sofie::feature(FLAG)]`: a bare `feature` is Rust's own crate attribute.
183#[proc_macro_attribute]
184pub fn feature(_arguments: TokenStream, item: TokenStream) -> TokenStream {
185    item
186}
187
188/// Expands to nothing unless sofie's `fixtures` feature is on.
189#[proc_macro_derive(Fixture)]
190pub fn derive_fixture(item: TokenStream) -> TokenStream {
191    #[cfg(feature = "fixtures")]
192    return expand(item, fixture::derive_fixture);
193    #[cfg(not(feature = "fixtures"))]
194    {
195        drop(item);
196        TokenStream::new()
197    }
198}
199
200/// One database per test; the crate's wiring must export `MODULES`, `app` and `worker`.
201#[cfg(feature = "fixtures")]
202#[proc_macro_attribute]
203pub fn test_app(arguments: TokenStream, item: TokenStream) -> TokenStream {
204    let function = parse_macro_input!(item as syn::ItemFn);
205    fixture::test_app(arguments.into(), &function)
206        .unwrap_or_else(syn::Error::into_compile_error)
207        .into()
208}
209
210fn expand(
211    item: TokenStream,
212    derive: fn(&DeriveInput) -> syn::Result<proc_macro2::TokenStream>,
213) -> TokenStream {
214    let input = parse_macro_input!(item as DeriveInput);
215    derive(&input)
216        .and_then(|expanded| crate_path::with_crate_path(&input, expanded))
217        .unwrap_or_else(syn::Error::into_compile_error)
218        .into()
219}