Skip to main content

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