Skip to main content

apalis_core/
macros.rs

1#[cfg(feature = "tracing")]
2#[allow(unused_macros)]
3macro_rules! debug {
4    ($($tt:tt)*) => {
5        tracing::debug!($($tt)*)
6    }
7}
8
9#[cfg(feature = "tracing")]
10#[allow(unused_macros)]
11macro_rules! trace {
12    ($($tt:tt)*) => {
13        tracing::trace!($($tt)*)
14    }
15}
16
17#[cfg(feature = "tracing")]
18#[allow(unused_macros)]
19macro_rules! info {
20    ($($tt:tt)*) => {
21        tracing::info!($($tt)*)
22    }
23}
24
25#[cfg(feature = "tracing")]
26#[allow(unused_macros)]
27macro_rules! error {
28    ($($tt:tt)*) => {
29        tracing::error!($($tt)*)
30    };
31}
32
33#[cfg(feature = "tracing")]
34#[allow(unused_macros)]
35macro_rules! warn {
36    ($($tt:tt)*) => {
37        tracing::warn!($($tt)*)
38    }
39}
40
41#[cfg(not(feature = "tracing"))]
42#[allow(unused_macros)]
43macro_rules! debug {
44    ($($tt:tt)*) => {};
45}
46
47#[cfg(not(feature = "tracing"))]
48#[allow(unused_macros)]
49macro_rules! trace {
50    ($($tt:tt)*) => {};
51}
52
53#[cfg(not(feature = "tracing"))]
54#[allow(unused_macros)]
55macro_rules! info {
56    ($($tt:tt)*) => {};
57}
58
59#[cfg(not(feature = "tracing"))]
60#[allow(unused_macros)]
61macro_rules! error {
62    ($($tt:tt)*) => {};
63}
64
65#[cfg(not(feature = "tracing"))]
66#[allow(unused_macros)]
67macro_rules! warn {
68    ($($tt:tt)*) => {};
69}
70
71/// Macro to generate feature tables for backend documentation with standardized assert functions
72///
73/// The table contains ONLY feature names, status icons, and descriptions.
74/// Code examples appear as separate sections below the table.
75///
76/// Usage:
77/// ```rust
78/// use apalis_core::features_table;
79/// # fn example() -> &'static str {
80/// features_table! {
81///     setup = "MemoryStorage::new();",
82///     TaskSink => supported("Ability to push new tasks", true),
83///     Serialization => limited("Serialization support for arguments. Only accepts `json`"),
84///     FetchById => not_implemented("Allow fetching a task by its ID"),
85///     RegisterWorker => not_supported("Allow registering a worker with the backend"),
86/// }
87/// # }
88///
89/// ```
90#[macro_export]
91macro_rules! features_table {
92    (
93        setup = $setup:literal,
94        $(
95            $feature:tt => $status:ident($description:literal $(, $include_example:tt)?)
96        ),* $(,)?
97    ) => {
98        concat!(
99            "## Features\n",
100            "| Feature | Status | Description            |\n",
101            "|---------|--------|------------------------|\n",
102            $(
103                "| ",
104                features_table!(@format_feature_name $feature),
105                " | ",
106                features_table!(@status_icon $status),
107                " | ",
108                $description,
109                " |\n"
110            ),*,
111            "\n",
112            "Key:\n",
113            "✅ : Supported | ⚠️ : Not implemented | ❌ : Not Supported | ❗ Limited support\n\n",
114            "<details>\n",
115            "<summary>Tests:</summary>\n",
116            "\n",
117            $(
118                features_table!(@maybe_generate_doctest $setup, $feature, $status $(, $include_example)?)
119            ),*,
120            "</details>\n"
121        )
122    };
123
124    // Format feature names ONLY - no code, just name and link
125    (@format_feature_name WebUI) => {
126        concat!("[`Web Interface`](https://docs.rs/apalis-board-api/)")
127    };
128
129    (@format_feature_name Serialization) => {
130        concat!("[`Serialization`](https://docs.rs/apalis-core/latest/apalis_core/backend/codec/index.html)")
131    };
132
133    (@format_feature_name Workflow) => {
134        concat!("[`Workflow`](https://docs.rs/apalis-workflow/)")
135    };
136
137    (@format_feature_name $feature:ident) => {
138        concat!("[`", stringify!($feature), "`](https://docs.rs/apalis-core/latest/apalis_core/backend/trait.", stringify!($feature), ".html)")
139    };
140    (@format_feature_name $feature:literal) => {
141        $feature
142    };
143
144    // Helper to decide whether to generate doctest - with explicit flag
145    (@maybe_generate_doctest $setup:literal, $feature:tt, $status:ident, $include_example:tt) => {
146        features_table!(@check_should_generate $setup, $feature, $status, $include_example)
147    };
148
149    // Helper to decide whether to generate doctest - without explicit flag (default behavior)
150    (@maybe_generate_doctest $setup:literal, $feature:tt, $status:ident) => {
151        features_table!(@check_should_generate $setup, $feature, $status, default)
152    };
153
154    // Check if we should generate - true flag
155    (@check_should_generate $setup:literal, $feature:tt, $status:ident, true) => {
156        features_table!(@do_generate_doctest $setup, $feature, $status)
157    };
158
159    // Check if we should generate - false flag
160    (@check_should_generate $setup:literal, $feature:tt, $status:ident, false) => {
161        ""
162    };
163
164    // Check if we should generate - default behavior
165    (@check_should_generate $setup:literal, $feature:tt, supported, default) => {
166        features_table!(@do_generate_doctest $setup, $feature, supported)
167    };
168    (@check_should_generate $setup:literal, $feature:tt, limited, default) => {
169        features_table!(@do_generate_doctest $setup, $feature, limited)
170    };
171    (@check_should_generate $setup:literal, $feature:tt, not_implemented, default) => {
172        ""
173    };
174    (@check_should_generate $setup:literal, $feature:tt, not_supported, default) => {
175        ""
176    };
177
178    // Actually generate the doctest
179    (@do_generate_doctest $setup:literal, $feature:ident, $status:ident) => {
180        concat!(
181            "#### ", stringify!($feature), "\n\n",
182            "```rust\n",
183            "# use apalis_core::worker::context::WorkerContext;\n",
184            "# use apalis_core::worker::builder::WorkerBuilder;\n",
185            "#[tokio::main]\n",
186            "async fn main() {\n",
187            "    // let mut backend = /* snip */;\n",
188            "    # let mut backend = ", $setup, "\n",
189            features_table!(@assert_function $feature), "\n",
190            "```\n\n"
191        )
192    };
193
194    (@assert_function Backend) => { concat!(
195        "    # use futures_util::StreamExt; \n",
196        "    # use apalis_core::backend::Backend;\n",
197        "    async fn task(task: u32, worker: WorkerContext) {\n",
198        "        // Do some work \n",
199        "    #    worker.stop().unwrap();\n",
200        "    }\n",
201        "    let worker = WorkerBuilder::new(\"backend-test\")\n",
202        "        .backend(backend)\n",
203        "        .build(task);\n",
204        "    let _ = worker.stream().take(1).collect::<Vec<_>>().await;\n",
205        "}\n"
206    ) };
207    (@assert_function TaskSink) => { concat!(
208        "    # use apalis_core::backend::TaskSink;\n",
209        "    backend.push(42).await.unwrap();\n\n",
210        "    async fn task(task: u32, worker: WorkerContext) {\n",
211        "        worker.stop().unwrap();\n",
212        "    }\n",
213        "    let worker = WorkerBuilder::new(\"task-sink-test\")\n",
214        "        .backend(backend)\n",
215        "        .build(task);\n",
216        "    worker.run().await.unwrap();\n",
217        "}\n"
218    ) };
219    (@assert_function BackendFactory) => { "fn assert_make_shared<T: Clone + Send + 'static>(t: T); assert_make_shared(backend);" };
220    // Standardized assert function mapping for identifiers
221    (@assert_function Workflow) => { concat!(
222        "    # use apalis_workflow::*;\n",
223        "    # use apalis_core::backend::TaskSink;\n",
224        "    backend.push(42).await.unwrap();\n\n",
225        "    async fn task1(task: u32, worker: WorkerContext) -> u32 {\n",
226        "        task + 99 \n",
227        "    }\n",
228        "    async fn task2(task: u32, worker: WorkerContext) -> u32 {\n",
229        "        task + 1 \n",
230        "    }\n",
231        "    async fn task3(task: u32, worker: WorkerContext) {\n",
232        "        assert_eq!(task, 142);\n",
233        "        worker.stop().unwrap();\n",
234        "    }\n",
235        "    let workflow = SteppedFlow::new(\"test-workflow\")\n",
236        "       .and_then(task1)\n",
237        "       .and_then(task2)\n",
238        "       .and_then(task3);\n",
239        "    let worker = WorkerBuilder::new(\"workflow-test\")\n",
240        "        .backend(backend)\n",
241        "        .build(workflow);\n",
242        "    worker.run().await.unwrap();\n",
243        "}\n"
244    ) };
245    (@assert_function Serialization) => { concat!(
246        "    # use apalis_core::backend::codec::Codec;\n",
247        "    # use apalis_core::backend::BackendConfig;\n",
248        "    # use apalis_core::backend::WireFormatBackend;\n",
249        "   fn assert_codec<B: BackendConfig + WireFormatBackend>(backend: B) \n",
250        "   where\n",
251        "       B::Codec: Codec<B::Args, Compact=Vec<u8>>,\n",
252        "   {\n",
253        "   }\n",
254        "   assert_codec(backend);\n",
255        "}\n"
256    ) };
257    (@assert_function WaitForCompletion) => { concat!(
258        "    # use apalis_core::backend::Backend;\n",
259        "    # use apalis_core::backend::WaitForCompletion;\n",
260        "    fn assert_wait_for_completion<B: WaitForCompletion<u32> + Backend>(backend: B) {};\n",
261        "    assert_wait_for_completion(backend);\n",
262        "}\n"
263    ) };
264    (@assert_function WebUI) => { concat!(
265        "# use apalis_core::backend::Expose;\n",
266        "# use apalis_core::backend::finalize::Durable;\n",
267        "    fn assert_web_ui<B: Expose<u32, Durable>>(backend: B) {};\n",
268        "    assert_web_ui(backend);\n",
269        "}\n"
270    ) };
271    // Status icons
272    (@status_icon supported) => { "✅" };
273    (@status_icon limited) => { "✅ ❗" };
274    (@status_icon not_implemented) => { "⚠️" };
275    (@status_icon not_supported) => { "❌" };
276}
277
278#[cfg(test)]
279mod tests {
280    #[test]
281    fn test_clean_table_structure_with_example_flags() {
282        let table = features_table! {
283            setup = "{
284                use apalis_core::backend::memory::MemoryStorage;
285                // No migrations
286                MemoryStorage::new()
287            };",
288            TaskSink => supported("Ability to push new tasks", true),
289            Serialization => limited("Serialization support for arguments. Only accepts `json`", false),
290            FetchById => not_implemented("Allow fetching a task by its ID"),
291            RegisterWorker => not_supported("Allow registering a worker with the backend"),
292        };
293
294        println!("Generated table:\n{table}");
295
296        // Table should have proper structure
297        assert!(table.contains("## Features"));
298        assert!(table.contains("| Feature | Status | Description"));
299        assert!(table.contains("|---------|--------|------------------------"));
300
301        // Table rows should contain ONLY name, status, description
302        assert!(table.contains("| ✅ | Ability to push new tasks |"));
303
304        // Code examples should be in separate sections for features with true flag or no flag (default)
305        assert!(table.contains("#### TaskSink"));
306
307        // Serialization should NOT have example because it has explicit false flag
308        assert!(!table.contains("#### Serialization"));
309
310        // Non-supported features should NOT have example sections
311        assert!(!table.contains("#### FetchById"));
312        assert!(!table.contains("#### RegisterWorker"));
313
314        // Verify table cells don't contain code blocks
315        let lines: Vec<&str> = table.lines().collect();
316        for line in lines {
317            if line.starts_with("|") && line.contains("TaskSink") {
318                // This table row should NOT contain ```rust
319                assert!(
320                    !line.contains("```rust"),
321                    "Table row contains code block: {line}"
322                );
323            }
324        }
325    }
326
327    #[test]
328    fn test_explicit_false_flag() {
329        let table = features_table! {
330            setup = "{ unreachable!() }",
331            TaskSink => supported("Ability to push new tasks", false),
332        };
333
334        // Should not contain any examples
335        assert!(!table.contains("#### TaskSink Example"));
336        assert!(!table.contains("```rust"));
337    }
338
339    #[test]
340    fn test_explicit_true_flag() {
341        let table = features_table! {
342            setup = "{ unreachable!() }",
343            TaskSink => supported("Ability to push new tasks", true),
344            Serialization => limited("Serialization support", true),
345        };
346
347        // Should contain examples for both
348        assert!(table.contains("#### TaskSink"));
349        assert!(table.contains("#### Serialization"));
350    }
351}