///|
/// JSON 解码助手,以及分页响应的统一形状。
///
/// OpenList 有两个与 MoonBit core json 不一致的地方,这里统一处理:
///
/// 1. 可选字段既可能「缺失」也可能显式写成 `null`,而 MoonBit 的 `T?` 字段只在
/// 字段缺失时给出 `None`、显式 `null` 会让内层类型解码失败。因此带类型化
/// 解码的通道统一先过一遍 `strip_nulls`:把对象里所有 `null` 字段删掉,
/// 于是 `T?` 字段的语义就变得干净(有值 → `Some`,没有值 → `None`)。
/// 2. Go 的 `int64`/`uint64` 会被序列化成 JSON **数字**,而 core json 的
/// `Int64::from_json` 只接受字符串形态(JSON 数字统一按 `Double` 解析)。
/// 所以带 64 位整数字段的模型手写 `FromJson`,用 `int64_field` 读它们。
///
/// `request` 返回原始 `Json` 的那条逃生通道**不**做剔除,保证用户看到的
/// 是服务端原样返回的数据。
///|
/// 递归剔除 JSON 对象里值为 `null` 的字段;数组元素原样保留但会递归处理。
pub fn strip_nulls(json : Json) -> Json {
match json {
Object(fields) => {
let result : Map[String, Json] = Map([])
for key, value in fields {
match value {
Null => ()
_ => result[key] = strip_nulls(value)
}
}
Json::object(result)
}
Array(items) => Json::array(items.map(strip_nulls))
other => other
}
}
///|
/// 读一个字段并按目标类型解码;字段缺失、显式 `null` 或类型不符时给 `None`。
///
/// 手写 `FromJson` 的模型用它读普通字段,避免为每个字段重复一遍 try/catch。
pub fn[T : @json.FromJson] json_field(
fields : Map[String, Json],
key : String,
) -> T? {
match fields.get(key) {
Some(value) => Some(@json.from_json(value)) catch { _ => None }
None => None
}
}
///|
/// 字符串字段,缺失时给空串。
pub fn string_field(fields : Map[String, Json], key : String) -> String {
json_field(fields, key).unwrap_or("")
}
///|
/// 布尔字段,缺失时给 `false`。
pub fn bool_field(fields : Map[String, Json], key : String) -> Bool {
json_field(fields, key).unwrap_or(false)
}
///|
/// 32 位整数字段(`role`、`order`、`type` 这类小整数),缺失时给 0。
pub fn int_field(fields : Map[String, Json], key : String) -> Int {
json_field(fields, key).unwrap_or(0)
}
///|
/// 32 位无符号整数字段(各类 ID),缺失时给 0。
pub fn uint_field(fields : Map[String, Json], key : String) -> UInt {
json_field(fields, key).unwrap_or(0U)
}
///|
/// 浮点字段(进度百分比这类),缺失时给 0。
pub fn double_field(fields : Map[String, Json], key : String) -> Double {
json_field(fields, key).unwrap_or(0.0)
}
///|
/// 数组字段;缺失或 `null` 时给空数组。
///
/// Go 会把值为 `nil` 的切片序列化成 `null`(而不是 `[]`),所以模型里的
/// 列表字段一律走这里,用户拿到的永远是数组、不用处理 `null`。
pub fn[T : @json.FromJson] array_field(
fields : Map[String, Json],
key : String,
) -> Array[T] {
json_field(fields, key).unwrap_or([])
}
///|
/// 字符串映射字段(哈希值表这类);缺失或 `null` 时给空映射。
pub fn[V : @json.FromJson] map_field(
fields : Map[String, Json],
key : String,
) -> Map[String, V] {
json_field(fields, key).unwrap_or(Map([]))
}
///|
/// 64 位整数字段(文件大小、分页总数这类),缺失时给 0。
pub fn int64_field(fields : Map[String, Json], key : String) -> Int64 {
match fields.get(key) {
Some(value) => int64_from_json(value).unwrap_or(0L)
None => 0L
}
}
///|
/// 把一个 JSON 值读成 `Int64`:接受数字(OpenList 的 `int64` 就是数字),
/// 也接受字符串形态(core json 的 `Int64` 表示法)。
pub fn int64_from_json(value : Json) -> Int64? {
match value {
Json::Number(number, ..) => Some(number.to_int64())
Json::String(text) =>
Some(@json.from_json(Json::string(text))) catch {
_ => None
}
_ => None
}
}
///|
/// 把统一信封里的 `data` 解码成目标类型(解码前先剔除 `null` 字段)。
///
/// 解码失败会抛 `OpenListError::Decode`,其中带上 core json 给出的
/// 字段路径与原因,便于定位是哪个接口的哪个字段与模型不符。
pub fn[T : @json.FromJson] decode_data(json : Json) -> T raise OpenListError {
@json.from_json(strip_nulls(json)) catch {
@json.JsonDecodeError((_, message)) => raise OpenListError::Decode(message)
}
}
///|
/// OpenList 分页接口的统一结果。
pub struct PageResult[T] {
/// 当前页的元素。
content : Array[T]
/// 符合条件的记录总数(不是当前页长度)。
total : Int64
}
///|
/// 创建分页结果。
pub fn[T] PageResult::new(content : Array[T], total : Int64) -> PageResult[T] {
{ content, total, }
}
///|
/// 从 `{content, total}` 形态的 JSON 解码分页结果。
///
/// 两个字段都容错:缺失时退化为空列表 / 0,这样服务端某个版本少给一个
/// 字段也不会让整次请求失败。
pub impl[T : @json.FromJson] @json.FromJson for PageResult[T] with fn from_json(
json,
path,
) {
guard json is Object(fields) else {
raise @json.JsonDecodeError(
(path, "PageResult::from_json: expected object"),
)
}
let content : Array[T] = json_field(fields, "content").unwrap_or([])
let total = int64_field(fields, "total")
{ content, total, }
}
///|
/// 显式声明手写的 `FromJson` 实现以普通方法暴露,避免工具链的
/// 「隐式提升为方法」弃用告警(行为与 `derive` 完全一致)。
pub extend PageResult with @json.FromJson::{from_json}