﻿# moonfig 路线图

记录「按什么顺序做、每部分做完能验收什么」，不是契约，可以随时改。

设计细节见 [docs/design.md](design.md)。

状态分三档：✅ 已完成（进入维护）、🚧 进行中、⬜ 未进行。

---

## 核心　🚧

**方向**：配置只在内存里存在——统一表示、按键合并、按 key 取值。不接触任何外部环境，因此在所有 target 上都能编译。

- [x] `Value` / `ValueKind`：一份表示覆盖所有格式的配置值
- [x] 点号路径取值
- [x] 合并规则：按键合并、数组整体替换
- [x] `Config` 构建器与 `get_string` / `get_int` / `get_bool` / `get_float`
- [x] 错误处理：`MissingKey` / `TypeMismatch` / `ParseError`
- [x] 按结构体取值（`derive(FromJson)`）
- [ ] 来源追踪：某个键最终来自哪一层

**为什么还是进行中**：来源追踪要么现在就把来源带进 `Value`，越晚定代价越大——事后补要改 `Value` 的定义（每个值多带一个来源），而 `Value` 是所有东西的地基。

---

## 多格式　🚧

**方向**：把各种格式的配置读成同一份 `Value`。格式之间可以自由替换，声明不用改。

- [x] JSON 文本（`core/source/json_source/` 包）
- [x] TOML（`core/source/toml_source/` 包，依赖 `moonbit-community/toml`）
- [x] YAML（`core/source/yaml_source/` 包，依赖 `moonbit-community/yaml`）
- [x] `.env` 文本（`core/source/dotenv_source/` 包）
- [x] INI（`core/source/ini_source/` 包）
- [x] XML（`core/source/xml_source/` 包，依赖 `Milky2018/xml`）
- [ ] CBOR —— 嵌入式 / 物联网领域

每加一种格式只做一件事：格式 → `Value`。合并、取值、校验都复用核心，不为格式写第二套逻辑。

**格式说的是内容怎么写，来源说的是从哪读。** `.env` 文件是一种格式，环境变量是一种来源，两件事分开做：`EnvSource` 读环境变量时不需要知道它最后变成哪种格式。

**CBOR 是二进制格式，体积小，MCU 上常见。** 它也是检验 `Value` 表示能力的好例子：CBOR 的整数、字符串、数组、映射都是明确标类型的，`Value` 里的六个分支正好对得上，不用为它加特例。

**代价**：YAML、TOML 和 XML 依赖第三方解析器（`moonbit-community/yaml`、`moonbit-community/toml`、`Milky2018/xml`），CBOR 还没有现成实现。所以每种格式单独一个包，`moon.pkg` 写各自的 `import`，核心和已完成的 `json_source`、`ini_source`、`dotenv_source` 不受影响。

**JSON、INI 和 `.env` 都是自己写的解析器，没有第三方依赖。** 三者都只用 core 的包：JSON 走 `@json`，INI 和 `.env` 走 `@string` 里的 `parse_int64` / `parse_double`。INI 的类型猜法写在包的文档注释里：段落变层级、引号强制字符串、`;` 和 `#` 只在整行开头算注释；`.env` 的规则也写在包文档里：`export` 前缀可选、键名里的 `.` 变层级、单引号原样保留、双引号认转义、`#` 前面有空白才算行尾注释、不做变量展开。

**YAML 的语法交给 `moonbit-community/yaml`，本库只做 `Yaml` → `Value` 的转换。** 包文档写明了边界：只接受一个文档，空文件、空文档、只有注释的文件以及根节点为 `null` 的文件得到空表；根节点通常应是映射；整数超出 `Int64` 报错，不静默降级成浮点。

**TOML 的语法交给 `moonbit-community/toml`，本库只做 `TomlValue` → `Value` 的转换。** 包文档写明了边界：根节点必须是一张表；数组里混着不同类型时报错（TOML 要求同类型数组）；日期时间在 `Value` 里没有对应形状，按原文当字符串存；大整数保持 `Int64`。

**XML 的语法交给 `Milky2018/xml`，本库只做元素树 → `Value` 的转换。** XML 没有「键值」这回事，映射规则是本库定的，写在包文档里：根元素只是壳，子元素落在顶层；同名兄弟元素收成数组；属性在 `@` 前缀的键上；混合内容在 `#text` 上；CDATA 是字面文字，一定是字符串（这也是 XML 本身给「我要字符串」的办法）。

---

## 外部来源　⬜

**方向**：把外部世界读成配置。IO 只发生在这里，核心那一层看不到文件和环境变量。

- [ ] `FileSource`：读配置文件，格式交给上一层判断
- [ ] `EnvSource`：读环境变量
- [ ] 多个来源任意组合，优先级由加入顺序决定

### 环境变量是这一段里坑最多的

环境变量全是字符串：

- **类型推断必须在这一步做完**（`"5432"` → `Int`，`"true"` → `Bool`）。等到按结构体取值那步就晚了——core 的 `Bool::from_json` 和 `Double::from_json` 都拒收字符串。
- 分隔符约定要定：`_` 变 `.`？大小写敏感？
- 前缀、空值、列表值怎么处理。

---

## 校验　⬜

**方向**：配置读进来先验一遍，问题在启动时暴露，而不是在业务代码跑了一半才发现。

- [ ] 必填项缺失
- [ ] 取值范围校验（端口范围、字符串长度上限）
- [ ] 未知键是否报错，做成开关
- [ ] 校验规则由用户自己写
- [ ] 校验失败给出键名、期望值、实际值

---

## 自动发现格式　⬜

**方向**：给一个文件，不指定格式也能读进来。

- [ ] 按后缀判断（`.json` / `.toml` / `.yaml`）
- [ ] 后缀缺失或不可靠时按内容判断
- [ ] 判断失败时报错，并列出支持的格式
- [ ] 解析失败时给出位置和原因

---

## 热更新　⬜

**方向**：配置文件变了就重新生效，不必重启进程。

- [ ] 监听文件变化
- [ ] 重新读取并合并
- [ ] 通过回调通知使用方
- [ ] 新配置解析失败或校验不通过时保留旧配置，不让坏配置生效

---

## 依赖关系

```
核心 ──→ 多格式 ──→ 外部来源 ──→ 自动发现格式 ──→ 热更新
  │
  └──→ 校验
```

「校验」和「多格式」互不阻塞，可以先做任意一个。「自动发现格式」要等格式多于一种才有意义。

---

## 实测记录

| 实测 | 结果 | 影响 |
|---|---|---|
| `@json.parse("9007199254740993")` | `Number(9007199254740992, repr=Some(9007199254740993))` | 超出精确整数范围时 `repr` 留着原文 |
| `@json.parse("30")`、`@json.parse("1.5")` | `repr` 均为 `None` | 常见情况拿不到原文，只能按值判断整数／浮点（详见 [docs/design.md 第 9 节](design.md#9-已实测的坑)更正） |
| `from_json` 到 `Double`，输入字符串 `"1.5"` | 失败 `Double::from_json: expected number` | 字符串自动转数字在 Json 层做不到 |
| `from_json` 到 `Bool`，输入字符串 `"true"` | 失败 `Bool::from_json: expected boolean` | 同上 |
| `from_json` 到 `Int`，输入 `5432.7` | 成功，得 `5432` | **静默截断**，不报错 |
