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}