///|
/// Request binding — extract and validate data from requests.
///
/// inspired by `ShouldBindJSON` / `ShouldBindQuery` but adapted
/// for MoonBit's type system.
///| ——————————————————————————————————————————————————————————————————————
/// JSON body binding
///| ——————————————————————————————————————————————————————————————————————
///|
/// Bind JSON body to a struct that implements `from_json`.
/// Returns `Some(value)` on success, `None` on parse failure.
///
/// ```
/// struct LoginRequest {
/// username : String
/// password : String
/// } derive(FromJson)
///
/// let req : LoginRequest? = bind_json(ctx)
/// ```
pub async fn[T : @json.FromJson] bind_json(ctx : Context) -> T? {
let json = ctx.body_json()
match json {
Some(j) =>
try { Some(@json.from_json(j)) } catch { _ => None }
None => None
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Query string binding
///| ——————————————————————————————————————————————————————————————————————
///|
/// Bind query parameters to a struct that implements `from_json`.
/// Converts query string to a JSON object first.
pub fn[T : @json.FromJson] bind_query(ctx : Context) -> T? {
// Build a JSON object from query parameters
let obj : Map[String, Json] = Map([])
let qs = ctx.full_path()
match qs.find("?") {
Some(pos) => {
let raw = qs[pos + 1:].to_owned()
let pairs = raw.split("&")
for pair in pairs {
match pair.find("=") {
Some(i) => {
let k = url_decode(pair[:i].to_owned())
let v = url_decode(pair[i + 1:].to_owned())
obj.set(k, Json::string(v))
}
None => {
let k = url_decode(pair.to_owned())
obj.set(k, Json::string(""))
}
}
}
}
None => ()
}
let json = Json::object(obj)
try { Some(@json.from_json(json)) } catch { _ => None }
}
///| ——————————————————————————————————————————————————————————————————————
/// Form binding (application/x-www-form-urlencoded)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Bind form-encoded body to a struct that implements `from_json`.
/// Returns `Some(value)` on success, `None` on parse failure.
///
/// ```
/// struct ContactForm {
/// name : String
/// email : String
/// message : String
/// } derive(FromJson)
///
/// let form : ContactForm? = bind_form(ctx)
/// ```
pub async fn[T : @json.FromJson] bind_form(ctx : Context) -> T? {
let raw = ctx.body_string()
if raw == "" {
return None
}
let obj : Map[String, Json] = Map([])
let pairs = raw.split("&")
for pair in pairs {
match pair.find("=") {
Some(i) => {
let k = url_decode(pair[:i].to_owned())
let v = url_decode(pair[i + 1:].to_owned())
obj.set(k, Json::string(v))
}
None => {
let k = url_decode(pair.to_owned())
obj.set(k, Json::string(""))
}
}
}
let json = Json::object(obj)
try { Some(@json.from_json(json)) } catch { _ => None }
}
///| ——————————————————————————————————————————————————————————————————————
/// Header binding
///| ——————————————————————————————————————————————————————————————————————
///|
/// Bind request headers to a struct that implements `from_json`.
/// Header names are normalized to lowercase.
///
/// ```
/// struct Headers {
/// content_type : String
/// authorization : String
/// } derive(FromJson)
///
/// let hdrs : Headers? = bind_header(ctx)
/// ```
pub fn[T : @json.FromJson] bind_header(ctx : Context) -> T? {
let obj : Map[String, Json] = Map([])
// We extract common headers manually since we don't have
// a full headers iterator on the native request
let common_headers = [
"content-type", "accept", "authorization", "user-agent", "host", "origin",
"referer", "x-forwarded-for", "x-real-ip", "x-request-id", "content-length",
"cache-control", "cookie",
]
for name in common_headers {
match ctx.header(name) {
Some(v) => obj.set(name, Json::string(v))
None => ()
}
}
let json = Json::object(obj)
try { Some(@json.from_json(json)) } catch { _ => None }
}
///| ——————————————————————————————————————————————————————————————————————
/// Path parameter binding
///| ——————————————————————————————————————————————————————————————————————
///|
/// Bind path parameters to a struct that implements `from_json`.
///
/// ```
/// struct ArticleParams {
/// id : String
/// slug : String
/// } derive(FromJson)
///
/// // Route: /article/:id/:slug
/// let params : ArticleParams? = bind_path(ctx)
/// ```
pub fn[T : @json.FromJson] bind_path(ctx : Context) -> T? {
let obj : Map[String, Json] = Map([])
let all_params = ctx.params_all()
for k, v in all_params {
obj.set(k, Json::string(v))
}
let json = Json::object(obj)
try { Some(@json.from_json(json)) } catch { _ => None }
}
///| ——————————————————————————————————————————————————————————————————————
/// Combined binding
///| ——————————————————————————————————————————————————————————————————————
///|
/// Try to bind from JSON body first, then fall back to query string.
pub async fn[T : @json.FromJson] bind(ctx : Context) -> T? {
// Try JSON first
let json_result : T? = bind_json(ctx)
match json_result {
Some(v) => return Some(v)
None => ()
}
// Fall back to query string
bind_query(ctx)
}
///|
/// Bind URI (path) parameters — alias for `bind_path`.
pub fn[T : @json.FromJson] bind_uri(ctx : Context) -> T? {
bind_path(ctx)
}
///| ——————————————————————————————————————————————————————————————————————
/// Validation
///| ——————————————————————————————————————————————————————————————————————
///|
/// Internal holder for the bind-validation flag.
priv struct BindValidationBox {
mut enabled : Bool
}
///|
/// Whether automatic validation is enabled.
let bind_validation_box : BindValidationBox = { enabled: true }
///|
/// Disable automatic validation during binding.
pub fn disable_bind_validation() -> Unit {
bind_validation_box.enabled = false
}
///|
/// Enable automatic validation during binding.
pub fn enable_bind_validation() -> Unit {
bind_validation_box.enabled = true
}
///|
/// Check if bind validation is enabled.
pub fn is_bind_validation_enabled() -> Bool {
bind_validation_box.enabled
}
///|
/// Validate a value and return the first error message, or None if valid.
/// Override this in your types for custom validation.
pub fn[T] validate(_value : T) -> String? {
None
}
///|
/// Bind is a standalone function that auto-detects content type and binds.
/// Auto-detect content type and bind - alias.
pub async fn[T : @json.FromJson] gin_bind(ctx : Context, _obj : T) -> T? {
ctx.bind_api()
}