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_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_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 (@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 (@maybe_generate_doctest $setup:literal, $feature:tt, $status:ident) => {
151 features_table!(@check_should_generate $setup, $feature, $status, default)
152 };
153
154 (@check_should_generate $setup:literal, $feature:tt, $status:ident, true) => {
156 features_table!(@do_generate_doctest $setup, $feature, $status)
157 };
158
159 (@check_should_generate $setup:literal, $feature:tt, $status:ident, false) => {
161 ""
162 };
163
164 (@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 (@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 (@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_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 assert!(table.contains("## Features"));
298 assert!(table.contains("| Feature | Status | Description"));
299 assert!(table.contains("|---------|--------|------------------------"));
300
301 assert!(table.contains("| ✅ | Ability to push new tasks |"));
303
304 assert!(table.contains("#### TaskSink"));
306
307 assert!(!table.contains("#### Serialization"));
309
310 assert!(!table.contains("#### FetchById"));
312 assert!(!table.contains("#### RegisterWorker"));
313
314 let lines: Vec<&str> = table.lines().collect();
316 for line in lines {
317 if line.starts_with("|") && line.contains("TaskSink") {
318 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 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 assert!(table.contains("#### TaskSink"));
349 assert!(table.contains("#### Serialization"));
350 }
351}