kit_macros/
lib.rs

1//! Procedural macros for the Kit framework
2//!
3//! This crate provides compile-time validated macros for:
4//! - Inertia.js responses with component validation
5//! - Named route redirects with route validation
6//! - Service auto-registration
7//! - Handler attribute for controller methods
8//! - FormRequest for validated request data
9//! - Jest-like testing with describe! and test! macros
10
11use proc_macro::TokenStream;
12
13mod describe;
14mod domain_error;
15mod handler;
16mod inertia;
17mod injectable;
18mod kit_test;
19mod redirect;
20mod request;
21mod service;
22mod test_macro;
23mod utils;
24
25/// Derive macro for generating `Serialize` implementation for Inertia props
26///
27/// # Example
28///
29/// ```rust,ignore
30/// #[derive(InertiaProps)]
31/// struct HomeProps {
32///     title: String,
33///     user: User,
34/// }
35/// ```
36#[proc_macro_derive(InertiaProps)]
37pub fn derive_inertia_props(input: TokenStream) -> TokenStream {
38    inertia::derive_inertia_props_impl(input)
39}
40
41/// Create an Inertia response with compile-time component validation
42///
43/// # Examples
44///
45/// ## With typed struct (recommended for type safety):
46/// ```rust,ignore
47/// #[derive(InertiaProps)]
48/// struct HomeProps {
49///     title: String,
50///     user: User,
51/// }
52///
53/// inertia_response!("Home", HomeProps { title: "Welcome".into(), user })
54/// ```
55///
56/// ## With JSON-like syntax (for quick prototyping):
57/// ```rust,ignore
58/// inertia_response!("Dashboard", { "user": { "name": "John" } })
59/// ```
60///
61/// This macro validates that the component file exists at compile time.
62/// If `frontend/src/pages/Dashboard.tsx` doesn't exist, you'll get a compile error.
63#[proc_macro]
64pub fn inertia_response(input: TokenStream) -> TokenStream {
65    inertia::inertia_response_impl(input)
66}
67
68/// Create a redirect to a named route with compile-time validation
69///
70/// # Examples
71///
72/// ```rust,ignore
73/// // Simple redirect
74/// redirect!("users.index").into()
75///
76/// // Redirect with route parameters
77/// redirect!("users.show").with("id", "42").into()
78///
79/// // Redirect with query parameters
80/// redirect!("users.index").query("page", "1").into()
81/// ```
82///
83/// This macro validates that the route name exists at compile time.
84/// If the route doesn't exist, you'll get a compile error with suggestions.
85#[proc_macro]
86pub fn redirect(input: TokenStream) -> TokenStream {
87    redirect::redirect_impl(input)
88}
89
90/// Mark a trait as a service for the App container
91///
92/// This attribute macro automatically adds `Send + Sync + 'static` bounds
93/// to your trait, making it suitable for use with the dependency injection
94/// container.
95///
96/// # Example
97///
98/// ```rust,ignore
99/// use kit::service;
100///
101/// #[service]
102/// pub trait HttpClient {
103///     async fn get(&self, url: &str) -> Result<String, Error>;
104/// }
105///
106/// // This expands to:
107/// pub trait HttpClient: Send + Sync + 'static {
108///     async fn get(&self, url: &str) -> Result<String, Error>;
109/// }
110/// ```
111///
112/// Then you can use it with the App container:
113///
114/// ```rust,ignore
115/// // Register
116/// App::bind::<dyn HttpClient>(Arc::new(RealHttpClient::new()));
117///
118/// // Resolve
119/// let client: Arc<dyn HttpClient> = App::make::<dyn HttpClient>().unwrap();
120/// ```
121#[proc_macro_attribute]
122pub fn service(attr: TokenStream, input: TokenStream) -> TokenStream {
123    service::service_impl(attr, input)
124}
125
126/// Attribute macro to auto-register a concrete type as a singleton
127///
128/// This macro automatically:
129/// 1. Derives `Default` and `Clone` for the struct
130/// 2. Registers it as a singleton in the App container at startup
131///
132/// # Example
133///
134/// ```rust,ignore
135/// use kit::injectable;
136///
137/// #[injectable]
138/// pub struct AppState {
139///     pub counter: u32,
140/// }
141///
142/// // Automatically registered at startup
143/// // Resolve via:
144/// let state: AppState = App::get().unwrap();
145/// ```
146#[proc_macro_attribute]
147pub fn injectable(_attr: TokenStream, input: TokenStream) -> TokenStream {
148    injectable::injectable_impl(input)
149}
150
151/// Define a domain error with automatic HTTP response conversion
152///
153/// This macro automatically:
154/// 1. Derives `Debug` and `Clone` for the type
155/// 2. Implements `Display`, `Error`, and `HttpError` traits
156/// 3. Implements `From<T> for FrameworkError` for seamless `?` usage
157///
158/// # Attributes
159///
160/// - `status`: HTTP status code (default: 500)
161/// - `message`: Error message for Display (default: struct name converted to sentence)
162///
163/// # Example
164///
165/// ```rust,ignore
166/// use kit::domain_error;
167///
168/// #[domain_error(status = 404, message = "User not found")]
169/// pub struct UserNotFoundError {
170///     pub user_id: i32,
171/// }
172///
173/// // Usage in controller - just use ? operator
174/// pub async fn get_user(id: i32) -> Result<User, FrameworkError> {
175///     users.find(id).ok_or(UserNotFoundError { user_id: id })?
176/// }
177/// ```
178#[proc_macro_attribute]
179pub fn domain_error(attr: TokenStream, input: TokenStream) -> TokenStream {
180    domain_error::domain_error_impl(attr, input)
181}
182
183/// Attribute macro for controller handler methods
184///
185/// Transforms handler functions to automatically extract typed parameters
186/// from HTTP requests using the `FromRequest` trait.
187///
188/// # Examples
189///
190/// ## With Request parameter:
191/// ```rust,ignore
192/// use kit::{handler, Request, Response, json_response};
193///
194/// #[handler]
195/// pub async fn index(req: Request) -> Response {
196///     json_response!({ "message": "Hello" })
197/// }
198/// ```
199///
200/// ## With FormRequest parameter:
201/// ```rust,ignore
202/// use kit::{handler, Response, json_response, request};
203///
204/// #[request]
205/// pub struct CreateUserRequest {
206///     #[validate(email)]
207///     pub email: String,
208/// }
209///
210/// #[handler]
211/// pub async fn store(form: CreateUserRequest) -> Response {
212///     // `form` is already validated - returns 422 if invalid
213///     json_response!({ "email": form.email })
214/// }
215/// ```
216///
217/// ## Without parameters:
218/// ```rust,ignore
219/// #[handler]
220/// pub async fn health_check() -> Response {
221///     json_response!({ "status": "ok" })
222/// }
223/// ```
224#[proc_macro_attribute]
225pub fn handler(attr: TokenStream, input: TokenStream) -> TokenStream {
226    handler::handler_impl(attr, input)
227}
228
229/// Derive macro for FormRequest trait
230///
231/// Generates the `FormRequest` trait implementation for a struct.
232/// The struct must also derive `serde::Deserialize` and `validator::Validate`.
233///
234/// For the cleanest DX, use the `#[request]` attribute macro instead,
235/// which handles all derives automatically.
236///
237/// # Example
238///
239/// ```rust,ignore
240/// use kit::{FormRequest, Deserialize, Validate};
241///
242/// #[derive(Deserialize, Validate, FormRequest)]
243/// pub struct CreateUserRequest {
244///     #[validate(email)]
245///     pub email: String,
246///
247///     #[validate(length(min = 8))]
248///     pub password: String,
249/// }
250/// ```
251#[proc_macro_derive(FormRequest)]
252pub fn derive_form_request(input: TokenStream) -> TokenStream {
253    request::derive_request_impl(input)
254}
255
256/// Attribute macro for clean request data definition
257///
258/// This is the recommended way to define validated request types.
259/// It automatically adds the necessary derives and generates the trait impl.
260///
261/// Works with both:
262/// - `application/json` - JSON request bodies
263/// - `application/x-www-form-urlencoded` - HTML form submissions
264///
265/// # Example
266///
267/// ```rust,ignore
268/// use kit::request;
269///
270/// #[request]
271/// pub struct CreateUserRequest {
272///     #[validate(email)]
273///     pub email: String,
274///
275///     #[validate(length(min = 8))]
276///     pub password: String,
277/// }
278///
279/// // This can now be used directly in handlers:
280/// #[handler]
281/// pub async fn store(form: CreateUserRequest) -> Response {
282///     // Automatically validated - returns 422 with errors if invalid
283///     json_response!({ "email": form.email })
284/// }
285/// ```
286#[proc_macro_attribute]
287pub fn request(attr: TokenStream, input: TokenStream) -> TokenStream {
288    request::request_attr_impl(attr, input)
289}
290
291/// Attribute macro for database-enabled tests
292///
293/// This macro simplifies writing tests that need database access by automatically
294/// setting up an in-memory SQLite database with migrations applied.
295///
296/// By default, it uses `crate::migrations::Migrator` as the migrator type,
297/// following Kit's convention for migration location.
298///
299/// # Examples
300///
301/// ## Basic usage (recommended):
302/// ```rust,ignore
303/// use kit::kit_test;
304/// use kit::testing::TestDatabase;
305///
306/// #[kit_test]
307/// async fn test_user_creation(db: TestDatabase) {
308///     // db is an in-memory SQLite database with all migrations applied
309///     // Any code using DB::connection() will use this test database
310///     let action = CreateUserAction::new();
311///     let user = action.execute("test@example.com").await.unwrap();
312///     assert!(user.id > 0);
313/// }
314/// ```
315///
316/// ## Without TestDatabase parameter:
317/// ```rust,ignore
318/// #[kit_test]
319/// async fn test_action_without_direct_db_access() {
320///     // Database is set up but not directly accessed
321///     // Actions using DB::connection() still work
322///     let action = MyAction::new();
323///     action.execute().await.unwrap();
324/// }
325/// ```
326///
327/// ## With custom migrator:
328/// ```rust,ignore
329/// #[kit_test(migrator = my_crate::CustomMigrator)]
330/// async fn test_with_custom_migrator(db: TestDatabase) {
331///     // Uses custom migrator instead of default
332/// }
333/// ```
334#[proc_macro_attribute]
335pub fn kit_test(attr: TokenStream, input: TokenStream) -> TokenStream {
336    kit_test::kit_test_impl(attr, input)
337}
338
339/// Group related tests with a descriptive name
340///
341/// Creates a module containing related tests, similar to Jest's describe blocks.
342/// Supports nesting for hierarchical test organization.
343///
344/// # Example
345///
346/// ```rust,ignore
347/// use kit::{describe, test, expect};
348/// use kit::testing::TestDatabase;
349///
350/// describe!("ListTodosAction", {
351///     test!("returns empty list when no todos exist", async fn(db: TestDatabase) {
352///         let action = ListTodosAction::new();
353///         let todos = action.execute().await.unwrap();
354///         expect!(todos).to_be_empty();
355///     });
356///
357///     // Nested describe for grouping related tests
358///     describe!("with pagination", {
359///         test!("returns first page", async fn(db: TestDatabase) {
360///             // ...
361///         });
362///     });
363/// });
364/// ```
365#[proc_macro]
366pub fn describe(input: TokenStream) -> TokenStream {
367    describe::describe_impl(input)
368}
369
370/// Define an individual test case with a descriptive name
371///
372/// Creates a test function with optional TestDatabase parameter.
373/// The test name is displayed in failure output for easy identification.
374///
375/// # Examples
376///
377/// ## Async test with database
378/// ```rust,ignore
379/// test!("creates a user", async fn(db: TestDatabase) {
380///     let user = CreateUserAction::new().execute("test@example.com").await.unwrap();
381///     expect!(user.email).to_equal("test@example.com".to_string());
382/// });
383/// ```
384///
385/// ## Async test without database
386/// ```rust,ignore
387/// test!("calculates sum", async fn() {
388///     let result = calculate_sum(1, 2).await;
389///     expect!(result).to_equal(3);
390/// });
391/// ```
392///
393/// ## Sync test
394/// ```rust,ignore
395/// test!("adds numbers", fn() {
396///     expect!(1 + 1).to_equal(2);
397/// });
398/// ```
399///
400/// On failure, the test name is shown:
401/// ```text
402/// Test: "creates a user"
403///   at src/actions/user_action.rs:25
404///
405///   expect!(actual).to_equal(expected)
406///
407///   Expected: "test@example.com"
408///   Received: "wrong@email.com"
409/// ```
410#[proc_macro]
411pub fn test(input: TokenStream) -> TokenStream {
412    test_macro::test_impl(input)
413}