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