Compare commits

...

8 Commits

Author SHA1 Message Date
Betterlol 60f955dbcc [docs]: update API.md
MoonZod CI / build-and-test (ubuntu-latest) (push) Failing after 17m54s Details
MoonZod CI / build-and-test (macos-latest) (push) Has been cancelled Details
MoonZod CI / build-and-test (windows-latest) (push) Has been cancelled Details
2026-07-12 22:54:50 +08:00
Betterlol d52dc5030c [feat]: implement '.required_error()' / '.invalid_type_error()', and update docs. 2026-07-12 22:50:22 +08:00
Betterlol 7d46147e9b [feat]: add 'pub fn json_schema_to_prompt' in combinators. 2026-07-12 22:39:16 +08:00
Betterlol 60e8ac7715 [fix]: delete combinators/reexporter.mbt; and change refer with @importers and @exporters in schema_combinators.mbt. 2026-07-12 22:34:07 +08:00
Betterlol 3d6fdcc8dd [docs & fix]: fix enum and additionalProperties support in from_json_schema.mbt; add importers/README.md; update docs. 2026-07-12 22:30:32 +08:00
Betterlol 72ca625635 [docs & fix]: fix trait any/unknow/tuple/preprocess fn; add exporters/README.md; update docs. 2026-07-12 22:00:42 +08:00
Betterlol 5ce4612f8c [docs]: add vscode extension reference. 2026-07-12 20:23:41 +08:00
Betterlol 654cb5ed69 [mooncakes]: update moon.mod 2026-07-11 15:41:41 +08:00
24 changed files with 602 additions and 110 deletions

View File

@ -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)
---

View File

@ -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.

View File

@ -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) — 核心架构、设计决策与未来方向

View File

@ -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}

View File

@ -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
}

View File

@ -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

View File

@ -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 |

View File

@ -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)

View File

@ -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.

View File

@ -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 源代码 |

View File

@ -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

View File

@ -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) — 核心架构、设计决策与未来方向

115
exporters/README.md Normal file
View File

@ -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 schemasprimitive、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) }`,自定义闭包无法序列化。

View File

@ -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 ──
///|

View File

@ -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(

View File

@ -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(

View File

@ -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.

122
importers/README.md Normal file
View File

@ -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` 等元数据**:忽略。

View File

@ -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({})
}

View File

@ -11,7 +11,7 @@
name = "Betterlol/moon_zod"
version = "0.7.5"
version = "0.8.1"
readme = "README.mbt.md"

View File

@ -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}

View File

@ -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")
}

View File

@ -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")
}
}

View File

@ -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")
}
}