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