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}