ferro_macros/
lib.rs

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