Skip to main content

admixture_macros/
lib.rs

1//! Procedural macros for Admixture test framework.
2//!
3//! This crate provides macros for declaratively defining test contexts and services.
4
5use proc_macro::TokenStream;
6use syn::parse_macro_input;
7
8mod context;
9mod service;
10
11/// Declares a test context with multiple services.
12///
13/// Supports optional inline configuration for each service using the syntax:
14/// `service_name: ServiceType = config_expr`
15///
16/// # Examples
17///
18/// ## With inline configuration (recommended for different configs)
19///
20/// ```ignore
21/// use admixture::context;
22/// use testcontainers_modules::postgres::Postgres;
23///
24/// context! {
25///     MyTestContext {
26///         primary_db: SqlxPostgresServiceSetup = Postgres::default().with_tag("15"),
27///         replica_db: SqlxPostgresServiceSetup = Postgres::default().with_tag("14"),
28///         redis: RedisServiceSetup = Redis::default(),
29///     }
30/// }
31/// ```
32///
33/// ## Without inline configuration (uses Config::default())
34///
35/// ```ignore
36/// use admixture::context;
37///
38/// context! {
39///     MyTestContext {
40///         postgres: SqlxPostgresServiceSetup,
41///         redis: RedisServiceSetup,
42///     }
43/// }
44/// // Requires that each service's Config type implements Default
45/// ```
46///
47/// ## Lifecycle Hooks
48///
49/// Contexts can define optional lifecycle hooks that run at specific points
50/// in the test lifecycle. Hooks are defined in a separate `hooks { ... }` block
51/// and can be placed in any order relative to service definitions.
52///
53/// ```ignore
54/// use admixture::context;
55/// use std::error::Error;
56///
57/// context! {
58///     TestContext {
59///         postgres: PostgresServiceSetup,
60///     },
61///     hooks {
62///         before_all = setup_test_data,
63///         after_all = cleanup_test_data,
64///         before_each = reset_database,
65///         after_each = verify_invariants,
66///     }
67/// }
68///
69/// async fn setup_test_data(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
70///     // Runs once after context starts, before any tests
71///     let client = ctx.postgres().client().await?;
72///     client.execute("INSERT INTO users (name) VALUES ('test')", &[]).await?;
73///     Ok(())
74/// }
75///
76/// async fn reset_database(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
77///     // Runs before each test
78///     ctx.postgres().client().await?.execute("TRUNCATE users", &[]).await?;
79///     Ok(())
80/// }
81///
82/// async fn verify_invariants(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
83///     // Runs after each test (even if test failed)
84///     let count: i64 = ctx.postgres().client().await?
85///         .query_one("SELECT COUNT(*) FROM users WHERE invalid = true", &[])
86///         .await?
87///         .get(0);
88///     if count > 0 {
89///         return Err("Found invalid users after test".into());
90///     }
91///     Ok(())
92/// }
93///
94/// async fn cleanup_test_data(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
95///     // Runs once after all tests, before context stops (best-effort)
96///     ctx.postgres().client().await?.execute("DROP TABLE IF EXISTS temp_data", &[]).await?;
97///     Ok(())
98/// }
99/// ```
100///
101/// ### Hook Execution Order
102///
103/// 1. Context starts
104/// 2. **before_all** - If this fails, all tests in the context group fail
105/// 3. For each test:
106///    - **before_each** - If this fails, that specific test fails
107///    - Test runs
108///    - **after_each** - If this fails, that test fails (always runs, even if test failed)
109/// 4. **after_all** - Failure is logged but doesn't fail tests (best-effort cleanup)
110/// 5. Context stops
111///
112/// ### Hook Failure Behavior
113///
114/// - `before_all` failure → all tests in context group fail
115/// - `before_each` failure → that specific test fails
116/// - `after_each` failure → that specific test fails  
117/// - `after_all` failure → logged as warning, doesn't fail tests
118///
119/// ### Hook Function Signature
120///
121/// All hook functions must have this signature:
122/// ```ignore
123/// async fn hook_name(ctx: &ContextNameRunning) -> Result<(), Box<dyn Error + Send>>
124/// ```
125///
126/// ### Flexible Ordering
127///
128/// Both services and hooks can appear in any order:
129///
130/// ```ignore
131/// context! {
132///     MyContext {
133///         hooks {
134///             before_each = reset_state,
135///         },
136///         postgres: PostgresServiceSetup,
137///         hooks {  // Can even split hooks if needed (though not recommended)
138///             after_each = verify_state,
139///         },
140///         redis: RedisServiceSetup,
141///     }
142/// }
143/// ```
144///
145/// This generates:
146/// - A `MyTestContextConfig` struct with service config fields
147/// - A `MyTestContextSetup` struct with service setup fields
148/// - A `MyTestContextRunning` struct with running service fields
149/// - Implementations of `ContextSetup` and `ContextRunning` traits
150/// - A type alias `MyTestContext = TestContext<MyTestContextRunning>`
151/// - A constructor method `MyTestContext::new(setup)`
152/// - A static `MYTESTCONTEXT_HOOKS` containing hook function pointers
153#[proc_macro]
154pub fn context(input: TokenStream) -> TokenStream {
155    let input = parse_macro_input!(input as context::ContextMacroInput);
156    context::generate(input).into()
157}
158
159/// Declares a service with setup and running states.
160///
161/// ## Flexible Ordering
162///
163/// Elements can be specified in **any order** (except the service name must be first).
164/// This allows you to organize your service definition in the way that makes most sense:
165///
166/// ```ignore
167/// // Functions first
168/// service! {
169///     MyService {
170///         error: MyError,
171///         
172///         async fn start(self) -> Result<MyServiceRunning, MyError> { /**/ }
173///         async fn stop(&mut self) -> Result<(), MyError> { /**/ }
174///         
175///         running { connection: Connection, }
176///     }
177/// }
178///
179/// // Or fields first
180/// service! {
181///     MyService {
182///         error: MyError,
183///         
184///         setup { config: Config, }
185///         running { handle: Handle, }
186///         
187///         async fn start(self) -> Result<MyServiceRunning, MyError> { /**/ }
188///         async fn stop(&mut self) -> Result<(), MyError> { /**/ }
189///     }
190/// }
191/// ```
192///
193/// ## Optional Elements
194///
195/// All of `setup`, `running`, `client`, and `healthy` are optional:
196/// - `setup` defaults to an empty struct if omitted
197/// - `running` defaults to an empty struct if omitted
198/// - `client` defaults to `()` with a default implementation returning `Ok(())`
199/// - `healthy` defaults to a no-op implementation returning `Ok(())`
200///
201/// ## Required Elements
202///
203/// Only three elements are required:
204/// - `error: ErrorType,` - The error type for this service
205/// - `async fn start(...)` - Transition from setup to running state
206/// - `async fn stop(...)` - Cleanup when stopping the service
207///
208/// # Absolute minimal example (no setup/running fields, no client)
209///
210/// ```ignore
211/// use admixture::service;
212///
213/// service! {
214///     TimerService {
215///         error: TimerError,
216///         
217///         async fn start(self) -> Result<TimerServiceRunning, TimerError> {
218///             tokio::spawn(async { /* background timer */ });
219///             Ok(TimerServiceRunning {})
220///         }
221///         
222///         async fn stop(&mut self) -> Result<(), TimerError> {
223///             Ok(())
224///         }
225///     }
226/// }
227/// // No setup, running, client, or healthy needed!
228/// // All auto-generated with sensible defaults
229/// ```
230///
231/// # Example with client and custom health check
232///
233/// ```ignore
234/// use admixture::service;
235///
236/// service! {
237///     MockService {
238///         error: MockError,
239///         client: String,
240///         
241///         setup {
242///             name: String,
243///         }
244///         
245///         running {
246///             name: String,
247///         }
248///         
249///         async fn start(self) -> Result<MockServiceRunning, MockError> {
250///             Ok(MockServiceRunning { name: self.name })
251///         }
252///         
253///         async fn client(&self) -> Result<String, MockError> {
254///             Ok(self.name.clone())
255///         }
256///         
257///         async fn healthy(&self) -> Result<(), MockError> {
258///             // Custom health check logic
259///             if self.name.is_empty() {
260///                 Err(MockError::InvalidState)
261///             } else {
262///                 Ok(())
263///             }
264///         }
265///         
266///         async fn stop(&mut self) -> Result<(), MockError> {
267///             Ok(())
268///         }
269///     }
270/// }
271/// ```
272///
273/// # Example with flexible ordering
274///
275/// ```ignore
276/// use admixture::service;
277///
278/// service! {
279///     BackgroundService {
280///         error: BackgroundError,
281///         
282///         // Stop function first
283///         async fn stop(&mut self) -> Result<(), BackgroundError> {
284///             self.handle.abort();
285///             Ok(())
286///         }
287///         
288///         // Running state in the middle
289///         running {
290///             handle: JoinHandle<()>,
291///         }
292///         
293///         // Start function last
294///         async fn start(self) -> Result<BackgroundServiceRunning, BackgroundError> {
295///             let handle = tokio::spawn(async { /* background work */ });
296///             Ok(BackgroundServiceRunning { handle })
297///         }
298///     }
299/// }
300/// // Order doesn't matter - organize however makes sense!
301/// ```
302///
303/// This generates:
304/// - A `MockServiceConfig` struct matching setup fields
305/// - A `MockServiceSetup` struct with the setup fields
306/// - A `MockServiceRunning` struct with the running fields
307/// - Implementations of `ServiceSetup` and `ServiceRunning` traits
308#[proc_macro]
309pub fn service(input: TokenStream) -> TokenStream {
310    let input = parse_macro_input!(input as service::ServiceMacroInput);
311    service::generate(input).into()
312}