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}