// Sol Router Helpers
//
// Provides simplified APIs for defining routes, handling responses,
// and extracting parameters.
// ============================================================================
// Route Definition Helpers
// ============================================================================
///|
/// Create a page route with minimal boilerplate
///
/// Example:
/// ```moonbit nocheck
/// page("/", home, title="Home")
/// page("/blog/:slug", blog_post, title="Blog", revalidate=Some(60))
/// ```
pub fn page(
path : String,
handler : async (PageProps) -> @server_dom.ServerNode,
title? : String = "",
meta? : Array[(String, String)] = [],
revalidate? : Int? = None,
cache? : CacheStrategy? = None,
) -> SolRoutes {
SolRoutes::Page(
path~,
handler=PageHandler(handler),
title~,
meta~,
revalidate~,
cache~,
)
}
///|
fn make_api_handler[T : ToJson](
handler : async (PageProps) -> T,
) -> ApiHandler {
ApiHandler(async fn(props) { handler(props).to_json() })
}
///|
/// Create a GET API route
///
/// Example:
/// ```moonbit nocheck
/// api_get("/api/users/:id", get_user)
/// ```
pub fn api_get[T : ToJson](
path : String,
handler : async (PageProps) -> T,
) -> SolRoutes {
SolRoutes::Get(path~, handler=make_api_handler(handler))
}
///|
/// Create a POST API route
///
/// Example:
/// ```moonbit nocheck
/// api_post("/api/users", create_user)
/// ```
pub fn api_post[T : ToJson](
path : String,
handler : async (PageProps) -> T,
) -> SolRoutes {
SolRoutes::Post(path~, handler=make_api_handler(handler))
}
///|
/// Create a PUT API route
pub fn api_put[T : ToJson](
path : String,
handler : async (PageProps) -> T,
) -> SolRoutes {
SolRoutes::Put(path~, handler=make_api_handler(handler))
}
///|
/// Create a DELETE API route
pub fn api_delete[T : ToJson](
path : String,
handler : async (PageProps) -> T,
) -> SolRoutes {
SolRoutes::Delete(path~, handler=make_api_handler(handler))
}
///|
/// Create a PATCH API route
pub fn api_patch[T : ToJson](
path : String,
handler : async (PageProps) -> T,
) -> SolRoutes {
SolRoutes::Patch(path~, handler=make_api_handler(handler))
}
///|
/// Create a layout wrapper
///
/// Example:
/// ```moonbit nocheck
/// wrap("/admin", admin_layout, [
/// page("/", admin_dashboard),
/// page("/settings", admin_settings),
/// ])
/// ```
pub fn wrap(
segment : String,
layout : (PageProps, @server_dom.ServerNode) -> @server_dom.ServerNode raise Error,
children : Array[SolRoutes],
) -> SolRoutes {
SolRoutes::Layout(segment~, layout~, children~)
}
///|
/// Apply middleware to a group of routes
///
/// Example:
/// ```moonbit nocheck
/// with_mw([@mw.logger(), @mw.cors()], [
/// api_get("/api/data", get_data),
/// ])
/// ```
pub fn with_mw(
middleware : Array[@middleware.Middleware],
children : Array[SolRoutes],
) -> SolRoutes {
SolRoutes::WithMiddleware(middleware~, children~)
}
// ============================================================================
// API Response Builder
// ============================================================================
///|
/// Type-safe API response enum
///
/// Use with `.to_json()` to convert to `Json` for handler return.
pub(all) enum ApiResponse {
/// JSON data response
Json(Json)
/// Redirect response (url, status_code)
Redirect(String, Int)
/// Not found (404)
NotFound(String)
/// Error response (message, status_code)
Error(String, Int)
}
///|
/// Convert ApiResponse to Json for handler return
pub fn ApiResponse::to_json(self : ApiResponse) -> Json {
match self {
Json(data) => data
Redirect(url, status) =>
{
"redirect": url.to_json(),
"status": status.to_json(),
}
NotFound(msg) =>
{ "error": msg.to_json(), "status": (404 : Int).to_json() }
Error(msg, status) =>
{ "error": msg.to_json(), "status": status.to_json() }
}
}
///|
/// Create a JSON response from key-value pairs
///
/// Example:
/// ```moonbit nocheck
/// ok([("status", "ok".to_json())]).to_json()
/// ```
pub fn ok(pairs : Array[(String, Json)]) -> ApiResponse {
let obj : Map[String, Json] = {}
for pair in pairs {
obj[pair.0] = pair.1
}
ApiResponse::Json(obj.to_json())
}
///|
/// Create a redirect response (default 302)
pub fn redirect_to(url : String, status? : Int = 302) -> ApiResponse {
ApiResponse::Redirect(url, status)
}
///|
/// Create a permanent redirect (301)
pub fn redirect_permanent_to(url : String) -> ApiResponse {
ApiResponse::Redirect(url, 301)
}
///|
/// Create a 404 not found response
pub fn not_found_response(message? : String = "Not found") -> ApiResponse {
ApiResponse::NotFound(message)
}
///|
/// Create an error response
pub fn error_response(message : String, status? : Int = 500) -> ApiResponse {
ApiResponse::Error(message, status)
}
///|
/// Create a bad request (400) response
pub fn bad_request(message : String) -> ApiResponse {
ApiResponse::Error(message, 400)
}
///|
/// Create an unauthorized (401) response
pub fn unauthorized(message? : String = "Unauthorized") -> ApiResponse {
ApiResponse::Error(message, 401)
}
///|
/// Create a forbidden (403) response
pub fn forbidden(message? : String = "Forbidden") -> ApiResponse {
ApiResponse::Error(message, 403)
}
// ============================================================================
// Parameter Extraction Helpers
// ============================================================================
///|
/// Extract a required path parameter (delegates to RouteParams)
pub fn require(props : PageProps, name : String) -> Result[String, ApiResponse] {
props.params.require(name)
}
///|
/// Extract an optional path parameter with default value (delegates to RouteParams)
pub fn optional(props : PageProps, name : String, default : String) -> String {
props.params.optional(name, default)
}
///|
/// Extract and parse an integer parameter (delegates to RouteParams)
pub fn require_int(
props : PageProps,
name : String,
) -> Result[Int, ApiResponse] {
props.params.require_int(name)
}
///|
/// Extract a required query parameter (delegates to RouteParams)
pub fn require_query(
props : PageProps,
name : String,
) -> Result[String, ApiResponse] {
props.params.require_query(name)
}
///|
/// Extract an optional query parameter with default value (delegates to RouteParams)
pub fn optional_query(
props : PageProps,
name : String,
default : String,
) -> String {
props.params.optional_query(name, default)
}
// ============================================================================
// ServerNode Helpers
// ============================================================================
///|
/// Create a sync ServerNode from an array of nodes
///
/// Example:
/// ```moonbit nocheck
/// nodes([
/// @element.h1([@element.text("Hello")]),
/// @element.p([@element.text("World")]),
/// ])
/// ```
pub fn nodes(
content : Array[@luna.Node[Unit, String]],
) -> @server_dom.ServerNode {
@server_dom.ServerNode::sync(@luna.fragment(content))
}