Compare commits
8 Commits
575e861e0a
...
60f955dbcc
| Author | SHA1 | Date |
|---|---|---|
|
|
60f955dbcc | |
|
|
d52dc5030c | |
|
|
7d46147e9b | |
|
|
60e8ac7715 | |
|
|
3d6fdcc8dd | |
|
|
72ca625635 | |
|
|
5ce4612f8c | |
|
|
654cb5ed69 |
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -4,6 +4,17 @@
|
|||
|
||||
**To see the full details, please refer to docs in branch `doc`.**
|
||||
|
||||
## v0.8.2 (2026-07-12)
|
||||
|
||||
### Add README in exporters/ and importers/ subpackages
|
||||
- `exporters/README.md` and `importers/README.md` added to document public API, file structure, and implementation details
|
||||
|
||||
### Schema Chainable Error Methods
|
||||
- `Schema::required_error(text)` and `Schema::invalid_type_error(text)` chainable methods added to `core/schema.mbt`
|
||||
- Previously only available as factory parameters (`string(required_error?, invalid_type_error?)`), now also chainable: `string().min(2).required_error("...").invalid_type_error("...")`
|
||||
|
||||
**479 tests** (all passing, 0 warnings)
|
||||
|
||||
## v0.8.1 (2026-07-07)
|
||||
|
||||
**Exporter Hardening — Prompt Unification, JSON Schema Fixes, Renderer Convergence (Phase 41)**
|
||||
|
|
@ -28,8 +39,7 @@
|
|||
### C — Code Exporter Fixes
|
||||
- `schema_to_moon_zod_code()` now handles `any()`, `unknown()`, `tuple()`, `preprocess()` with valid code output
|
||||
- Fixed `json_to_literal` for boolean/null values (no longer uses non-existent `Json::boolean()` / `Json::null()` constructors)
|
||||
|
||||
**470 tests** (all passing, 0 warnings)
|
||||
- `schema_to_moonbit_struct_named()` and `schema_to_moonbit_struct_named_full()` removed from public API (deferred: moonbit_struct exporter not yet hardened to Phase 41 standard for tuple/any/unknown)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
| [CLI Reference](./docs/en/CLI.md) | Command-line usage |
|
||||
| [Benchmark](./docs/en/BENCHMARK.md) | Performance comparison with other validation libraries |
|
||||
| [Examples](./docs/en/EXAMPLES.md) | Practical usage examples |
|
||||
| [Exporters](./exporters/README.md) | moon_zod exporters documentation |
|
||||
| [Importers](./importers/README.md) | moon_zod importers documentation |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -126,8 +128,7 @@ moon_zod/
|
|||
│ └── moon_zod_wbtest.mbt # White-box tests (path stack invariants)
|
||||
│
|
||||
├── combinators/ # Schema combinator utilities
|
||||
│ ├── schema_combinators.mbt # Schema composition helpers
|
||||
│ └── reexporter.mbt # Re-exports
|
||||
│ └── schema_combinators.mbt # Schema composition helpers
|
||||
│
|
||||
├── exporters/ # Code/schema export tools
|
||||
│ ├── prompt.mbt # schema_to_prompt() / schema_to_prompt_named()
|
||||
|
|
@ -135,12 +136,10 @@ moon_zod/
|
|||
│ ├── json_schema.mbt # to_json_schema() / to_json_schema_named()
|
||||
│ ├── json_schema_renderer.mbt # Trait-based JSON Schema rendering
|
||||
│ ├── moonbit_struct.mbt # schema_to_moonbit_struct() + static to_schema() generation
|
||||
│ ├── schema_exporter.mbt # Shared exporter utilities
|
||||
│ └── reexporter.mbt # Module re-exports
|
||||
│ └── schema_exporter.mbt # Shared exporter utilities
|
||||
│
|
||||
├── importers/ # Schema import tools
|
||||
│ ├── from_json_schema.mbt # json_schema_to_moon_zod() — reverse JSON Schema → moon_zod code generation
|
||||
│ └── reexporter.mbt # Module re-exports
|
||||
│ └── from_json_schema.mbt # json_schema_to_moon_zod() — reverse JSON Schema → moon_zod code generation
|
||||
│
|
||||
├── tests/ # Test suite (466 tests)
|
||||
│ ├── test_string.mbt # string() validator tests (trim, to_lower, to_upper, nonempty)
|
||||
|
|
@ -251,6 +250,13 @@ moon run examples/json2schema # JSON → moon_zod schema
|
|||
|
||||
---
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [vscode-moon-zod-schema](https://github.com/Betterlol/vscode-moon-zod-schema.git) - VSCode extension for moon_zod.
|
||||
> website: [extension](https://marketplace.visualstudio.com/items?itemName=Betterlol.vscode-moon-zod-schema).
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
- [DESIGN.md](./DESIGN.md) for architecture, design decisions, and future directions.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
| [CLI 参考](./docs/zh/CLI.md) | 命令行使用说明 |
|
||||
| [性能基准](./docs/zh/BENCHMARK.md) | 与其他校验库的性能对比 |
|
||||
| [使用示例](./docs/zh/EXAMPLES.md) | 实际使用示例 |
|
||||
| [导出器说明](./exporters/README.md) | moon_zod 的导出器说明 |
|
||||
| [导入器说明](./importers/README.md) | moon_zod 的导入器说明 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -126,8 +128,7 @@ moon_zod/
|
|||
│ └── moon_zod_wbtest.mbt # 白盒测试(路径栈不变量)
|
||||
│
|
||||
├── combinators/ # Schema 组合器工具
|
||||
│ ├── schema_combinators.mbt # Schema 组合辅助函数
|
||||
│ └── reexporter.mbt # 重新导出
|
||||
│ └── schema_combinators.mbt # Schema 组合辅助函数
|
||||
│
|
||||
├── exporters/ # 代码/Schema 导出工具
|
||||
│ ├── prompt.mbt # schema_to_prompt() / schema_to_prompt_named()
|
||||
|
|
@ -135,12 +136,10 @@ moon_zod/
|
|||
│ ├── json_schema.mbt # to_json_schema() / to_json_schema_named()
|
||||
│ ├── json_schema_renderer.mbt # 基于特性的 JSON Schema 渲染
|
||||
│ ├── moonbit_struct.mbt # schema_to_moonbit_struct() + 静态 to_schema() 生成
|
||||
│ ├── schema_exporter.mbt # 共享导出器工具
|
||||
│ └── reexporter.mbt # 模块重新导出
|
||||
│ └── schema_exporter.mbt # 共享导出器工具
|
||||
│
|
||||
├── importers/ # Schema 导入工具
|
||||
│ ├── from_json_schema.mbt # json_schema_to_moon_zod() —— 反向 JSON Schema → moon_zod 代码生成
|
||||
│ └── reexporter.mbt # 模块重新导出
|
||||
│ └── from_json_schema.mbt # json_schema_to_moon_zod() —— 反向 JSON Schema → moon_zod 代码生成
|
||||
│
|
||||
├── tests/ # 测试套件(466 个测试)
|
||||
│ ├── test_string.mbt # string() 校验器测试(trim、to_lower、to_upper、nonempty)
|
||||
|
|
@ -251,6 +250,13 @@ moon run examples/json2schema # JSON → moon_zod Schema
|
|||
|
||||
---
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [vscode-moon-zod-schema](https://github.com/Betterlol/vscode-moon-zod-schema.git) - moon_zod 的 VSCode 插件。
|
||||
> 网站: [extension](https://marketplace.visualstudio.com/items?itemName=Betterlol.vscode-moon-zod-schema).
|
||||
|
||||
---
|
||||
|
||||
## 了解更多
|
||||
|
||||
- [架构设计文档](./DESIGN.md) — 核心架构、设计决策与未来方向
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
///|
|
||||
pub using @exporters {
|
||||
trait JsonSchemaRenderer,
|
||||
trait StringRenderer,
|
||||
to_json_schema,
|
||||
to_json_schema_skeleton,
|
||||
to_json_schema_named,
|
||||
schema_to_moonbit_struct,
|
||||
schema_to_moonbit_struct_full,
|
||||
schema_to_prompt,
|
||||
schema_to_prompt_named,
|
||||
schema_to_moon_zod_code,
|
||||
schema_to_moon_zod_code_named,
|
||||
schema_to_moon_zod_code_inline_with_refs,
|
||||
}
|
||||
|
||||
///|
|
||||
pub using @importers {json_schema_to_schema}
|
||||
|
|
@ -14,8 +14,19 @@
|
|||
/// let code = json_schema_to_moon_zod(schema)
|
||||
/// // "let root = @moon_zod.string().min(3).name(\"Root\")"
|
||||
/// ```
|
||||
|
||||
///|
|
||||
/// **JSON @core.Schema → moon_zod source code**
|
||||
pub fn json_schema_to_moon_zod(schema : Json) -> String {
|
||||
let schema_obj = json_schema_to_schema(schema)
|
||||
let named_defs = schema_to_moon_zod_code_named(schema_obj)
|
||||
let schema_obj = @importers.json_schema_to_schema(schema)
|
||||
let named_defs = @exporters.schema_to_moon_zod_code_named(schema_obj)
|
||||
named_defs
|
||||
}
|
||||
|
||||
///|
|
||||
/// **JSON @core.Schema → prompt**
|
||||
pub fn json_schema_to_prompt(schema : Json) -> String {
|
||||
let schema_obj = @importers.json_schema_to_schema(schema)
|
||||
let prompt = @exporters.schema_to_prompt_named(schema_obj)
|
||||
prompt
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,19 @@ pub fn Schema::brand(self : Schema, text : String) -> Schema {
|
|||
{ ..self, brand: text }
|
||||
}
|
||||
|
||||
///|
|
||||
/// Override the error message when a required field is missing.
|
||||
/// Only effective when this schema is used as an object field value.
|
||||
pub fn Schema::required_error(self : Schema, text : String) -> Schema {
|
||||
{ ..self, required_error: text }
|
||||
}
|
||||
|
||||
///|
|
||||
/// Override the error message when the input type does not match.
|
||||
pub fn Schema::invalid_type_error(self : Schema, text : String) -> Schema {
|
||||
{ ..self, invalid_type_error: text }
|
||||
}
|
||||
|
||||
///|
|
||||
/// Override the error message of the last rule in the chain.
|
||||
/// Peels through OptionalType / DefaultType / TransformType wrappers
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@
|
|||
| `.describe(text)` | any | **Phase 17**: Attach description for LLM prompts |
|
||||
| `.name(text)` | any | **Phase 25**: Assign a name for schema exports |
|
||||
| `.brand(text)` | any | **Phase 37**: Assign a brand marker for nominal typing |
|
||||
| `.required_error(text)` | any | Override the error message when a required field is missing |
|
||||
| `.invalid_type_error(text)` | any | Override the error message when the input type does not match |
|
||||
| `.message(text)` | any | **Phase 19**: Override the last rule's error message |
|
||||
| `.intersect(other)` | any | **Phase 18**: Intersection — input must match both schemas |
|
||||
| `.refine(check, msg)` | any | Custom validation predicate |
|
||||
|
|
@ -79,6 +81,7 @@
|
|||
| `to_json_schema_skeleton(Schema)` | **Phase 15**: Export lightweight JSON Schema skeleton (structure only) |
|
||||
| `to_json_schema_named(Schema, include_names?)` | **Phase 26, 34**: Export named schemas as `$defs` with `$ref` |
|
||||
| `json_schema_to_moon_zod(Json)` | **Phase 27, 36**: Reverse-generate moon_zod code from JSON Schema |
|
||||
| `json_schema_to_prompt(Json)` | **Phase 41**: Convert JSON Schema to TypeScript-interface prompt string |
|
||||
| `schema_to_moonbit_struct(Schema)` | Generate MoonBit struct/enum definitions for every object/enum schema |
|
||||
| `schema_to_moonbit_struct_full(Schema)` | Generate definitions plus static `Type::to_schema()` functions |
|
||||
| `schema_to_moon_zod_code(Schema)` | Generate moon_zod schema source code |
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ moon_zod/
|
|||
│ └── moon_zod_wbtest.mbt # White-box tests (path stack invariants)
|
||||
│
|
||||
├── combinators/ # Schema combinator utilities
|
||||
│ ├── schema_combinators.mbt # Schema composition helpers
|
||||
│ └── reexporter.mbt # Re-exports
|
||||
│ └── schema_combinators.mbt # Schema composition helpers
|
||||
│
|
||||
├── exporters/ # Code/schema export tools
|
||||
│ ├── prompt.mbt # schema_to_prompt() / schema_to_prompt_named()
|
||||
|
|
@ -52,12 +51,10 @@ moon_zod/
|
|||
│ ├── json_schema.mbt # to_json_schema() / to_json_schema_named()
|
||||
│ ├── json_schema_renderer.mbt # Trait-based JSON Schema rendering
|
||||
│ ├── moonbit_struct.mbt # schema_to_moonbit_struct() + static to_schema() generation
|
||||
│ ├── schema_exporter.mbt # Shared exporter utilities
|
||||
│ └── reexporter.mbt # Module re-exports
|
||||
│ └── schema_exporter.mbt # Shared exporter utilities
|
||||
│
|
||||
├── importers/ # Schema import tools
|
||||
│ ├── from_json_schema.mbt # json_schema_to_moon_zod() — reverse JSON Schema → moon_zod code generation
|
||||
│ └── reexporter.mbt # Module re-exports
|
||||
│ └── from_json_schema.mbt # json_schema_to_moon_zod() — reverse JSON Schema → moon_zod code generation
|
||||
│
|
||||
├── tests/ # Test suite (466 tests)
|
||||
│ ├── test_string.mbt # string() validator tests (trim, to_lower, to_upper, nonempty)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
| [CLI Reference](./docs/en/CLI.md) | Command-line usage |
|
||||
| [Benchmark](./docs/en/BENCHMARK.md) | Performance comparison with other validation libraries |
|
||||
| [Examples](./docs/en/EXAMPLES.md) | Practical usage examples |
|
||||
| [Exporters](./exporters/README.md) | moon_zod exporters documentation |
|
||||
| [Importers](./importers/README.md) | moon_zod importers documentation |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -36,6 +38,13 @@ A Schema IR core providing runtime validation, multi-source import, multi-format
|
|||
|
||||
---
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [vscode-moon-zod-schema](https://github.com/Betterlol/vscode-moon-zod-schema.git) - VSCode extension for moon_zod.
|
||||
> website: [extension](https://marketplace.visualstudio.com/items?itemName=Betterlol.vscode-moon-zod-schema).
|
||||
|
||||
---
|
||||
|
||||
## Learn More
|
||||
|
||||
- [DESIGN.md](./DESIGN.md) for architecture, design decisions, and future directions.
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@
|
|||
| `.describe(text)` | any | **Phase 17**:为 LLM 提示附加描述 |
|
||||
| `.name(text)` | any | **Phase 25**:为 schema 导出指定名称 |
|
||||
| `.brand(text)` | any | **Phase 37**:为名义类型指定品牌标记 |
|
||||
| `.required_error(text)` | any | 覆盖必填字段缺失时的错误消息 |
|
||||
| `.invalid_type_error(text)` | any | 覆盖输入类型不匹配时的错误消息 |
|
||||
| `.message(text)` | any | **Phase 19**:覆盖最后一条规则的错误消息 |
|
||||
| `.intersect(other)` | any | **Phase 18**:交集 — 输入必须同时匹配两个 schema |
|
||||
| `.refine(check, msg)` | any | 自定义校验谓词 |
|
||||
|
|
@ -79,6 +81,7 @@
|
|||
| `to_json_schema_skeleton(Schema)` | **Phase 15**:导出轻量级 JSON Schema 骨架(仅结构) |
|
||||
| `to_json_schema_named(Schema, include_names?)` | **Phase 26, 34**:将命名 schema 导出为 `$defs` 和 `$ref` |
|
||||
| `json_schema_to_moon_zod(Json)` | **Phase 27, 36**:从 JSON Schema 反向生成 moon_zod 代码 |
|
||||
| `json_schema_to_prompt(Json)` | **Phase 41**:将 JSON Schema 转换为 TypeScript 接口提示字符串 |
|
||||
| `schema_to_moonbit_struct(Schema)` | 为每个对象/枚举 schema 生成 MoonBit struct/enum 定义 |
|
||||
| `schema_to_moonbit_struct_full(Schema)` | 生成定义并附加静态 `Type::to_schema()` 函数 |
|
||||
| `schema_to_moon_zod_code(Schema)` | 生成 moon_zod schema 源代码 |
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ moon_zod/
|
|||
│ └── moon_zod_wbtest.mbt # 白盒测试(路径栈不变量)
|
||||
│
|
||||
├── combinators/ # Schema 组合器工具
|
||||
│ ├── schema_combinators.mbt # Schema 组合辅助函数
|
||||
│ └── reexporter.mbt # 重新导出
|
||||
│ └── schema_combinators.mbt # Schema 组合辅助函数
|
||||
│
|
||||
├── exporters/ # 代码/Schema 导出工具
|
||||
│ ├── prompt.mbt # schema_to_prompt() / schema_to_prompt_named()
|
||||
|
|
@ -52,12 +51,10 @@ moon_zod/
|
|||
│ ├── json_schema.mbt # to_json_schema() / to_json_schema_named()
|
||||
│ ├── json_schema_renderer.mbt # 基于特性的 JSON Schema 渲染
|
||||
│ ├── moonbit_struct.mbt # schema_to_moonbit_struct() + 静态 to_schema() 生成
|
||||
│ ├── schema_exporter.mbt # 共享导出器工具
|
||||
│ └── reexporter.mbt # 模块重新导出
|
||||
│ └── schema_exporter.mbt # 共享导出器工具
|
||||
│
|
||||
├── importers/ # Schema 导入工具
|
||||
│ ├── from_json_schema.mbt # json_schema_to_moon_zod() —— 反向 JSON Schema → moon_zod 代码生成
|
||||
│ └── reexporter.mbt # 模块重新导出
|
||||
│ └── from_json_schema.mbt # json_schema_to_moon_zod() —— 反向 JSON Schema → moon_zod 代码生成
|
||||
│
|
||||
├── tests/ # 测试套件(466 个测试)
|
||||
│ ├── test_string.mbt # string() 校验器测试(trim、to_lower、to_upper、nonempty)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
| [CLI 参考](./docs/zh/CLI.md) | 命令行使用说明 |
|
||||
| [性能基准](./docs/zh/BENCHMARK.md) | 与其他校验库的性能对比 |
|
||||
| [使用示例](./docs/zh/EXAMPLES.md) | 实际使用示例 |
|
||||
| [导出器说明](./exporters/README.md) | moon_zod 的导出器说明 |
|
||||
| [导入器说明](./importers/README.md) | moon_zod 的导入器说明 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -36,6 +38,13 @@ moon_zod 是一个**运行时 Schema 中间表示(IR)**——独立于输入
|
|||
|
||||
---
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [vscode-moon-zod-schema](https://github.com/Betterlol/vscode-moon-zod-schema.git) - moon_zod 的 VSCode 插件。
|
||||
> 网站: [extension](https://marketplace.visualstudio.com/items?itemName=Betterlol.vscode-moon-zod-schema).
|
||||
|
||||
---
|
||||
|
||||
## 了解更多
|
||||
|
||||
- [架构设计文档](./DESIGN.md) — 核心架构、设计决策与未来方向
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
# exporters
|
||||
|
||||
Schema → String / Json / MoonBit source code 导出工具包。
|
||||
|
||||
本包提供多条独立导出管线(Prompt / JSON Schema / MoonBit struct / MoonZod Schema)。每条管线对 `@core.SchemaType` 的 17 个变体做逐一渲染,支持内联展开和命名引用两种模式。
|
||||
|
||||
---
|
||||
|
||||
## 公开 API
|
||||
|
||||
### Prompt 导出
|
||||
|
||||
- `schema_to_prompt(schema) -> String` — 将 Schema 渲染为 TypeScript-interface 风格的提示文本(全内联)
|
||||
- `schema_to_prompt_named(schema, include_names?) -> String` — 提取带 `name` 的 Schema 生成模块化接口,带拓扑排序和类型引用
|
||||
|
||||
### JSON Schema 导出
|
||||
|
||||
- `to_json_schema(schema) -> Json` — 标准 JSON Schema(完整约束注释)
|
||||
- `to_json_schema_skeleton(schema) -> Json` — 轻量级骨架(仅类型结构,无约束)
|
||||
- `to_json_schema_named(schema, include_names?) -> Json` — 命名 Schema 导出为 `$defs` + `$ref`
|
||||
|
||||
### MoonBit 结构导出
|
||||
|
||||
- `schema_to_moonbit_struct(schema) -> String` — 生成 MoonBit `pub struct` / `pub enum` 定义
|
||||
- `schema_to_moonbit_struct_full(schema) -> String` — 生成定义 + 静态 `Type::to_schema()` 方法
|
||||
|
||||
### 代码生成
|
||||
|
||||
- `schema_to_moon_zod_code(schema) -> String` — 将 Schema 反转为可 copy-paste 的 moon_zod 源码
|
||||
- `schema_to_moon_zod_code_named(schema, include_names?) -> String` — 带命名 Schema 引用的源码生成
|
||||
- `schema_to_moon_zod_code_inline_with_refs(schema, include_names) -> String` — 内联渲染,可选命名引用替换
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
exporters/
|
||||
├── prompt.mbt # schema_to_prompt / schema_to_prompt_named 公共 API
|
||||
├── prompt_renderer.mbt # StringRenderer trait + NamedPromptRenderer 实现
|
||||
├── json_schema.mbt # to_json_schema / to_json_schema_named / to_json_schema_skeleton 公共 API
|
||||
├── json_schema_renderer.mbt # JsonSchemaRenderer trait + NamedJsonRenderer 实现
|
||||
├── moonbit_struct.mbt # schema_to_moonbit_struct / _full 公共 API
|
||||
├── schema_exporter.mbt # schema_to_moon_zod_code / _named / _inline 公共 API
|
||||
└── pkg.generated.mbti # moon info 生成的接口描述(勿手动编辑)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现说明
|
||||
|
||||
### 渲染器架构
|
||||
|
||||
导出管线统一采用 **trait 分发** 模式:
|
||||
|
||||
1. 每个 `SchemaType` 变体对应 trait 中的一个方法
|
||||
2. 顶层的 `render_type` / `render_json_type` 做单点分发
|
||||
3. 渲染器只负责「类型 → 文本/Json」的转换,约束注释、命名引用、描述文本由各模块独立处理
|
||||
|
||||
```
|
||||
Schema ──► render_type(renderer, schema) ──► StringRenderer ──► String
|
||||
└── render_json_type(renderer, schema) ──► JsonSchemaRenderer ──► Json
|
||||
```
|
||||
|
||||
#### Prompt 导出(`prompt.mbt` + `prompt_renderer.mbt`)
|
||||
|
||||
- `schema_to_prompt()` 内部复用 `NamedPromptRenderer([])`,named 集合为空 → 全内联展开
|
||||
- `schema_to_prompt_named()` 收集所有带 `name` 的 Schema,拓扑排序后生成 `export interface / export type` 定义
|
||||
- `include_names?` 参数支持选择性导出(`None` = 全部,`Some([])` = 无,`Some([...])` = 指定)
|
||||
- named wrapper schemas(primitive、array、optional、default、transform、tuple、any、unknown、preprocess)统一生成 `export type X = T`
|
||||
- named intersection 当包含非 object 分支时输出 `export type X = A & B`,否则合并 object 字段
|
||||
|
||||
#### JSON Schema 导出(`json_schema.mbt` + `json_schema_renderer.mbt`)
|
||||
|
||||
- `NamedJsonRenderer` 的 `include_annotations` 参数统一三种模式:
|
||||
- `include_annotations=true` → `to_json_schema()`(完整约束)
|
||||
- `include_annotations=false` → `to_json_schema_skeleton()`(仅结构)
|
||||
- `optional()` / `default()` 导出 nullable 语义:`anyOf: [inner, {"type": "null"}]`
|
||||
- `Strip` 模式导出 `additionalProperties: false`(与运行时幻觉防御对齐)
|
||||
- object intersection 合并为单个 closed object;同名字段用属性级 `allOf` 保留各自约束
|
||||
- `$defs` 渲染时排除自引用(`ref_names.filter(n => n != ns.name)`)
|
||||
|
||||
### 代码生成架构
|
||||
|
||||
本包包含两条**反向代码生成**管线,均不采用 trait 分发,而是直接 match `SchemaType` 生成目标语言源码。
|
||||
|
||||
#### MoonBit 结构导出(`moonbit_struct.mbt`)
|
||||
|
||||
- `schema_to_moonbit_struct()` 将 `ObjectType` 映射为 `pub struct`,`EnumType` 映射为 `pub enum`
|
||||
- `schema_to_moonbit_struct_full()` 额外生成静态 `Type::to_schema() -> @moon_zod.Schema` 方法,实现 struct ↔ Schema 往返
|
||||
- 基础类型映射:`String → String`,`Number → Int64 / Double`(按 `.int()` 规则区分),`Boolean → Bool`
|
||||
- `ArrayType → Array[T]`,可空 `UnionType` 剥除 `null` 后映射
|
||||
- `AnyType / UnknownType` 当前回退为 `Json`
|
||||
- `TupleType` / 非可空 `UnionType` / `IntersectionType` 当前回退为 `Json // TODO`
|
||||
- 字段名和类型名自动转义(MoonBit 关键字、保留字、首字母数字)
|
||||
- 约束注释以行内注释形式附加在 struct field 上
|
||||
|
||||
#### MoonZod 代码导出(`schema_exporter.mbt`)
|
||||
|
||||
- `schema_to_moon_zod_code()` 将运行时 Schema 反转为可 copy-paste 的 moon_zod 方法链源码
|
||||
- 输出格式:`let name = @moon_zod.string().min(3).describe("...").name("name")`
|
||||
- 根 Schema 无 `name` 时自动赋予 `"Root"`
|
||||
- `schema_to_moon_zod_code_named()` 提取所有命名 Schema,拓扑排序后输出 `let name = ...` 定义列表
|
||||
- 规则注释通过 `Rule.annotation` 反推 `.min()` / `.max()` / `.email()` / `.url()` / `.int()` 等
|
||||
- `json_to_literal()` 将 Json 值转为 MoonBit 构造函数调用(`Json::string(...)` / `Json::number(...)` / `true` / `false` / `null`)
|
||||
- `PreprocessType` / `TransformType` 输出占位闭包 `fn(x) { Ok(x) }`,标记为最佳代码输出(自定义闭包无法序列化)
|
||||
- `.required_error()` / `.invalid_type_error()` 链式方法仍会输出代码,但当前 `Schema` 工厂 API 未暴露这两个参数——round-trip 不安全
|
||||
|
||||
---
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 当前 `moonbit_struct.mbt` 中 `TupleType` 仍回退为 `Json`,`any/unknown` 仅做最低限度映射。待后续整体重构时重新评估。
|
||||
- MoonBit 结构导出尚不支持 `tuple` / `intersection` / `union` 等复杂类型的精确类型映射,当前回退为 `Json // TODO`。
|
||||
- MoonZod 代码导出对 `TransformType` / `PreprocessType` 输出占位闭包 `fn(x) { Ok(x) }`,自定义闭包无法序列化。
|
||||
|
|
@ -183,6 +183,21 @@ fn[R : JsonSchemaRenderer] render_intersection_schema(
|
|||
)
|
||||
}
|
||||
|
||||
///|
|
||||
fn[R : JsonSchemaRenderer] render_json_tuple_type(
|
||||
renderer : R,
|
||||
items : Array[@core.Schema],
|
||||
) -> Json {
|
||||
Json::object({
|
||||
"type": Json::string("array"),
|
||||
"prefixItems": Json::array(
|
||||
items.map(fn(item) { render_json_type(renderer, item) }),
|
||||
),
|
||||
"minItems": Json::number(items.length().to_double()),
|
||||
"maxItems": Json::number(items.length().to_double()),
|
||||
})
|
||||
}
|
||||
|
||||
///|
|
||||
fn can_merge_object_intersection(schemas : Array[@core.Schema]) -> Bool {
|
||||
for schema in schemas {
|
||||
|
|
@ -499,6 +514,44 @@ pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_literal(
|
|||
)
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_any(
|
||||
self : NamedJsonRenderer,
|
||||
_schema : @core.Schema,
|
||||
) -> Json {
|
||||
Json::object({})
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_unknown(
|
||||
self : NamedJsonRenderer,
|
||||
_schema : @core.Schema,
|
||||
) -> Json {
|
||||
Json::object({})
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_tuple(
|
||||
self : NamedJsonRenderer,
|
||||
items : Array[@core.Schema],
|
||||
_schema : @core.Schema,
|
||||
) -> Json {
|
||||
render_json_tuple_type(self, items)
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl JsonSchemaRenderer for NamedJsonRenderer with fn render_preprocess(
|
||||
self : NamedJsonRenderer,
|
||||
inner : @core.Schema,
|
||||
_schema : @core.Schema,
|
||||
) -> Json {
|
||||
render_json_type(self, inner)
|
||||
}
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
///|
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ pub(open) trait JsonSchemaRenderer {
|
|||
schema : @core.Schema,
|
||||
) -> Json
|
||||
fn render_literal(Self, value : Json, schema : @core.Schema) -> Json
|
||||
fn render_any(Self, schema : @core.Schema) -> Json
|
||||
fn render_unknown(Self, schema : @core.Schema) -> Json
|
||||
fn render_tuple(Self, items : Array[@core.Schema], schema : @core.Schema) -> Json
|
||||
fn render_preprocess(Self, inner : @core.Schema, schema : @core.Schema) -> Json
|
||||
}
|
||||
|
||||
///|
|
||||
|
|
@ -49,38 +53,24 @@ pub fn[R : JsonSchemaRenderer] render_json_type(
|
|||
NumberType => renderer.render_number(schema)
|
||||
BooleanType => renderer.render_boolean(schema)
|
||||
NullType => renderer.render_null(schema)
|
||||
AnyType | UnknownType => Json::object({})
|
||||
AnyType => renderer.render_any(schema)
|
||||
UnknownType => renderer.render_unknown(schema)
|
||||
ObjectType(spec, mode) => renderer.render_object(spec, mode, schema)
|
||||
ArrayType(elem) => renderer.render_array(elem, schema)
|
||||
TupleType(items) => render_json_tuple_type(renderer, items)
|
||||
TupleType(items) => renderer.render_tuple(items, schema)
|
||||
OptionalType(inner) => renderer.render_optional(inner, schema)
|
||||
DefaultType(inner, default_val) =>
|
||||
renderer.render_default(inner, default_val, schema)
|
||||
EnumType(values) => renderer.render_enum(values, schema)
|
||||
UnionType(schemas) => renderer.render_union(schemas, schema)
|
||||
IntersectionType(schemas) => renderer.render_intersection(schemas, schema)
|
||||
PreprocessType(_, inner) => render_json_type(renderer, inner)
|
||||
PreprocessType(_, inner) => renderer.render_preprocess(inner, schema)
|
||||
TransformType(inner, closure) =>
|
||||
renderer.render_transform(inner, closure, schema)
|
||||
LiteralType(value) => renderer.render_literal(value, schema)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn[R : JsonSchemaRenderer] render_json_tuple_type(
|
||||
renderer : R,
|
||||
items : Array[@core.Schema],
|
||||
) -> Json {
|
||||
Json::object({
|
||||
"type": Json::string("array"),
|
||||
"prefixItems": Json::array(
|
||||
items.map(fn(item) { render_json_type(renderer, item) }),
|
||||
),
|
||||
"minItems": Json::number(items.length().to_double()),
|
||||
"maxItems": Json::number(items.length().to_double()),
|
||||
})
|
||||
}
|
||||
|
||||
///|
|
||||
/// Named-aware version: emits a `$ref` if schema has a name in the list.
|
||||
pub fn[R : JsonSchemaRenderer] render_json_type_ref(
|
||||
|
|
|
|||
|
|
@ -250,6 +250,48 @@ pub impl StringRenderer for NamedPromptRenderer with fn render_literal(
|
|||
}
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl StringRenderer for NamedPromptRenderer with fn render_any(
|
||||
self : NamedPromptRenderer,
|
||||
_schema : @core.Schema,
|
||||
_indent : Int,
|
||||
) -> String {
|
||||
"any"
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl StringRenderer for NamedPromptRenderer with fn render_unknown(
|
||||
self : NamedPromptRenderer,
|
||||
_schema : @core.Schema,
|
||||
_indent : Int,
|
||||
) -> String {
|
||||
"unknown"
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl StringRenderer for NamedPromptRenderer with fn render_tuple(
|
||||
self : NamedPromptRenderer,
|
||||
items : Array[@core.Schema],
|
||||
_schema : @core.Schema,
|
||||
indent : Int,
|
||||
) -> String {
|
||||
render_tuple_type(self, items, indent)
|
||||
}
|
||||
|
||||
///|
|
||||
#warnings("-unused_value")
|
||||
pub impl StringRenderer for NamedPromptRenderer with fn render_preprocess(
|
||||
self : NamedPromptRenderer,
|
||||
inner : @core.Schema,
|
||||
_schema : @core.Schema,
|
||||
indent : Int,
|
||||
) -> String {
|
||||
render_type(self, inner, indent)
|
||||
}
|
||||
|
||||
///|
|
||||
/// Public API: convert a schema to a TypeScript-interface-style prompt @core.string.
|
||||
pub fn schema_to_prompt(schema : @core.Schema) -> String {
|
||||
|
|
@ -551,6 +593,19 @@ fn enum_values_to_string(values : Array[String]) -> String {
|
|||
result
|
||||
}
|
||||
|
||||
///|
|
||||
fn[R : StringRenderer] render_tuple_type(
|
||||
renderer : R,
|
||||
items : Array[@core.Schema],
|
||||
indent : Int,
|
||||
) -> String {
|
||||
let parts : Array[String] = []
|
||||
for item in items {
|
||||
parts.push(render_type(renderer, item, indent))
|
||||
}
|
||||
"[" + parts.join(", ") + "]"
|
||||
}
|
||||
|
||||
///|
|
||||
/// Helper: check if a schema has a name present in the named schemas list.
|
||||
fn has_name_in(
|
||||
|
|
|
|||
|
|
@ -60,6 +60,20 @@ pub(open) trait StringRenderer {
|
|||
indent : Int,
|
||||
) -> String
|
||||
fn render_literal(Self, value : Json, schema : @core.Schema, indent : Int) -> String
|
||||
fn render_any(Self, schema : @core.Schema, indent : Int) -> String
|
||||
fn render_unknown(Self, schema : @core.Schema, indent : Int) -> String
|
||||
fn render_tuple(
|
||||
Self,
|
||||
items : Array[@core.Schema],
|
||||
schema : @core.Schema,
|
||||
indent : Int,
|
||||
) -> String
|
||||
fn render_preprocess(
|
||||
Self,
|
||||
inner : @core.Schema,
|
||||
schema : @core.Schema,
|
||||
indent : Int,
|
||||
) -> String
|
||||
}
|
||||
|
||||
///|
|
||||
|
|
@ -74,11 +88,11 @@ pub fn[R : StringRenderer] render_type(
|
|||
NumberType => renderer.render_number(schema, indent)
|
||||
BooleanType => renderer.render_boolean(schema, indent)
|
||||
NullType => renderer.render_null(schema, indent)
|
||||
AnyType => "any"
|
||||
UnknownType => "unknown"
|
||||
AnyType => renderer.render_any(schema, indent)
|
||||
UnknownType => renderer.render_unknown(schema, indent)
|
||||
ObjectType(spec, mode) => renderer.render_object(spec, mode, schema, indent)
|
||||
ArrayType(elem) => renderer.render_array(elem, schema, indent)
|
||||
TupleType(items) => render_tuple_type(renderer, items, indent)
|
||||
TupleType(items) => renderer.render_tuple(items, schema, indent)
|
||||
OptionalType(inner) => renderer.render_optional(inner, schema, indent)
|
||||
DefaultType(inner, default_val) =>
|
||||
renderer.render_default(inner, default_val, schema, indent)
|
||||
|
|
@ -86,26 +100,14 @@ pub fn[R : StringRenderer] render_type(
|
|||
UnionType(schemas) => renderer.render_union(schemas, schema, indent)
|
||||
IntersectionType(schemas) =>
|
||||
renderer.render_intersection(schemas, schema, indent)
|
||||
PreprocessType(_, inner) => render_type(renderer, inner, indent)
|
||||
PreprocessType(_, inner) =>
|
||||
renderer.render_preprocess(inner, schema, indent)
|
||||
TransformType(inner, closure) =>
|
||||
renderer.render_transform(inner, closure, schema, indent)
|
||||
LiteralType(value) => renderer.render_literal(value, schema, indent)
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
fn[R : StringRenderer] render_tuple_type(
|
||||
renderer : R,
|
||||
items : Array[@core.Schema],
|
||||
indent : Int,
|
||||
) -> String {
|
||||
let parts : Array[String] = []
|
||||
for item in items {
|
||||
parts.push(render_type(renderer, item, indent))
|
||||
}
|
||||
"[" + parts.join(", ") + "]"
|
||||
}
|
||||
|
||||
///|
|
||||
/// Named-aware version: renders a name reference if the schema has a name
|
||||
/// present in the named schemas list, otherwise dispatches normally.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
# importers
|
||||
|
||||
JSON Schema → moon_zod Schema 导入工具包。
|
||||
|
||||
本包提供 **JSON Schema draft-07 → moon_zod @core.Schema** 的运行时转换能力,是四层架构中的第一层(Input → IR)。转换结果可直接用于 `schema.parse()` 运行时校验,或送入 exporters 做进一步代码/Schema 导出。
|
||||
|
||||
---
|
||||
|
||||
## 公开 API
|
||||
|
||||
- `json_schema_to_schema(schema : Json) -> @core.Schema` — 将 JSON Schema 文档(draft-07)解析为 moon_zod 运行时 Schema 对象
|
||||
|
||||
> **注意**:`json_schema_to_moon_zod()` 定义在 `combinators/` 包,不在本包。它先调用本包的 `json_schema_to_schema()` 转为 Schema 对象,再调用 `exporters/schema_exporter.mbt` 生成源码。
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
importers/
|
||||
├── from_json_schema.mbt # json_schema_to_schema() 单文件实现
|
||||
├── moon.pkg # 包声明(仅依赖 core + moonbitlang/core/json)
|
||||
└── pkg.generated.mbti # moon info 生成的接口描述(勿手动编辑)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实现说明
|
||||
|
||||
### 架构
|
||||
|
||||
本包采用**单文件、递归下降**解析器架构:
|
||||
|
||||
```
|
||||
JSON Schema (Json)
|
||||
│
|
||||
▼
|
||||
extract_defs() ── 提取 $defs / definitions
|
||||
│
|
||||
▼
|
||||
json_to_schema_impl() ── 递归 match JSON Schema 关键字
|
||||
│ 分发到对应的 @core.Schema 工厂
|
||||
▼
|
||||
@core.Schema ── 可直接用于 schema.parse() 的运行时对象
|
||||
```
|
||||
|
||||
### 处理流程
|
||||
|
||||
`json_to_schema_impl()` 按以下优先级依次匹配:
|
||||
|
||||
1. **`$ref`** — 从 `$defs` / `definitions` 缓存或前向引用解析;循环引用用 `null().name(ref_name)` 占位
|
||||
2. **`const`** — 直接映射为 `@core.literal(val)`
|
||||
3. **`enum`** — 全字符串 → `enum_values(strs)`;全数字 → `number()` + 自定义规则校验;混合/布尔/null → `union(values.map(literal))`
|
||||
4. **`type`** — 映射基础类型 + `properties` / `items` / `required` 递归处理
|
||||
5. **`anyOf` / `allOf` / `oneOf`** — 映射为 `union()` / `intersection()`
|
||||
|
||||
### 类型映射
|
||||
|
||||
| JSON Schema type | moon_zod Schema | 备注 |
|
||||
|-----------------|-----------------|------|
|
||||
| `string` | `@core.string()` | |
|
||||
| `number` | `@core.number()` | |
|
||||
| `integer` | `@core.number().int()` | |
|
||||
| `boolean` | `@core.boolean()` | |
|
||||
| `null` | `@core.null()` | |
|
||||
| `object` | `@core.object(fields)` | `required` 数组决定 optional |
|
||||
| `array` | `@core.array(elem)` | `items` 指定元素类型 |
|
||||
| `enum` (全字符串) | `@core.enum_values(strs)` | |
|
||||
| `enum` (全数字) | `@core.number()` + 自定义规则 | |
|
||||
| `enum` (混合/布尔/null) | `@core.union(values.map(literal))` | |
|
||||
| `const` | `@core.literal(val)` | |
|
||||
| `anyOf` | `@core.union(parts)` | |
|
||||
| `allOf` | `@core.intersection(parts)` | |
|
||||
| `oneOf` | `@core.union(parts)` | 与 anyOf 等价 |
|
||||
|
||||
### Object Mode 映射
|
||||
|
||||
| JSON Schema additionalProperties | moon_zod ObjectMode |
|
||||
|--------------------------------|---------------------|
|
||||
| `true` | `Passthrough` — 保留未知字段 |
|
||||
| `false` | `Strip`(默认)— 剥离未知字段 |
|
||||
| 省略 | `Strip`(默认) | |
|
||||
|
||||
### 约束关键字映射
|
||||
|
||||
| JSON Schema 关键字 | moon_zod 方法 | 备注 |
|
||||
|--------------------|---------------|------|
|
||||
| `minLength` | `.min(n)` | 字符串/数组长度 |
|
||||
| `maxLength` | `.max(n)` | 字符串/数组长度 |
|
||||
| `pattern` | `.regex(pattern)` | |
|
||||
| `format: email` | `.email()` | |
|
||||
| `format: uri` | `.url()` | |
|
||||
| `format: date-time` | `.datetime()` | |
|
||||
| `format: ipv4` | `.ipv4()` | |
|
||||
| `format: ipv6` | `.ipv6()` | |
|
||||
| `format: uuid` | `.uuid()` | |
|
||||
| `minimum` | `.min(n)` | **⚠️ 浮点数截断为整数** |
|
||||
| `maximum` | `.max(n)` | **⚠️ 浮点数截断为整数** |
|
||||
| `exclusiveMinimum` | `.positive()` / `.min(n+1)` + 非整数精确规则 | |
|
||||
| `exclusiveMaximum` | `.negative()` / `.max(n-1)` + 非整数精确规则 | |
|
||||
| `multipleOf` | `.multipleOf(n)` | |
|
||||
| `minItems` | `.min(n)` | 数组最小长度 |
|
||||
| `maxItems` | `.max(n)` | 数组最大长度 |
|
||||
| `default` | `.default(val)` | |
|
||||
|
||||
### 循环引用处理
|
||||
|
||||
- `$defs` 首次遍历时预填充缓存
|
||||
- `$ref` 前向引用:递归处理目标定义后再返回
|
||||
- 循环引用检测:`visiting` 数组记录当前 DFS 路径,重复访问时返回 `null().name(name)` 占位
|
||||
|
||||
---
|
||||
|
||||
## 已知限制
|
||||
|
||||
- **@moon_zod.enum_values() 限制**:`enum_values()` 目前仅支持全字符串的枚举,混合类型枚举会被降级为 `@core.union()`,并且数字枚举会被降级为 `@core.number()` + 自定义规则校验。
|
||||
> 有点丑陋,但目前还能接受,后续实现升级即可。
|
||||
- **`minimum` / `maximum` 浮点数截断**:`minimum: 1.5` 经 `.to_int()` 变为 `.min(1)`,允许 `1.0` 通过。与 `exclusiveMinimum` 不同,此处未做非整数精确规则补救。如需精确浮点边界,建议在 JSON Schema 中使用 `exclusiveMinimum` / `exclusiveMaximum` 配合整数边界,或手动用 `refine()` 补充校验。
|
||||
- **`$ref` 引用缺失定义**:如果 `$ref` 指向的 key 在 `$defs` 中不存在,回退为 `@core.string()`,不报错。
|
||||
- **复杂 `format`**:仅支持 `email` / `uri` / `date-time` / `ipv4` / `ipv6` / `uuid`,其余 format 关键字静默忽略。
|
||||
- **`readOnly` / `writeOnly`**:未映射。
|
||||
- **`$schema` / `$id` 等元数据**:忽略。
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
///
|
||||
/// Two functions:
|
||||
/// 1. json_schema_to_schema() - Parse JSON @core.Schema → @core.Schema @core.object (runtime-ready)
|
||||
/// 2. json_schema_to_moon_zod() - Parse JSON @core.Schema → moon_zod source code (uses layer 2)
|
||||
/// 2. json_schema_to_moon_zod() - Parse JSON @core.Schema → moon_zod source code (defined in combinators)
|
||||
|
||||
///|
|
||||
/// **JSON @core.Schema → @core.Schema @core.object**
|
||||
|
|
@ -108,6 +108,10 @@ fn json_to_schema_impl(
|
|||
// 3. Check enum
|
||||
match m.get("enum") {
|
||||
Some(Array(values)) => {
|
||||
if values.length() == 0 {
|
||||
return @core.string()
|
||||
}
|
||||
|
||||
let strs : Array[String] = []
|
||||
let all_numbers : Array[Double] = []
|
||||
let mut has_non_numbers = false
|
||||
|
|
@ -148,12 +152,14 @@ fn json_to_schema_impl(
|
|||
return result_schema
|
||||
}
|
||||
|
||||
// Otherwise, use string enum for any string values
|
||||
if strs.length() > 0 {
|
||||
// If all values are strings, use string enum
|
||||
if strs.length() == values.length() {
|
||||
return @core.enum_values(strs)
|
||||
} else {
|
||||
return @core.string()
|
||||
}
|
||||
|
||||
// Mixed or non-string types: use union of literals
|
||||
let parts = values.map(fn(v) { @core.literal(v) })
|
||||
return @core.union(parts)
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
|
|
@ -175,7 +181,13 @@ fn json_to_schema_impl(
|
|||
}
|
||||
fields.set(key, field)
|
||||
}
|
||||
@core.object(fields)
|
||||
let mut obj_schema = @core.object(fields)
|
||||
obj_schema = match m.get("additionalProperties") {
|
||||
Some(True) => obj_schema.passthrough()
|
||||
Some(False) => obj_schema.strip()
|
||||
_ => obj_schema
|
||||
}
|
||||
obj_schema
|
||||
}
|
||||
_ => @core.object({})
|
||||
}
|
||||
|
|
|
|||
2
moon.mod
2
moon.mod
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
name = "Betterlol/moon_zod"
|
||||
|
||||
version = "0.7.5"
|
||||
version = "0.8.1"
|
||||
|
||||
readme = "README.mbt.md"
|
||||
|
||||
|
|
|
|||
|
|
@ -82,4 +82,4 @@ pub using @exporters {
|
|||
pub using @importers {json_schema_to_schema}
|
||||
|
||||
///|
|
||||
pub using @combinators {json_schema_to_moon_zod}
|
||||
pub using @combinators {json_schema_to_moon_zod, json_schema_to_prompt}
|
||||
|
|
|
|||
|
|
@ -98,3 +98,40 @@ test "string invalid_type_error defaults to expected" {
|
|||
guard s.parse(Json::number(42.0)) is Err(errors) else { fail("expected Err") }
|
||||
@debug.assert_eq(errors[0].message, "Expected string")
|
||||
}
|
||||
|
||||
///|
|
||||
/// Chainable .required_error() method
|
||||
test "schema required_error chainable method" {
|
||||
let s = object({ "name": string().required_error("姓名是必填项") })
|
||||
let input = parse_json("{}")
|
||||
match s.parse(input) {
|
||||
Err(errors) => @debug.assert_eq(errors[0].message, "姓名是必填项")
|
||||
_ => fail("expected Err")
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
/// Chainable .invalid_type_error() method on string
|
||||
test "schema invalid_type_error chainable method string" {
|
||||
let s = string().invalid_type_error("必须是字符串")
|
||||
guard s.parse(Json::number(42.0)) is Err(errors) else { fail("expected Err") }
|
||||
@debug.assert_eq(errors[0].message, "必须是字符串")
|
||||
}
|
||||
|
||||
///|
|
||||
/// Chainable .invalid_type_error() method on number
|
||||
test "schema invalid_type_error chainable method number" {
|
||||
let s = number().invalid_type_error("必须是数字")
|
||||
guard s.parse(Json::string("abc")) is Err(errors) else {
|
||||
fail("expected Err")
|
||||
}
|
||||
@debug.assert_eq(errors[0].message, "必须是数字")
|
||||
}
|
||||
|
||||
///|
|
||||
/// Chainable .invalid_type_error() overrides factory default
|
||||
test "schema invalid_type_error chainable overrides factory" {
|
||||
let s = string(invalid_type_error="factory").invalid_type_error("chain")
|
||||
guard s.parse(Json::number(42.0)) is Err(errors) else { fail("expected Err") }
|
||||
@debug.assert_eq(errors[0].message, "chain")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1136,3 +1136,50 @@ test "json_schema_to_schema: $defs and $ref" {
|
|||
_ => fail("expected Err")
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
/// Test json_schema_to_schema: additionalProperties true → passthrough
|
||||
test "json_schema_to_schema: additionalProperties true allows extra fields" {
|
||||
let input = parse_json(
|
||||
"{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"additionalProperties\":true}",
|
||||
)
|
||||
let s = json_schema_to_schema(input)
|
||||
// Should accept extra fields in passthrough mode
|
||||
match
|
||||
s.parse(
|
||||
Json::object({
|
||||
"name": Json::string("Alice"),
|
||||
"extra": Json::string("allowed"),
|
||||
}),
|
||||
) {
|
||||
Ok(_) => ()
|
||||
_ => fail("expected Ok with extra field in passthrough mode")
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
/// Test json_schema_to_schema: additionalProperties false → strip
|
||||
test "json_schema_to_schema: additionalProperties false strips extra fields" {
|
||||
let input = parse_json(
|
||||
"{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"additionalProperties\":false}",
|
||||
)
|
||||
let s = json_schema_to_schema(input)
|
||||
// Should strip extra fields (default Strip mode)
|
||||
match
|
||||
s.parse(
|
||||
Json::object({
|
||||
"name": Json::string("Alice"),
|
||||
"extra": Json::string("stripped"),
|
||||
}),
|
||||
) {
|
||||
Ok(parsed) =>
|
||||
match parsed {
|
||||
Object(m) => {
|
||||
let has_extra = m.contains("extra")
|
||||
@debug.assert_eq(has_extra, false)
|
||||
}
|
||||
_ => fail("expected object")
|
||||
}
|
||||
Err(_) => fail("expected Ok")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ test "enum_with_numbers_rejects_invalid" {
|
|||
}
|
||||
|
||||
///|
|
||||
/// Test mixed string+number enum: only strings are kept
|
||||
/// Test mixed string+number enum: union of literals preserves all values
|
||||
test "enum_mixed_string_number" {
|
||||
let json_schema = parse_json("{\"enum\": [\"a\", \"b\", 1, 2]}")
|
||||
let schema = json_schema_to_schema(json_schema)
|
||||
|
|
@ -222,44 +222,57 @@ test "enum_mixed_string_number" {
|
|||
fail("enum: should reject \"c\"")
|
||||
}
|
||||
|
||||
// Should reject number 1 (numbers dropped in mixed enum)
|
||||
guard schema.parse(parse_json("1")) is Err(_) else {
|
||||
fail("enum: should reject 1 when enum is mixed string+number")
|
||||
// Should accept number 1 (preserved in mixed enum via union of literals)
|
||||
match schema.parse(parse_json("1")) {
|
||||
Ok(_) => ()
|
||||
Err(_e) => fail("expected Ok for 1 in mixed enum")
|
||||
}
|
||||
|
||||
// Should reject number 3 (not in enum)
|
||||
guard schema.parse(parse_json("3")) is Err(_) else {
|
||||
fail("enum: should reject 3")
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
/// Test boolean enum falls back to string (known limitation)
|
||||
test "enum_boolean_falls_back_to_string" {
|
||||
/// Test boolean enum: union of literals (not string fallback)
|
||||
test "enum_boolean_literals" {
|
||||
let json_schema = parse_json("{\"enum\": [true, false]}")
|
||||
let schema = json_schema_to_schema(json_schema)
|
||||
|
||||
// Current limitation: boolean enum falls back to string(),
|
||||
// so it accepts any string but rejects booleans
|
||||
guard schema.parse(parse_json("\"anything\"")) is Ok(_) else {
|
||||
fail("boolean enum fallback: should accept any string")
|
||||
// Should accept boolean true
|
||||
match schema.parse(parse_json("true")) {
|
||||
Ok(_) => ()
|
||||
Err(_e) => fail("expected Ok for true")
|
||||
}
|
||||
|
||||
// Should reject boolean true (not a string)
|
||||
guard schema.parse(parse_json("true")) is Err(_) else {
|
||||
fail("boolean enum fallback: should reject true")
|
||||
// Should accept boolean false
|
||||
match schema.parse(parse_json("false")) {
|
||||
Ok(_) => ()
|
||||
Err(_e) => fail("expected Ok for false")
|
||||
}
|
||||
|
||||
// Should reject string (not in boolean enum)
|
||||
guard schema.parse(parse_json("\"anything\"")) is Err(_) else {
|
||||
fail("boolean enum: should reject string")
|
||||
}
|
||||
}
|
||||
|
||||
///|
|
||||
/// Test null enum falls back to string (known limitation)
|
||||
test "enum_null_falls_back_to_string" {
|
||||
/// Test null enum: union of literals (not string fallback)
|
||||
test "enum_null_literal" {
|
||||
let json_schema = parse_json("{\"enum\": [null]}")
|
||||
let schema = json_schema_to_schema(json_schema)
|
||||
|
||||
// Current limitation: null enum falls back to string()
|
||||
guard schema.parse(parse_json("\"any\"")) is Ok(_) else {
|
||||
fail("null enum fallback: should accept any string")
|
||||
// Should accept null
|
||||
match schema.parse(parse_json("null")) {
|
||||
Ok(_) => ()
|
||||
Err(_e) => fail("expected Ok for null")
|
||||
}
|
||||
|
||||
// Should reject null (not a string)
|
||||
guard schema.parse(parse_json("null")) is Err(_) else {
|
||||
fail("null enum fallback: should reject null")
|
||||
// Should reject string (not in null enum)
|
||||
guard schema.parse(parse_json("\"any\"")) is Err(_) else {
|
||||
fail("null enum: should reject string")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue