Skip to main content

resource_post

Attribute Macro resource_post 

Source
#[resource_post]
Expand description

Attribute macro for POST handlers mutating a single tenant-scoped resource.

Folds the same prelude as #[resource_get] plus the validation-failure redirect envelope (via handle_action_result). Requires redirect_to.

§Required arguments

  • First positional arg: the resource type implementing TenantScoped.
  • redirect_to = "/url" — default 303 redirect on success (and error fallback).

§Optional arguments

  • form_url = "/url/{id}/edit" — the edit form URL, synthesized from extracted path params; injected as __form_url: &str in the inner fn body.
  • on_miss = "/url" — 303 redirect on lookup miss; omitted → 404 HttpResponse.
  • tenant = "expr" — escape-hatch expression for tenant resolution.
  • find = "path::fn" — override the lookup function.

§Example

use ferro::{resource_post, ActionResult, Request, TenantContext};

#[resource_post(Customer,
    redirect_to = "/dashboard/clienti",
    form_url = "/dashboard/clienti/{id}/modifica")]
pub async fn save(req: &mut Request, tenant: &TenantContext, customer: &Customer) -> ActionResult {
    let data = serde_json::json!({ "name": "test" });
    ferro::Validator::new(&data)
        .rules("name", ferro::rules![ferro::required()])
        .validate_or_redirect(__form_url)?;
    Ok(())
}

§Expands to (abridged)

The attribute is equivalent to the following expansion (shown via cargo expand):

// Generated outer fn — same prelude as resource_get, plus form_url synthesis and
// validation-redirect envelope via handle_action_result.
pub async fn save(__ferro_req: ::ferro::Request) -> ::ferro::Response {
    let mut __ferro_req = __ferro_req;
    let __resource_id: <Customer as ::ferro::TenantScoped>::Id =
        __ferro_req.param_as("id").map_err(|_| ::ferro::HttpResponse::new().status(400))?;
    let __tenant: ::ferro::TenantContext = ::ferro::current_tenant()
        .ok_or_else(|| ::ferro::HttpResponse::new().status(400))?;
    let __resource_opt = <Customer as ::ferro::TenantScoped>::find_for_tenant(
        __resource_id, __tenant.id,
    ).await.map_err(|_| ::ferro::HttpResponse::new().status(500))?;
    let __resource = match __resource_opt {
        Some(r) => r,
        None => return Err(::ferro::HttpResponse::new().status(404)),
    };
    let __form_url_owned = format!("/dashboard/clienti/{}/modifica", __resource_id);
    let __form_url: &str = &__form_url_owned;
    // Inner fn borrow ends before handle_action_result borrows __ferro_req again (Pitfall 3).
    let __action_result: ::ferro::ActionResult =
        __save_inner(&mut __ferro_req, &__tenant, &__resource, __form_url).await;
    ::ferro::http::action::handle_action_result(
        __action_result, "/dashboard/clienti", "module::save", &mut __ferro_req,
    )
}

// Named inner fn — tenant, resource, and __form_url are real typed parameters.
async fn __save_inner(
    req: &mut ::ferro::Request,
    tenant: &::ferro::TenantContext,
    customer: &Customer,
    __form_url: &str,
) -> ::ferro::ActionResult {
    // user body here
    Ok(())
}

§Security

Same tenant-scoping guarantee as #[resource_get]: lookup always passes tenant.id. T-212-01.