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 asset;
15mod authenticatable;
16mod describe;
17mod domain_error;
18mod ferro_test;
19mod handler;
20mod inertia;
21mod injectable;
22mod memoize;
23mod model;
24mod redirect;
25mod request;
26mod resource;
27mod resource_get;
28mod resource_post;
29mod service;
30mod test_macro;
31mod utils;
32mod validate;
33
34/// Derive macro for generating `Serialize` implementation for Inertia props
35///
36/// # Example
37///
38/// ```rust,ignore
39/// #[derive(InertiaProps)]
40/// struct HomeProps {
41/// title: String,
42/// user: User,
43/// }
44/// ```
45#[proc_macro_derive(InertiaProps, attributes(inertia))]
46pub fn derive_inertia_props(input: TokenStream) -> TokenStream {
47 inertia::derive_inertia_props_impl(input)
48}
49
50/// Create an Inertia response with compile-time component validation
51///
52/// # Examples
53///
54/// ## With typed struct (recommended for type safety):
55/// ```rust,ignore
56/// #[derive(InertiaProps)]
57/// struct HomeProps {
58/// title: String,
59/// user: User,
60/// }
61///
62/// inertia_response!("Home", HomeProps { title: "Welcome".into(), user })
63/// ```
64///
65/// ## With JSON-like syntax (for quick prototyping):
66/// ```rust,ignore
67/// inertia_response!("Dashboard", { "user": { "name": "John" } })
68/// ```
69///
70/// This macro validates that the component file exists at compile time.
71/// If `frontend/src/pages/Dashboard.tsx` doesn't exist, you'll get a compile error.
72#[proc_macro]
73pub fn inertia_response(input: TokenStream) -> TokenStream {
74 inertia::inertia_response_impl(input)
75}
76
77/// Create a redirect to a path or named route
78///
79/// # Examples
80///
81/// ```rust,ignore
82/// // Path redirect (starts with /)
83/// redirect!("/dashboard").into()
84///
85/// // Named route redirect
86/// redirect!("users.index").into()
87///
88/// // Redirect with route parameters
89/// redirect!("users.show").with("id", "42").into()
90///
91/// // Redirect with query parameters
92/// redirect!("users.index").query("page", "1").into()
93/// ```
94///
95/// For named routes, this macro validates that the route exists at compile time.
96/// Path redirects (starting with `/`) bypass validation and redirect directly.
97#[proc_macro]
98pub fn redirect(input: TokenStream) -> TokenStream {
99 redirect::redirect_impl(input)
100}
101
102/// Mark a trait as a service for the App container
103///
104/// This attribute macro automatically adds `Send + Sync + 'static` bounds
105/// to your trait, making it suitable for use with the dependency injection
106/// container.
107///
108/// # Example
109///
110/// ```rust,ignore
111/// use ferro::service;
112///
113/// #[service]
114/// pub trait HttpClient {
115/// async fn get(&self, url: &str) -> Result<String, Error>;
116/// }
117///
118/// // This expands to:
119/// pub trait HttpClient: Send + Sync + 'static {
120/// async fn get(&self, url: &str) -> Result<String, Error>;
121/// }
122/// ```
123///
124/// Then you can use it with the App container:
125///
126/// ```rust,ignore
127/// // Register
128/// App::bind::<dyn HttpClient>(Arc::new(RealHttpClient::new()));
129///
130/// // Resolve
131/// let client: Arc<dyn HttpClient> = App::make::<dyn HttpClient>().unwrap();
132/// ```
133#[proc_macro_attribute]
134pub fn service(attr: TokenStream, input: TokenStream) -> TokenStream {
135 service::service_impl(attr, input)
136}
137
138/// Attribute macro to auto-register a concrete type as a singleton
139///
140/// This macro automatically:
141/// 1. Derives `Default` and `Clone` for the struct
142/// 2. Registers it as a singleton in the App container at startup
143///
144/// # Example
145///
146/// ```rust,ignore
147/// use ferro::injectable;
148///
149/// #[injectable]
150/// pub struct AppState {
151/// pub counter: u32,
152/// }
153///
154/// // Automatically registered at startup
155/// // Resolve via:
156/// let state: AppState = App::get().unwrap();
157/// ```
158#[proc_macro_attribute]
159pub fn injectable(_attr: TokenStream, input: TokenStream) -> TokenStream {
160 injectable::injectable_impl(input)
161}
162
163/// Define a domain error with automatic HTTP response conversion
164///
165/// This macro automatically:
166/// 1. Derives `Debug` and `Clone` for the type
167/// 2. Implements `Display`, `Error`, and `HttpError` traits
168/// 3. Implements `From<T> for FrameworkError` for seamless `?` usage
169///
170/// # Attributes
171///
172/// - `status`: HTTP status code (default: 500)
173/// - `message`: Error message for Display (default: struct name converted to sentence)
174///
175/// # Example
176///
177/// ```rust,ignore
178/// use ferro::domain_error;
179///
180/// #[domain_error(status = 404, message = "User not found")]
181/// pub struct UserNotFoundError {
182/// pub user_id: i32,
183/// }
184///
185/// // Usage in controller - just use ? operator
186/// pub async fn get_user(id: i32) -> Result<User, FrameworkError> {
187/// users.find(id).ok_or(UserNotFoundError { user_id: id })?
188/// }
189/// ```
190#[proc_macro_attribute]
191pub fn domain_error(attr: TokenStream, input: TokenStream) -> TokenStream {
192 domain_error::domain_error_impl(attr, input)
193}
194
195/// Attribute macro for controller handler methods
196///
197/// Transforms handler functions to automatically extract typed parameters
198/// from HTTP requests using the `FromRequest` trait.
199///
200/// # Examples
201///
202/// ## With Request parameter:
203/// ```rust,ignore
204/// use ferro::{handler, Request, Response, json_response};
205///
206/// #[handler]
207/// pub async fn index(req: Request) -> Response {
208/// json_response!({ "message": "Hello" })
209/// }
210/// ```
211///
212/// ## With FormRequest parameter:
213/// ```rust,ignore
214/// use ferro::{handler, Response, json_response, request};
215///
216/// #[request]
217/// pub struct CreateUserRequest {
218/// #[validate(email)]
219/// pub email: String,
220/// }
221///
222/// #[handler]
223/// pub async fn store(form: CreateUserRequest) -> Response {
224/// // `form` is already validated - returns 422 if invalid
225/// json_response!({ "email": form.email })
226/// }
227/// ```
228///
229/// ## Without parameters:
230/// ```rust,ignore
231/// #[handler]
232/// pub async fn health_check() -> Response {
233/// json_response!({ "status": "ok" })
234/// }
235/// ```
236#[proc_macro_attribute]
237pub fn handler(attr: TokenStream, input: TokenStream) -> TokenStream {
238 handler::handler_impl(attr, input)
239}
240
241/// Attribute macro for POST-style action handlers that mutate state and redirect.
242///
243/// Transforms an async function returning `ActionResult` into a
244/// `Response`-returning handler. On `Ok(())` emits a 303 redirect to
245/// `redirect_to`; on `Err(ActionError)` emits a 303 redirect to `redirect_to`
246/// (or `err.redirect_override` if set and same-origin) with a flash payload
247/// and back-compat `?error=...&msg=...` query parameters.
248///
249/// # Required attributes
250///
251/// - `redirect_to = "<path>"` — the default 303 target on success.
252///
253/// # Optional attributes
254///
255/// - `method = "<METHOD>"` — HTTP method hint (default `"POST"`).
256///
257/// # Example
258///
259/// ```rust,ignore
260/// use ferro::{action, ActionError, ActionResult, Request};
261///
262/// #[action(redirect_to = "/dashboard/pagine")]
263/// pub async fn publish_by_id(req: Request) -> ActionResult {
264/// let id: i64 = req.param("id")?.parse()?;
265/// publish_page(id).await?;
266/// Ok(())
267/// }
268/// ```
269#[proc_macro_attribute]
270pub fn action(attr: TokenStream, input: TokenStream) -> TokenStream {
271 action::action_impl(attr, input)
272}
273
274/// Embed a static asset at compile time and register it as a content-hashed
275/// `ferro::bundle::Bundle`, returning the hashed URL as `&'static str`.
276///
277/// The path is resolved relative to the source file (call-site-source-relative,
278/// identical to `include_bytes!`). Registration is lazy and happens once per call
279/// site, so `asset!()` is safe inside per-request / hot render paths.
280///
281/// # Example
282///
283/// ```rust,ignore
284/// let url: &'static str = ferro::asset!("assets/app.js");
285/// ```
286#[proc_macro]
287pub fn asset(input: TokenStream) -> TokenStream {
288 asset::asset_impl(input)
289}
290
291/// Mark an `async fn` or `async` impl method for request-scoped memoization.
292///
293/// The function body runs at most once per `(callsite, arguments)` per request.
294/// Concurrent callers for the same key within one request coalesce onto a single
295/// shared computation. Outside a request context the body runs normally with no
296/// caching (graceful no-op, D-02).
297///
298/// The full return value — including `Result::Err` — is cached for the duration
299/// of the request. Every caller within the same request observes the same
300/// resolved value (D-04).
301///
302/// # Constraints
303///
304/// - All value arguments must implement [`std::hash::Hash`].
305/// - The return type must implement `Clone + Send + Sync + 'static`.
306/// - Applied to impl methods, `&self` is excluded from the key (service
307/// singletons are stateless).
308/// - Only `async fn` is accepted; applying to a synchronous function is a
309/// compile error.
310/// - Only simple identifier argument patterns are supported in v17.0;
311/// destructuring patterns are a compile error.
312///
313/// # Example
314///
315/// ```rust,ignore
316/// use ferro::memoize;
317///
318/// #[memoize]
319/// pub async fn load_product(id: u32) -> Vec<String> {
320/// // expensive DB query — runs at most once per id per request
321/// vec![format!("product-{id}")]
322/// }
323/// ```
324#[proc_macro_attribute]
325pub fn memoize(attr: TokenStream, input: TokenStream) -> TokenStream {
326 memoize::memoize_impl(attr, input)
327}
328
329/// Derive macro for FormRequest trait
330///
331/// Generates the `FormRequest` trait implementation for a struct.
332/// The struct must also derive `serde::Deserialize` and `validator::Validate`.
333///
334/// For the cleanest DX, use the `#[request]` attribute macro instead,
335/// which handles all derives automatically.
336///
337/// # Example
338///
339/// ```rust,ignore
340/// use ferro::{FormRequest, Deserialize, Validate};
341///
342/// #[derive(Deserialize, Validate, FormRequest)]
343/// pub struct CreateUserRequest {
344/// #[validate(email)]
345/// pub email: String,
346///
347/// #[validate(length(min = 8))]
348/// pub password: String,
349/// }
350/// ```
351#[proc_macro_derive(FormRequest)]
352pub fn derive_form_request(input: TokenStream) -> TokenStream {
353 request::derive_request_impl(input)
354}
355
356/// Attribute macro for clean request data definition
357///
358/// This is the recommended way to define validated request types.
359/// It automatically adds the necessary derives and generates the trait impl.
360///
361/// Works with both:
362/// - `application/json` - JSON request bodies
363/// - `application/x-www-form-urlencoded` - HTML form submissions
364///
365/// # Example
366///
367/// ```rust,ignore
368/// use ferro::request;
369///
370/// #[request]
371/// pub struct CreateUserRequest {
372/// #[validate(email)]
373/// pub email: String,
374///
375/// #[validate(length(min = 8))]
376/// pub password: String,
377/// }
378///
379/// // This can now be used directly in handlers:
380/// #[handler]
381/// pub async fn store(form: CreateUserRequest) -> Response {
382/// // Automatically validated - returns 422 with errors if invalid
383/// json_response!({ "email": form.email })
384/// }
385/// ```
386#[proc_macro_attribute]
387pub fn request(attr: TokenStream, input: TokenStream) -> TokenStream {
388 request::request_attr_impl(attr, input)
389}
390
391/// Attribute macro for database-enabled tests
392///
393/// This macro simplifies writing tests that need database access by automatically
394/// setting up an in-memory SQLite database with migrations applied.
395///
396/// By default, it uses `crate::migrations::Migrator` as the migrator type,
397/// following Ferro's convention for migration location.
398///
399/// # Examples
400///
401/// ## Basic usage (recommended):
402/// ```rust,ignore
403/// use ferro::ferro_test;
404/// use ferro::testing::TestDatabase;
405///
406/// #[ferro_test]
407/// async fn test_user_creation(db: TestDatabase) {
408/// // db is an in-memory SQLite database with all migrations applied
409/// // Any code using DB::connection() will use this test database
410/// let action = CreateUserAction::new();
411/// let user = action.execute("test@example.com").await.unwrap();
412/// assert!(user.id > 0);
413/// }
414/// ```
415///
416/// ## Without TestDatabase parameter:
417/// ```rust,ignore
418/// #[ferro_test]
419/// async fn test_action_without_direct_db_access() {
420/// // Database is set up but not directly accessed
421/// // Actions using DB::connection() still work
422/// let action = MyAction::new();
423/// action.execute().await.unwrap();
424/// }
425/// ```
426///
427/// ## With custom migrator:
428/// ```rust,ignore
429/// #[ferro_test(migrator = my_crate::CustomMigrator)]
430/// async fn test_with_custom_migrator(db: TestDatabase) {
431/// // Uses custom migrator instead of default
432/// }
433/// ```
434#[proc_macro_attribute]
435pub fn ferro_test(attr: TokenStream, input: TokenStream) -> TokenStream {
436 ferro_test::ferro_test_impl(attr, input)
437}
438
439/// Group related tests with a descriptive name
440///
441/// Creates a module containing related tests, similar to Jest's describe blocks.
442/// Supports nesting for hierarchical test organization.
443///
444/// # Example
445///
446/// ```rust,ignore
447/// use ferro::{describe, test, expect};
448/// use ferro::testing::TestDatabase;
449///
450/// describe!("ListTodosAction", {
451/// test!("returns empty list when no todos exist", async fn(db: TestDatabase) {
452/// let action = ListTodosAction::new();
453/// let todos = action.execute().await.unwrap();
454/// expect!(todos).to_be_empty();
455/// });
456///
457/// // Nested describe for grouping related tests
458/// describe!("with pagination", {
459/// test!("returns first page", async fn(db: TestDatabase) {
460/// // ...
461/// });
462/// });
463/// });
464/// ```
465#[proc_macro]
466pub fn describe(input: TokenStream) -> TokenStream {
467 describe::describe_impl(input)
468}
469
470/// Define an individual test case with a descriptive name
471///
472/// Creates a test function with optional TestDatabase parameter.
473/// The test name is displayed in failure output for easy identification.
474///
475/// # Examples
476///
477/// ## Async test with database
478/// ```rust,ignore
479/// test!("creates a user", async fn(db: TestDatabase) {
480/// let user = CreateUserAction::new().execute("test@example.com").await.unwrap();
481/// expect!(user.email).to_equal("test@example.com".to_string());
482/// });
483/// ```
484///
485/// ## Async test without database
486/// ```rust,ignore
487/// test!("calculates sum", async fn() {
488/// let result = calculate_sum(1, 2).await;
489/// expect!(result).to_equal(3);
490/// });
491/// ```
492///
493/// ## Sync test
494/// ```rust,ignore
495/// test!("adds numbers", fn() {
496/// expect!(1 + 1).to_equal(2);
497/// });
498/// ```
499///
500/// On failure, the test name is shown:
501/// ```text
502/// Test: "creates a user"
503/// at src/actions/user_action.rs:25
504///
505/// expect!(actual).to_equal(expected)
506///
507/// Expected: "test@example.com"
508/// Received: "wrong@email.com"
509/// ```
510#[proc_macro]
511pub fn test(input: TokenStream) -> TokenStream {
512 test_macro::test_impl(input)
513}
514
515/// Derive macro for reducing SeaORM model boilerplate
516///
517/// Generates create builder, update builder, and convenience methods for Ferro models.
518/// Apply to a SeaORM Model struct to get:
519/// - `Model::query()` - Start a new QueryBuilder
520/// - `Model::create()` - Get a builder for inserting new records
521/// - `model.update()` - Get an UpdateBuilder for selective field updates
522/// - `model.delete()` - Delete the record
523///
524/// # Example
525///
526/// ```rust,ignore
527/// use ferro::FerroModel;
528/// use sea_orm::entity::prelude::*;
529///
530/// #[derive(Clone, Debug, DeriveEntityModel, FerroModel)]
531/// #[sea_orm(table_name = "users")]
532/// pub struct Model {
533/// #[sea_orm(primary_key)]
534/// pub id: i32,
535/// pub name: String,
536/// pub email: String,
537/// pub bio: Option<String>,
538/// }
539///
540/// // Create a new record
541/// let user = User::create()
542/// .set_name("John")
543/// .set_email("john@example.com")
544/// .insert()
545/// .await?;
546///
547/// // Update specific fields only (unchanged fields are not sent to DB)
548/// let updated = user
549/// .update()
550/// .set_name("John Doe")
551/// .set_bio("Developer")
552/// .save()
553/// .await?;
554///
555/// // Clear an optional field to NULL
556/// let updated = updated
557/// .update()
558/// .clear_bio()
559/// .save()
560/// .await?;
561///
562/// // Query records
563/// let users = User::query()
564/// .filter(Column::Name.contains("John"))
565/// .all()
566/// .await?;
567/// ```
568#[proc_macro_derive(FerroModel)]
569pub fn derive_ferro_model(input: TokenStream) -> TokenStream {
570 model::ferro_model_impl(input)
571}
572
573/// Derive the `Authenticatable` trait so `Auth::user_as::<T>()` works.
574///
575/// Generates `auth_identifier` (`self.id as i64`), `auth_identifier_name`, and
576/// `as_any` for a struct with an integer `id` field. Override the field with
577/// `#[auth(id = "user_id")]`.
578///
579/// ```rust,ignore
580/// #[derive(Clone, DeriveEntityModel, Authenticatable)]
581/// pub struct Model { #[sea_orm(primary_key)] pub id: i32, /* … */ }
582/// ```
583#[proc_macro_derive(Authenticatable, attributes(auth))]
584pub fn derive_authenticatable(input: TokenStream) -> TokenStream {
585 authenticatable::derive_authenticatable_impl(input)
586}
587
588/// Derive macro for declarative struct validation using Ferro's rules
589///
590/// Generates `Validatable` trait implementation from field attributes.
591/// Validation rules are co-located with the struct definition.
592///
593/// This uses Ferro's Laravel-style validation rules (required(), email(), etc.)
594/// rather than the external `validator` crate.
595///
596/// # Example
597///
598/// ```rust,ignore
599/// use ferro::ValidateRules;
600///
601/// #[derive(ValidateRules)]
602/// struct CreateUserRequest {
603/// #[rule(required, email)]
604/// email: String,
605///
606/// #[rule(required, min(8))]
607/// password: String,
608///
609/// #[rule(required, integer, min(18))]
610/// age: Option<i32>,
611/// }
612///
613/// // Usage
614/// let request = CreateUserRequest { ... };
615/// request.validate()?;
616/// ```
617#[proc_macro_derive(ValidateRules, attributes(rule))]
618pub fn derive_validate_rules(input: TokenStream) -> TokenStream {
619 validate::validate_impl(input)
620}
621
622/// Derive macro for generating `Resource` trait implementation from struct annotations
623///
624/// Supports struct-level and field-level `#[resource(...)]` attributes:
625///
626/// - `#[resource(model = "path::to::Model")]` (struct-level) — generates `From<Model>` impl
627/// - `#[resource(rename = "new_name")]` (field-level) — use a different key in JSON output
628/// - `#[resource(skip)]` (field-level) — exclude field from JSON output
629///
630/// # Example
631///
632/// ```rust,ignore
633/// use ferro::ApiResource;
634///
635/// #[derive(ApiResource)]
636/// #[resource(model = "entities::users::Model")]
637/// pub struct UserResource {
638/// pub id: i32,
639/// pub name: String,
640/// #[resource(rename = "member_since")]
641/// pub created_at: String,
642/// #[resource(skip)]
643/// pub password_hash: String,
644/// }
645/// ```
646#[proc_macro_derive(ApiResource, attributes(resource))]
647pub fn derive_api_resource(input: TokenStream) -> TokenStream {
648 resource::api_resource_impl(input)
649}
650
651/// Attribute macro for GET handlers displaying a single tenant-scoped resource.
652///
653/// Folds id-extraction + tenant resolution + tenant-scoped lookup + 404-on-miss
654/// into a single attribute. Tenant and resource remain real typed function
655/// parameters; the user body moves to a named inner fn `__<name>_inner`.
656///
657/// # Required arguments
658///
659/// - First positional arg: the resource type implementing `TenantScoped`, e.g. `Customer`.
660///
661/// # Optional arguments
662///
663/// - `on_miss = "/url"` — redirect target on lookup miss; omitted → 404.
664/// Supports `{id}` placeholder (substituted with the extracted resource id).
665/// - `tenant = "expr"` — escape-hatch Rust expression for tenant resolution (default: `current_tenant()`).
666/// - `find = "path::fn"` — override the lookup function (default: `TenantScoped::find_for_tenant`).
667///
668/// # Example
669///
670/// ```ignore
671/// use ferro::{resource_get, Response, Request, TenantContext};
672///
673/// #[resource_get(Customer, on_miss = "/dashboard/clienti")]
674/// pub async fn edit(req: &mut Request, tenant: &TenantContext, customer: &Customer) -> Response {
675/// // customer is guaranteed to exist and belong to tenant
676/// Ok(ferro::HttpResponse::new())
677/// }
678/// ```
679///
680/// # Expands to (abridged)
681///
682/// The attribute is equivalent to the following expansion (shown via `cargo expand`):
683///
684/// ```ignore
685/// // Generated outer fn — accepts a raw Request, performs prelude, delegates.
686/// pub async fn edit(__ferro_req: ::ferro::Request) -> ::ferro::Response {
687/// let mut __ferro_req = __ferro_req;
688/// let __resource_id: <Customer as ::ferro::TenantScoped>::Id =
689/// __ferro_req.param_as("id").map_err(|_| ::ferro::HttpResponse::new().status(400))?;
690/// let __tenant: ::ferro::TenantContext = ::ferro::current_tenant()
691/// .ok_or_else(|| ::ferro::HttpResponse::new().status(400))?;
692/// let __resource_opt = <Customer as ::ferro::TenantScoped>::find_for_tenant(
693/// __resource_id, __tenant.id,
694/// ).await.map_err(|_| ::ferro::HttpResponse::new().status(500))?;
695/// let __resource = match __resource_opt {
696/// Some(r) => r,
697/// None => return Err(::ferro::HttpResponse::new().status(302).header("Location", "/dashboard/clienti")),
698/// };
699/// __edit_inner(&mut __ferro_req, &__tenant, &__resource).await
700/// }
701///
702/// // Named inner fn — tenant and resource are real typed parameters; IDE jump-to-def works.
703/// async fn __edit_inner(
704/// req: &mut ::ferro::Request,
705/// tenant: &::ferro::TenantContext,
706/// customer: &Customer,
707/// ) -> ::ferro::Response {
708/// // user body here
709/// Ok(ferro::HttpResponse::new())
710/// }
711/// ```
712///
713/// # Security
714///
715/// The generated lookup always calls `TenantScoped::find_for_tenant(id, tenant.id)` —
716/// cross-tenant reads are structurally impossible through this macro. T-212-01.
717#[proc_macro_attribute]
718pub fn resource_get(attr: TokenStream, input: TokenStream) -> TokenStream {
719 resource_get::resource_get_impl(attr, input)
720}
721
722/// Attribute macro for POST handlers mutating a single tenant-scoped resource.
723///
724/// Folds the same prelude as `#[resource_get]` plus the validation-failure
725/// redirect envelope (via `handle_action_result`). Requires `redirect_to`.
726///
727/// # Required arguments
728///
729/// - First positional arg: the resource type implementing `TenantScoped`.
730/// - `redirect_to = "/url"` — default 303 redirect on success (and error fallback).
731///
732/// # Optional arguments
733///
734/// - `form_url = "/url/{id}/edit"` — the edit form URL, synthesized from extracted
735/// path params; injected as `__form_url: &str` in the inner fn body.
736/// - `on_miss = "/url"` — 303 redirect on lookup miss; omitted → 404 `HttpResponse`.
737/// - `tenant = "expr"` — escape-hatch expression for tenant resolution.
738/// - `find = "path::fn"` — override the lookup function.
739///
740/// # Example
741///
742/// ```ignore
743/// use ferro::{resource_post, ActionResult, Request, TenantContext};
744///
745/// #[resource_post(Customer,
746/// redirect_to = "/dashboard/clienti",
747/// form_url = "/dashboard/clienti/{id}/modifica")]
748/// pub async fn save(req: &mut Request, tenant: &TenantContext, customer: &Customer) -> ActionResult {
749/// let data = serde_json::json!({ "name": "test" });
750/// ferro::Validator::new(&data)
751/// .rules("name", ferro::rules![ferro::required()])
752/// .validate_or_redirect(__form_url)?;
753/// Ok(())
754/// }
755/// ```
756///
757/// # Expands to (abridged)
758///
759/// The attribute is equivalent to the following expansion (shown via `cargo expand`):
760///
761/// ```ignore
762/// // Generated outer fn — same prelude as resource_get, plus form_url synthesis and
763/// // validation-redirect envelope via handle_action_result.
764/// pub async fn save(__ferro_req: ::ferro::Request) -> ::ferro::Response {
765/// let mut __ferro_req = __ferro_req;
766/// let __resource_id: <Customer as ::ferro::TenantScoped>::Id =
767/// __ferro_req.param_as("id").map_err(|_| ::ferro::HttpResponse::new().status(400))?;
768/// let __tenant: ::ferro::TenantContext = ::ferro::current_tenant()
769/// .ok_or_else(|| ::ferro::HttpResponse::new().status(400))?;
770/// let __resource_opt = <Customer as ::ferro::TenantScoped>::find_for_tenant(
771/// __resource_id, __tenant.id,
772/// ).await.map_err(|_| ::ferro::HttpResponse::new().status(500))?;
773/// let __resource = match __resource_opt {
774/// Some(r) => r,
775/// None => return Err(::ferro::HttpResponse::new().status(404)),
776/// };
777/// let __form_url_owned = format!("/dashboard/clienti/{}/modifica", __resource_id);
778/// let __form_url: &str = &__form_url_owned;
779/// // Inner fn borrow ends before handle_action_result borrows __ferro_req again (Pitfall 3).
780/// let __action_result: ::ferro::ActionResult =
781/// __save_inner(&mut __ferro_req, &__tenant, &__resource, __form_url).await;
782/// ::ferro::http::action::handle_action_result(
783/// __action_result, "/dashboard/clienti", "module::save", &mut __ferro_req,
784/// )
785/// }
786///
787/// // Named inner fn — tenant, resource, and __form_url are real typed parameters.
788/// async fn __save_inner(
789/// req: &mut ::ferro::Request,
790/// tenant: &::ferro::TenantContext,
791/// customer: &Customer,
792/// __form_url: &str,
793/// ) -> ::ferro::ActionResult {
794/// // user body here
795/// Ok(())
796/// }
797/// ```
798///
799/// # Security
800///
801/// Same tenant-scoping guarantee as `#[resource_get]`: lookup always passes
802/// `tenant.id`. T-212-01.
803#[proc_macro_attribute]
804pub fn resource_post(attr: TokenStream, input: TokenStream) -> TokenStream {
805 resource_post::resource_post_impl(attr, input)
806}