///|
/// moonfig 示例:配置校验
///
/// 运行:`moon run examples/validate`
///
/// 场景:服务 `orders` 的配置写错了——端口越界、日志级别拼错、还多了一个键。
/// 校验放在启动阶段,问题一次暴露,不用等业务代码跑到一半才发现。
///
/// 两条路,共用同一套规则和错误类型:
///
/// 1. 按键取值:没有结构体声明,规则显式列出路径和约束。
/// 2. 提取到结构体:约束写在 `Validate` 实现里,提取后立刻跑。

///|
using @config {
  type Config,
  type MemorySource,
  type Rule,
  type Value,
  type ValueKind,
}

///|
using @jsonfmt {extract_validated_from}

///|
/// 故意写坏的配置:端口越界、用户名空、日志级别拼错、多了一个 `extra`
fn build_config() -> Config raise {
  Config::new()
  .add(
    MemorySource::new(
      Value::table({
        "db": Value::table({
          "host": Value::str("localhost"),
          "port": Value::int(70000L),
          "user": Value::str(""),
          "extra": Value::bool(true),
        }),
        "log": Value::table({ "level": Value::str("verbose") }),
      }),
    ),
  )
  .build()
}

///|
/// 按键校验的规则表:路径、约束、失败时怎么描述,都写在一处
fn db_rules() -> Array[Rule] {
  [
    Rule::int_range("db.port", 1L, 65535L),
    Rule::string_len("db.user", 1, 64),
    Rule::one_of("log.level", ["debug", "info", "warn", "error"]),
    Rule::allowed_keys("db", ["host", "port", "user"]),
  ]
}

///|
/// 第一种用法:按键校验,遇到第一个错误就停
///
/// 和 `get_*` 一样,先报错先停;要一次列全用 `validate_all`。
fn validate_by_key(cfg : Config) -> Unit raise {
  cfg.validate(db_rules())
}

///|
/// 第二种用法:一次列出全部问题
fn show_all_errors(cfg : Config) -> Unit raise {
  let errors = cfg.validate_all(db_rules())
  if errors.is_empty() {
    println("passed")
  } else {
    for error in errors {
      println(error.to_string())
    }
  }
}

///|
/// 结构体:字段和配置一一对应
struct DbCfg {
  host : String
  port : Int32
  user : String
} derive(@json.FromJson)

///|
pub extend DbCfg with @json.FromJson::{from_json}

///|
/// 约束写在结构体旁边:字段改了,约束跟着改
///
/// `prefix` 是提取时的起始路径(这里是 `db`),拼上字段名就是完整的键路径,
/// 报错时和按键校验给出同样的消息。
impl @config.Validate for DbCfg with fn validate(
  self : DbCfg,
  prefix : @config.KeyPath,
) -> Unit raise @config.ConfigError {
  @config.check_int_range(prefix.add("port"), self.port.to_int64(), 1L, 65535L)
  @config.check_string_len(prefix.add("user"), self.user, 1, 64)
}

///|
/// 第三种用法:先提取成结构体,再跑结构体自己的约束
///
/// 未知键不在这条路上:`derive(FromJson)` 会忽略结构体里没有的字段,
/// 它不知道自己外面多了什么。未知键只能按键查,见 `db_rules` 里的 `allowed_keys`。
fn validate_by_struct(cfg : Config) -> Unit raise {
  let db : DbCfg = extract_validated_from(cfg, "db")
  println("db.host = \{db.host}")
}

///|
/// 内置规则不够用时自己写:返回 `None` 表示通过,`Some(实际值)` 表示失败
///
/// `实际值` 会进错误消息,密码这类字段别原样返回,只写长度或类型。
fn validate_host(cfg : Config) -> Unit raise {
  cfg.validate([
    Rule::custom("db.host", "non-empty and without spaces", fn(value) {
      match value.kind {
        ValueKind::Str(text) =>
          if text.length() > 0 && !text.contains(" ") {
            None
          } else {
            Some(text)
          }
        _ => Some(value.kind.name())
      }
    }),
  ])
}

///|
fn main raise {
  let cfg = build_config()

  println("== 1. Validate by key: stop at the first error ==")
  try {
    validate_by_key(cfg)
    println("passed")
  } catch {
    error => println(error.to_string())
  }

  println("")
  println("== 2. Validate by key: list every error ==")
  show_all_errors(cfg)

  println("")
  println("== 3. Extract into a struct: constraints follow the fields ==")
  validate_by_struct(cfg) catch {
    error => println(error.to_string())
  }

  println("")
  println("== 4. Custom rules ==")
  try {
    validate_host(cfg)
    println("passed")
  } catch {
    error => println(error.to_string())
  }
}