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