[docs & fix]: fix trait any/unknow/tuple/preprocess fn; add exporters/README.md; update docs.

This commit is contained in:
Betterlol 2026-07-12 22:00:42 +08:00
parent 5ce4612f8c
commit 72ca625635
12 changed files with 267 additions and 59 deletions

View File

@ -28,6 +28,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)
- `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)
**470 tests** (all passing, 0 warnings)

View File

@ -17,6 +17,7 @@
| [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 |
---
@ -126,8 +127,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 +135,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,7 @@
| [CLI 参考](./docs/zh/CLI.md) | 命令行使用说明 |
| [性能基准](./docs/zh/BENCHMARK.md) | 与其他校验库的性能对比 |
| [使用示例](./docs/zh/EXAMPLES.md) | 实际使用示例 |
| [导出器说明](./exporters/README.md) | moon_zod 的导出器说明 |
---
@ -126,8 +127,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 +135,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

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

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,7 @@
| [CLI 参考](./docs/zh/CLI.md) | 命令行使用说明 |
| [性能基准](./docs/zh/BENCHMARK.md) | 与其他校验库的性能对比 |
| [使用示例](./docs/zh/EXAMPLES.md) | 实际使用示例 |
| [导出器说明](./exporters/README.md) | moon_zod 的导出器说明 |
---

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) }`,自定义闭包无法序列化;`.required_error()` / `.invalid_type_error()` round-trip 不安全(工厂 API 未暴露这两个参数)。

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.