queuey_macros/lib.rs
1//! Derive macros for [`queuey`](https://docs.rs/queuey).
2//!
3//! * [`macro@Queues`] implements `queuey_core::QueueSet` for a fieldless enum.
4//! * [`macro@Job`] implements `queuey_core::Job` for a serializable payload type.
5//!
6//! # Where the generated code points
7//!
8//! Generated code needs a path to `queuey-core`. Both macros work that
9//! out from the *calling* crate's `Cargo.toml` (via `proc-macro-crate`):
10//!
11//! 1. a dependency on `queuey` (the facade), which emits
12//! `::queuey::__core`, its hidden re-export of the core crate;
13//! 2. otherwise a dependency on `queuey-core`, which emits
14//! `::queuey_core`;
15//! 3. otherwise it falls back to `::queuey_core`.
16//!
17//! Renamed dependencies (`aq = { package = "queuey" }`) are handled. For
18//! anything else (a vendored copy, a re-export under yet another name) say so
19//! explicitly with `#[queues(crate = "...")]` / `#[job(crate = "...")]`, which
20//! always wins.
21//!
22//! # Duration literals
23//!
24//! Every duration in these attributes is a string literal parsed while the macro
25//! runs: an integer followed by an optional unit of `ms`, `s`, `m`, `h` or `d`.
26//! A bare integer means seconds. Whitespace is ignored, so `"500ms"`, `"30s"`,
27//! `"2 m"` and `"30"` are all valid. Anything else is a compile error pointing at
28//! the literal. Zero is rejected everywhere a duration is accepted: a zero
29//! `message_ttl` discards every message on publish, and a zero backoff is
30//! spelled `backoff = "none"`.
31//!
32//! # `retry(...)` grammar
33//!
34//! Shared by `#[queue(...)]` and `#[job(...)]`:
35//!
36//! ```text
37//! retry(
38//! max_attempts = 3, // u32 >= 1, default 3 (1 means no retries)
39//! backoff = "exponential", // "none" | "fixed" | "exponential", default "exponential"
40//! delay = "1s", // fixed only, required for "fixed"
41//! base = "1s", // exponential only, default "1s", must be <= max
42//! factor = 2.0, // exponential only, default 2.0, must be > 0
43//! max = "5m", // exponential only, default "5m"
44//! jitter = true, // exponential only, default true
45//! )
46//! ```
47//!
48//! The exponential defaults match `queuey_core::Backoff::exponential()`.
49
50#![forbid(unsafe_code)]
51#![warn(missing_docs)]
52
53mod attrs;
54mod duration;
55mod job;
56mod queues;
57
58use proc_macro::TokenStream;
59use syn::{DeriveInput, parse_macro_input};
60
61/// Implement `queuey_core::QueueSet` for a fieldless enum.
62///
63/// The enum must also derive the trait's supertraits; the macro deliberately does
64/// not add them for you:
65///
66/// ```text
67/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
68/// ```
69///
70/// # Attributes
71///
72/// Container attribute `#[queues(...)]`, optional:
73///
74/// ```text
75/// #[queues(
76/// prefix = "myapp", // queue names become "myapp.<name>"
77/// crate = "queuey", // path to the core crate re-export
78/// )]
79/// ```
80///
81/// Variant attribute `#[queue(...)]`, optional on every variant:
82///
83/// ```text
84/// #[queue(
85/// name = "img", // default: snake_case of the variant
86/// prefetch = 10, // u16
87/// durable = true, // bool
88/// message_ttl = "30s", // duration literal
89/// max_priority = 10, // u8 in 0..=255; 0 disables priorities
90/// retry(max_attempts = 3, backoff = "exponential", base = "1s", factor = 2.0,
91/// max = "5m", jitter = true),
92/// )]
93/// ```
94///
95/// `max_priority` is the number of AMQP priority levels the queue is declared
96/// with (`x-max-priority`). Omitted, the `QueueConfig` default applies;
97/// `max_priority = 0` turns priorities off, so the queue carries no
98/// `x-max-priority` argument at all. **Changing this value on a queue
99/// that already exists is refused by the broker**: RabbitMQ answers a redeclare
100/// with different arguments with `PRECONDITION_FAILED`, so an existing
101/// deployment must delete the queue first.
102///
103/// # Example
104///
105/// ```
106/// use queuey_core::{QueueSet, Backoff};
107/// use queuey_macros::Queues;
108///
109/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
110/// #[queues(prefix = "myapp")]
111/// enum AppQueues {
112/// #[queue(prefetch = 10, max_priority = 5)]
113/// Emails,
114/// #[queue(name = "img", message_ttl = "30s", max_priority = 0, retry(max_attempts = 5))]
115/// ImageResize,
116/// }
117///
118/// assert_eq!(AppQueues::Emails.name(), "myapp.emails");
119/// assert_eq!(AppQueues::ImageResize.name(), "myapp.img");
120/// assert_eq!(AppQueues::Emails.config().prefetch, 10);
121/// assert_eq!(AppQueues::Emails.config().max_priority, Some(5));
122/// assert_eq!(AppQueues::ImageResize.config().max_priority, None);
123/// assert_eq!(AppQueues::from_name("myapp.img"), Some(AppQueues::ImageResize));
124/// ```
125///
126/// # Errors
127///
128/// Compile errors, spanned at the offending token, are produced for: a non-enum
129/// item, a generic enum, an enum without variants, a variant with fields,
130/// duplicate resolved queue names, unknown or duplicated attribute keys, a
131/// literal of the wrong type, an empty `prefix`/`name`/resolved queue name, a
132/// `prefetch` outside `1..=65535` (`0` means *unlimited* in AMQP, so omit the key
133/// instead), a `max_priority` outside `0..=255`, a `max_attempts` outside
134/// `1..=u32::MAX`, a malformed or zero
135/// duration, an unknown backoff kind, `base` greater than `max`, `delay` outside
136/// of `backoff = "fixed"`, and `base`/`factor`/`max`/`jitter` outside of
137/// `backoff = "exponential"`.
138#[proc_macro_derive(Queues, attributes(queues, queue))]
139pub fn derive_queues(input: TokenStream) -> TokenStream {
140 let input = parse_macro_input!(input as DeriveInput);
141 queues::derive(&input)
142 .unwrap_or_else(syn::Error::into_compile_error)
143 .into()
144}
145
146/// Implement `queuey_core::Job` for a struct or enum.
147///
148/// The type must also be `Serialize + DeserializeOwned`; add those derives
149/// yourself, this macro never generates them.
150///
151/// # Attributes
152///
153/// ```text
154/// #[job(
155/// queue = AppQueues::Emails, // required; Job::Queue = AppQueues
156/// name = "emails.send", // default: module_path!() + "::" + type name
157/// retry(max_attempts = 5), // optional; generates retry_policy()
158/// crate = "queuey", // path to the core crate re-export
159/// )]
160/// ```
161///
162/// `queue` is a path with at least two segments: the last segment is the variant
163/// (`Job::QUEUE`) and everything before it is the queue set type (`Job::Queue`).
164///
165/// # Example
166///
167/// ```
168/// use queuey_core::Job;
169/// use queuey_macros::{Job, Queues};
170/// use serde::{Deserialize, Serialize};
171///
172/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Queues)]
173/// enum AppQueues {
174/// Emails,
175/// }
176///
177/// #[derive(Job, Serialize, Deserialize)]
178/// #[job(queue = AppQueues::Emails, retry(max_attempts = 5, backoff = "fixed", delay = "2s"))]
179/// struct SendEmail {
180/// to: String,
181/// }
182///
183/// assert_eq!(SendEmail::QUEUE, AppQueues::Emails);
184/// assert!(SendEmail::retry_policy().is_some());
185/// ```
186///
187/// # Errors
188///
189/// Compile errors, spanned at the offending token, are produced for: a missing
190/// `queue` key, a `queue` path with a single segment, a generic type, unknown or
191/// duplicated attribute keys, and every `retry(...)` error listed on
192/// [`macro@Queues`].
193#[proc_macro_derive(Job, attributes(job))]
194pub fn derive_job(input: TokenStream) -> TokenStream {
195 let input = parse_macro_input!(input as DeriveInput);
196 job::derive(&input)
197 .unwrap_or_else(syn::Error::into_compile_error)
198 .into()
199}