// Server Action Types
//
// Server Actions are secure server-side functions that can be called from the client.
// They automatically include CSRF protection and type-safe serialization.
// ============================================================================
// Action Handler Types
// ============================================================================
///|
/// Action handler context - provides request info and utilities
pub struct ActionContext {
/// Mars Context
ctx : @mars.Context
/// Raw request body (JSON string)
body : String
}
///|
/// Create ActionContext from Mars context
pub fn ActionContext::from_ctx(
ctx : @mars.Context,
body : String,
) -> ActionContext {
{ ctx, body }
}
///|
/// Decode an action request body as typed JSON.
pub fn[T : FromJson] decode_action_body(body : String) -> T? {
let json : Json = @json.parse(body.view()) catch { _ => return None }
let value : T = @json.from_json(json) catch { _ => return None }
Some(value)
}
///|
/// Decode this action request body as typed JSON.
pub fn[T : FromJson] ActionContext::decode_json(self : ActionContext) -> T? {
decode_action_body(self.body)
}
///|
/// Get header value (case-insensitive)
pub fn ActionContext::get_header(
self : ActionContext,
name : String,
) -> String? {
self.ctx.header(name)
}
///|
/// Get query parameter
pub fn ActionContext::get_query(self : ActionContext, name : String) -> String? {
self.ctx.query(name)
}
///|
/// Get state value
pub fn ActionContext::get_state(self : ActionContext, key : String) -> String? {
self.ctx.get(key)
}
///|
/// Set state value
pub fn ActionContext::set_state(
self : ActionContext,
key : String,
value : String,
) -> Unit {
self.ctx.set(key, value)
}
// ============================================================================
// Action Result Types
// ============================================================================
///|
/// Internal action result representation.
priv enum ActionResultKind {
JsonSuccess(Json)
ClientRedirect(String)
HttpRedirectResult(String)
ClientFailure(Int, String)
ServerFailure(String)
}
///|
/// Action result returned by server action handlers.
pub struct ActionResult {
priv kind : ActionResultKind
}
///|
/// Create success result from a typed JSON-serializable payload.
pub fn[T : ToJson] ActionResult::json(data : T) -> ActionResult {
{ kind: JsonSuccess(data.to_json()) }
}
///|
/// Create redirect result (returns JSON for JavaScript clients)
pub fn ActionResult::redirect(url : String) -> ActionResult {
{ kind: ClientRedirect(url) }
}
///|
/// Create HTTP redirect result (returns 302 with Location header)
/// Use this for non-JavaScript form submissions (progressive enhancement)
pub fn ActionResult::http_redirect(url : String) -> ActionResult {
{ kind: HttpRedirectResult(url) }
}
///|
/// Create bad request error
pub fn ActionResult::bad_request(message : String) -> ActionResult {
{ kind: ClientFailure(400, message) }
}
///|
/// Create validation error
pub fn ActionResult::validation_error(message : String) -> ActionResult {
{ kind: ClientFailure(422, message) }
}
///|
/// Create unauthorized error
pub fn ActionResult::unauthorized(message? : String) -> ActionResult {
{ kind: ClientFailure(401, message.unwrap_or("Unauthorized")) }
}
///|
/// Create forbidden error
pub fn ActionResult::forbidden(message? : String) -> ActionResult {
{ kind: ClientFailure(403, message.unwrap_or("Forbidden")) }
}
///|
/// Create not found error
pub fn ActionResult::not_found(message? : String) -> ActionResult {
{ kind: ClientFailure(404, message.unwrap_or("Not Found")) }
}
///|
/// Create server error
pub fn ActionResult::server_error(message? : String) -> ActionResult {
{ kind: ServerFailure(message.unwrap_or("Internal Server Error")) }
}
// ============================================================================
// Action Handler
// ============================================================================
///|
/// Action handler type - async function that receives ActionContext and returns ActionResult
pub(all) struct ActionHandler(async (ActionContext) -> ActionResult raise Error)
// ============================================================================
// ActionKey (generated typed action identifiers)
// ============================================================================
///|
/// Action key implemented by generated marker types.
pub(open) trait ActionKey {
/// Stable action identifier used in the HTTP endpoint.
id(Self) -> String
/// Base path for action endpoints.
base_path(Self) -> String
}
///|
/// Phantom-typed action key for generated request/response contracts.
pub struct TypedActionKey[Req, Res] {
id : String
base_path : String
_request : Req?
_response : Res?
}
///|
fn[Req, Res] TypedActionKey::new(
id : String,
base_path? : String = "/_action",
) -> TypedActionKey[Req, Res] {
{ id, base_path, _request: None, _response: None }
}
///|
pub fn[Req, Res, K : ActionKey] TypedActionKey::from_key(
key : K,
) -> TypedActionKey[Req, Res] {
TypedActionKey::new(key.id(), base_path=key.base_path())
}
///|
pub fn[Req, Res] TypedActionKey::id(self : TypedActionKey[Req, Res]) -> String {
self.id
}
///|
pub fn[Req, Res] TypedActionKey::base_path(
self : TypedActionKey[Req, Res],
) -> String {
self.base_path
}
///|
pub impl[Req, Res] ActionKey for TypedActionKey[Req, Res] with id(self) {
self.id
}
///|
pub impl[Req, Res] ActionKey for TypedActionKey[Req, Res] with base_path(self) {
self.base_path
}
///|
pub fn[Req, Res] typed_action_url(key : TypedActionKey[Req, Res]) -> String {
key.base_path() + "/" + key.id()
}
///|
/// Get the stable action identifier from a generated action key.
pub fn[K : ActionKey] action_id(key : K) -> String {
key.id()
}
///|
/// Get the full endpoint URL from a generated action key.
pub fn[K : ActionKey] action_url(key : K) -> String {
key.base_path() + "/" + key.id()
}
// ============================================================================
// Action Definition
// ============================================================================
///|
/// Server Action definition
pub struct ActionDef {
/// Action identifier (should be cryptographically secure in production)
priv id : String
/// Action handler
priv handler : ActionHandler
/// Additional middlewares for this action
priv middlewares : Array[@middleware.Middleware]
/// Whether to require JSON Content-Type
priv require_json : Bool
}
///|
/// Create action definition from a generated action key.
pub fn[K : ActionKey] ActionDef::from_key(
key : K,
handler : ActionHandler,
) -> ActionDef {
{ id: key.id(), handler, middlewares: [], require_json: true }
}
///|
/// Add middleware to action
pub fn ActionDef::with_middleware(
self : ActionDef,
mw : @middleware.Middleware,
) -> ActionDef {
let middlewares = self.middlewares.copy()
middlewares.push(mw)
{ ..self, middlewares, }
}
///|
/// Add multiple middlewares to action
pub fn ActionDef::with_middlewares(
self : ActionDef,
mws : Array[@middleware.Middleware],
) -> ActionDef {
let middlewares = self.middlewares.copy()
for mw in mws {
middlewares.push(mw)
}
{ ..self, middlewares, }
}
///|
/// Set JSON requirement
pub fn ActionDef::with_require_json(
self : ActionDef,
require : Bool,
) -> ActionDef {
{ ..self, require_json: require }
}
// ============================================================================
// Action Registry
// ============================================================================
///|
/// Registry for server actions
pub struct ActionRegistry {
/// Action definitions by ID
priv actions : Map[String, ActionDef]
/// Base path for action endpoints (default: "/_action")
priv base_path : String
/// Allowed origins for CSRF protection
priv allowed_origins : Array[String]
}
///|
/// Create new action registry
pub fn ActionRegistry::new(allowed_origins? : Array[String]) -> ActionRegistry {
{
actions: {},
base_path: "/_action",
allowed_origins: allowed_origins.unwrap_or([]),
}
}
///|
/// Set base path for action endpoints
pub fn ActionRegistry::with_base_path(
self : ActionRegistry,
base_path : String,
) -> ActionRegistry {
{ ..self, base_path, }
}
///|
/// Register an action
pub fn ActionRegistry::register(
self : ActionRegistry,
action : ActionDef,
) -> ActionRegistry {
let actions = self.actions
actions[action.id] = action
{ ..self, actions, }
}
///|
/// Register an action from a generated action key and handler
pub fn[K : ActionKey] ActionRegistry::register_key(
self : ActionRegistry,
key : K,
handler : ActionHandler,
) -> ActionRegistry {
self.register(ActionDef::from_key(key, handler))
}
///|
/// Register multiple actions
pub fn ActionRegistry::register_all(
self : ActionRegistry,
defs : Array[ActionDef],
) -> ActionRegistry {
let actions = self.actions
for def in defs {
actions[def.id] = def
}
{ ..self, actions, }
}
///|
/// Get action by ID
fn ActionRegistry::get(self : ActionRegistry, id : String) -> ActionDef? {
self.actions.get(id)
}
///|
/// Get all action IDs
pub fn ActionRegistry::action_ids(self : ActionRegistry) -> Array[String] {
self.actions.keys().collect()
}
///|
/// Get action endpoint URL
fn ActionRegistry::action_url(self : ActionRegistry, id : String) -> String {
self.base_path + "/" + id
}