schema-dsl 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
|
@@ -1,305 +1,305 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MarkdownExporter — Export JSON Schema as human-readable Markdown documentation.
|
|
3
|
-
*
|
|
4
|
-
* v2 fix:
|
|
5
|
-
* EX-01: required check prefers prop._required, then falls back to schema.required?.includes(key)
|
|
6
|
-
* (v1 already had this logic; v2 preserves it with stronger type safety)
|
|
7
|
-
*
|
|
8
|
-
* @version 2.0.0
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { JSONSchema } from '../types/schema.js'
|
|
12
|
-
import { BaseExporter, type ExporterOptions } from './BaseExporter.js'
|
|
13
|
-
|
|
14
|
-
// ==================== Type definitions ====================
|
|
15
|
-
|
|
16
|
-
export interface MarkdownExporterOptions extends ExporterOptions {
|
|
17
|
-
title?: string
|
|
18
|
-
locale?: 'zh-CN' | 'en-US' | 'ja-JP' | 'fr-FR' | 'es-ES'
|
|
19
|
-
includeExample?: boolean
|
|
20
|
-
includeDescription?: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type Locale = 'zh-CN' | 'en-US' | 'ja-JP' | 'fr-FR' | 'es-ES'
|
|
24
|
-
|
|
25
|
-
// ==================== MarkdownExporter ====================
|
|
26
|
-
|
|
27
|
-
export class MarkdownExporter extends BaseExporter<MarkdownExporterOptions> {
|
|
28
|
-
constructor(options: Partial<MarkdownExporterOptions> = {}) {
|
|
29
|
-
super({
|
|
30
|
-
title: 'Schema Documentation',
|
|
31
|
-
locale: 'zh-CN',
|
|
32
|
-
includeExample: true,
|
|
33
|
-
includeDescription: true,
|
|
34
|
-
...options,
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Export as a Markdown document.
|
|
40
|
-
*/
|
|
41
|
-
export(schema: JSONSchema, options?: Partial<MarkdownExporterOptions>): string {
|
|
42
|
-
return MarkdownExporter.export(schema, { ...this.options, ...options })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Static method: export directly without instantiation.
|
|
47
|
-
*/
|
|
48
|
-
static export(schema: JSONSchema, options: Partial<MarkdownExporterOptions> = {}): string {
|
|
49
|
-
const {
|
|
50
|
-
title = 'Schema Documentation',
|
|
51
|
-
locale = 'zh-CN',
|
|
52
|
-
includeExample = true,
|
|
53
|
-
includeDescription = true,
|
|
54
|
-
} = options
|
|
55
|
-
|
|
56
|
-
let markdown = `# ${title}\n\n`
|
|
57
|
-
|
|
58
|
-
if (includeDescription && schema.description) {
|
|
59
|
-
markdown += `${schema.description}\n\n`
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
markdown += this._generateFieldsTable(schema, locale)
|
|
63
|
-
|
|
64
|
-
if (includeExample) {
|
|
65
|
-
markdown += '\n' + this._generateExample(schema, locale)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
markdown += '\n' + this._generateConstraintsSection(schema, locale)
|
|
69
|
-
|
|
70
|
-
return markdown
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ==================== Private static methods ====================
|
|
74
|
-
|
|
75
|
-
private static _i18nFields = {
|
|
76
|
-
'zh-CN': { fields: '字段列表', name: '字段名', type: '类型', required: '必填', constraints: '约束', description: '说明' },
|
|
77
|
-
'en-US': { fields: 'Fields', name: 'Field', type: 'Type', required: 'Required', constraints: 'Constraints', description: 'Description' },
|
|
78
|
-
'ja-JP': { fields: 'フィールド一覧', name: 'フィールド名', type: 'タイプ', required: '必須', constraints: '制約', description: '説明' },
|
|
79
|
-
'fr-FR': { fields: 'Liste des champs', name: 'Champ', type: 'Type', required: 'Obligatoire', constraints: 'Contraintes', description: 'Description' },
|
|
80
|
-
'es-ES': { fields: 'Lista de campos', name: 'Campo', type: 'Tipo', required: 'Requerido', constraints: 'Restricciones', description: 'Descripción' },
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private static _i18nTypes: Record<Locale, Record<string, string>> = {
|
|
84
|
-
'zh-CN': { string: '字符串', number: '数字', integer: '整数', boolean: '布尔值', array: '数组', object: '对象', email: '邮箱', url: '网址', date: '日期', uuid: 'UUID' },
|
|
85
|
-
'en-US': { string: 'String', number: 'Number', integer: 'Integer', boolean: 'Boolean', array: 'Array', object: 'Object', email: 'Email', url: 'URL', date: 'Date', uuid: 'UUID' },
|
|
86
|
-
'ja-JP': { string: '文字列', number: '数値', integer: '整数', boolean: 'ブール値', array: '配列', object: 'オブジェクト', email: 'メールアドレス', url: 'URL', date: '日付', uuid: 'UUID' },
|
|
87
|
-
'fr-FR': { string: 'Chaîne', number: 'Nombre', integer: 'Entier', boolean: 'Booléen', array: 'Tableau', object: 'Objet', email: 'E-mail', url: 'URL', date: 'Date', uuid: 'UUID' },
|
|
88
|
-
'es-ES': { string: 'Cadena', number: 'Número', integer: 'Entero', boolean: 'Booleano', array: 'Array', object: 'Objeto', email: 'Correo', url: 'URL', date: 'Fecha', uuid: 'UUID' },
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private static _i18nConstraints: Record<Locale, Record<string, string>> = {
|
|
92
|
-
'zh-CN': { length: '长度', range: '范围', pattern: '正则', enum: '枚举', items: '元素数' },
|
|
93
|
-
'en-US': { length: 'Length', range: 'Range', pattern: 'Pattern', enum: 'Enum', items: 'Items' },
|
|
94
|
-
'ja-JP': { length: '長さ', range: '範囲', pattern: '正規表現', enum: '列挙', items: '要素数' },
|
|
95
|
-
'fr-FR': { length: 'Longueur', range: 'Plage', pattern: 'Modèle', enum: 'Énumération', items: 'Éléments' },
|
|
96
|
-
'es-ES': { length: 'Longitud', range: 'Rango', pattern: 'Patrón', enum: 'Enumeración', items: 'Elementos' },
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private static _i18nRules: Record<Locale, Record<string, string>> = {
|
|
100
|
-
'zh-CN': { rules: '约束规则', required: '必填字段', optional: '可选字段' },
|
|
101
|
-
'en-US': { rules: 'Validation Rules', required: 'Required Fields', optional: 'Optional Fields' },
|
|
102
|
-
'ja-JP': { rules: '検証ルール', required: '必須フィールド', optional: 'オプションフィールド' },
|
|
103
|
-
'fr-FR': { rules: 'Règles de validation', required: 'Champs obligatoires', optional: 'Champs facultatifs' },
|
|
104
|
-
'es-ES': { rules: 'Reglas de validación', required: 'Campos requeridos', optional: 'Campos opcionales' },
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private static _generateFieldsTable(schema: JSONSchema, locale: Locale): string {
|
|
108
|
-
const t = this._i18nFields[locale] ?? this._i18nFields['en-US']
|
|
109
|
-
|
|
110
|
-
let table = `## ${t.fields}\n\n`
|
|
111
|
-
table += `| ${t.name} | ${t.type} | ${t.required} | ${t.constraints} | ${t.description} |\n`
|
|
112
|
-
table += `|--------|------|------|------|------|\n`
|
|
113
|
-
|
|
114
|
-
if (schema.properties) {
|
|
115
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
116
|
-
const type = this._escapeTableCell(this._formatType(prop, locale))
|
|
117
|
-
// EX-01 fix: prefer _required flag, then fall back to schema.required
|
|
118
|
-
const isRequired = !!(
|
|
119
|
-
(prop as Record<string, unknown>)['_required'] === true ||
|
|
120
|
-
schema.required?.includes(key)
|
|
121
|
-
)
|
|
122
|
-
const required = isRequired ? '✅' : '❌'
|
|
123
|
-
const constraints = this._escapeTableCell(this._formatConstraints(prop, locale))
|
|
124
|
-
const description = this._escapeTableCell(this._getDescription(prop, locale))
|
|
125
|
-
const fieldName = this._escapeTableCell(key)
|
|
126
|
-
|
|
127
|
-
table += `| ${fieldName} | ${type} | ${required} | ${constraints} | ${description} |\n`
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return table
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
private static _escapeTableCell(value: string): string {
|
|
135
|
-
return value
|
|
136
|
-
.replace(/\|/g, '\\|')
|
|
137
|
-
.replace(/\r\n|\r|\n/g, '<br>')
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private static _formatType(prop: JSONSchema, locale: Locale): string {
|
|
141
|
-
const t = this._i18nTypes[locale] ?? this._i18nTypes['en-US']
|
|
142
|
-
|
|
143
|
-
if (prop.format) {
|
|
144
|
-
return t[prop.format] ?? prop.format
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (prop.type === 'array' && prop.items) {
|
|
148
|
-
const itemType = this._formatType(prop.items as JSONSchema, locale)
|
|
149
|
-
return `${t['array'] ?? 'array'}<${itemType}>`
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return t[prop.type as string] ?? String(prop.type ?? 'any')
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private static _formatConstraints(prop: JSONSchema, locale: Locale): string {
|
|
156
|
-
const constraints: string[] = []
|
|
157
|
-
const t = this._i18nConstraints[locale] ?? this._i18nConstraints['en-US']
|
|
158
|
-
|
|
159
|
-
if (prop.minLength !== undefined || prop.maxLength !== undefined) {
|
|
160
|
-
if (prop.minLength !== undefined && prop.maxLength !== undefined) {
|
|
161
|
-
constraints.push(`${t['length']}: ${prop.minLength}-${prop.maxLength}`)
|
|
162
|
-
} else if (prop.minLength !== undefined) {
|
|
163
|
-
constraints.push(`${t['length']}: ≥${prop.minLength}`)
|
|
164
|
-
} else if (prop.maxLength !== undefined) {
|
|
165
|
-
constraints.push(`${t['length']}: ≤${prop.maxLength}`)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (prop.minimum !== undefined || prop.maximum !== undefined) {
|
|
170
|
-
if (prop.minimum !== undefined && prop.maximum !== undefined) {
|
|
171
|
-
constraints.push(`${t['range']}: ${prop.minimum}-${prop.maximum}`)
|
|
172
|
-
} else if (prop.minimum !== undefined) {
|
|
173
|
-
constraints.push(`${t['range']}: ≥${prop.minimum}`)
|
|
174
|
-
} else if (prop.maximum !== undefined) {
|
|
175
|
-
constraints.push(`${t['range']}: ≤${prop.maximum}`)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (prop.minItems !== undefined || prop.maxItems !== undefined) {
|
|
180
|
-
if (prop.minItems !== undefined && prop.maxItems !== undefined) {
|
|
181
|
-
constraints.push(`${t['items']}: ${prop.minItems}-${prop.maxItems}`)
|
|
182
|
-
} else if (prop.minItems !== undefined) {
|
|
183
|
-
constraints.push(`${t['items']}: ≥${prop.minItems}`)
|
|
184
|
-
} else if (prop.maxItems !== undefined) {
|
|
185
|
-
constraints.push(`${t['items']}: ≤${prop.maxItems}`)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (prop.pattern) {
|
|
190
|
-
constraints.push(`${t['pattern']}: \`${prop.pattern}\``)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (prop.enum) {
|
|
194
|
-
const enumStr = (prop.enum as unknown[]).map(v => `\`${String(v)}\``).join(', ')
|
|
195
|
-
constraints.push(`${t['enum']}: ${enumStr}`)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return constraints.length > 0 ? constraints.join('<br>') : '-'
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private static _getDescription(prop: JSONSchema, locale: Locale): string {
|
|
202
|
-
const p = prop as Record<string, unknown>
|
|
203
|
-
|
|
204
|
-
if (p['_labelI18n'] && typeof p['_labelI18n'] === 'object') {
|
|
205
|
-
const i18n = p['_labelI18n'] as Record<string, string>
|
|
206
|
-
if (i18n[locale]) return i18n[locale]
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (p['_label']) return p['_label'] as string
|
|
210
|
-
if (prop.description) return prop.description
|
|
211
|
-
|
|
212
|
-
return '-'
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private static _generateExample(schema: JSONSchema, locale: Locale): string {
|
|
216
|
-
const i18n: Record<Locale, { example: string }> = {
|
|
217
|
-
'zh-CN': { example: '示例数据' },
|
|
218
|
-
'en-US': { example: 'Example Data' },
|
|
219
|
-
'ja-JP': { example: 'サンプルデータ' },
|
|
220
|
-
'fr-FR': { example: "Données d'exemple" },
|
|
221
|
-
'es-ES': { example: 'Datos de ejemplo' },
|
|
222
|
-
}
|
|
223
|
-
const t = i18n[locale] ?? i18n['en-US']
|
|
224
|
-
|
|
225
|
-
let example = `## ${t.example}\n\n\`\`\`json\n`
|
|
226
|
-
example += JSON.stringify(this._buildExample(schema), null, 2)
|
|
227
|
-
example += '\n```\n'
|
|
228
|
-
return example
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private static _buildExample(schema: JSONSchema): unknown {
|
|
232
|
-
if (schema.properties) {
|
|
233
|
-
const obj: Record<string, unknown> = {}
|
|
234
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
235
|
-
const isRequired = !!(
|
|
236
|
-
(prop as Record<string, unknown>)['_required'] === true ||
|
|
237
|
-
schema.required?.includes(key)
|
|
238
|
-
)
|
|
239
|
-
if (isRequired) {
|
|
240
|
-
obj[key] = this._getExampleValue(prop)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return obj
|
|
244
|
-
}
|
|
245
|
-
return null
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private static _getExampleValue(prop: JSONSchema): unknown {
|
|
249
|
-
if (prop.default !== undefined) return prop.default
|
|
250
|
-
if (prop.enum) return (prop.enum as unknown[])[0]
|
|
251
|
-
|
|
252
|
-
switch (prop.type) {
|
|
253
|
-
case 'string':
|
|
254
|
-
if (prop.format === 'email') return 'user@example.com'
|
|
255
|
-
if (prop.format === 'uri' || prop.format === 'url') return 'https://example.com'
|
|
256
|
-
if (prop.format === 'date') return '2025-12-29'
|
|
257
|
-
if (prop.format === 'uuid') return '550e8400-e29b-41d4-a716-446655440000'
|
|
258
|
-
return 'example'
|
|
259
|
-
case 'number':
|
|
260
|
-
case 'integer':
|
|
261
|
-
if (prop.minimum !== undefined) return prop.minimum
|
|
262
|
-
if (prop.maximum !== undefined) return Math.floor(prop.maximum / 2)
|
|
263
|
-
return 0
|
|
264
|
-
case 'boolean':
|
|
265
|
-
return true
|
|
266
|
-
case 'array':
|
|
267
|
-
if (prop.items) return [this._getExampleValue(prop.items as JSONSchema)]
|
|
268
|
-
return []
|
|
269
|
-
case 'object':
|
|
270
|
-
if (prop.properties) return this._buildExample(prop)
|
|
271
|
-
return {}
|
|
272
|
-
default:
|
|
273
|
-
return null
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private static _generateConstraintsSection(schema: JSONSchema, locale: Locale): string {
|
|
278
|
-
if (!schema.properties) return ''
|
|
279
|
-
|
|
280
|
-
const t = this._i18nRules[locale] ?? this._i18nRules['en-US']
|
|
281
|
-
const requiredFields: string[] = []
|
|
282
|
-
const optionalFields: string[] = []
|
|
283
|
-
|
|
284
|
-
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
285
|
-
const isRequired = !!(
|
|
286
|
-
(prop as Record<string, unknown>)['_required'] === true ||
|
|
287
|
-
schema.required?.includes(key)
|
|
288
|
-
)
|
|
289
|
-
if (isRequired) requiredFields.push(key)
|
|
290
|
-
else optionalFields.push(key)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
let section = `## ${t['rules']}\n\n`
|
|
294
|
-
|
|
295
|
-
if (requiredFields.length > 0) {
|
|
296
|
-
section += `**${t['required']}**: ${requiredFields.map(f => `\`${f}\``).join(', ')}\n\n`
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (optionalFields.length > 0) {
|
|
300
|
-
section += `**${t['optional']}**: ${optionalFields.map(f => `\`${f}\``).join(', ')}\n`
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return section
|
|
304
|
-
}
|
|
305
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownExporter — Export JSON Schema as human-readable Markdown documentation.
|
|
3
|
+
*
|
|
4
|
+
* v2 fix:
|
|
5
|
+
* EX-01: required check prefers prop._required, then falls back to schema.required?.includes(key)
|
|
6
|
+
* (v1 already had this logic; v2 preserves it with stronger type safety)
|
|
7
|
+
*
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
12
|
+
import { BaseExporter, type ExporterOptions } from './BaseExporter.js'
|
|
13
|
+
|
|
14
|
+
// ==================== Type definitions ====================
|
|
15
|
+
|
|
16
|
+
export interface MarkdownExporterOptions extends ExporterOptions {
|
|
17
|
+
title?: string
|
|
18
|
+
locale?: 'zh-CN' | 'en-US' | 'ja-JP' | 'fr-FR' | 'es-ES'
|
|
19
|
+
includeExample?: boolean
|
|
20
|
+
includeDescription?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Locale = 'zh-CN' | 'en-US' | 'ja-JP' | 'fr-FR' | 'es-ES'
|
|
24
|
+
|
|
25
|
+
// ==================== MarkdownExporter ====================
|
|
26
|
+
|
|
27
|
+
export class MarkdownExporter extends BaseExporter<MarkdownExporterOptions> {
|
|
28
|
+
constructor(options: Partial<MarkdownExporterOptions> = {}) {
|
|
29
|
+
super({
|
|
30
|
+
title: 'Schema Documentation',
|
|
31
|
+
locale: 'zh-CN',
|
|
32
|
+
includeExample: true,
|
|
33
|
+
includeDescription: true,
|
|
34
|
+
...options,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Export as a Markdown document.
|
|
40
|
+
*/
|
|
41
|
+
export(schema: JSONSchema, options?: Partial<MarkdownExporterOptions>): string {
|
|
42
|
+
return MarkdownExporter.export(schema, { ...this.options, ...options })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Static method: export directly without instantiation.
|
|
47
|
+
*/
|
|
48
|
+
static export(schema: JSONSchema, options: Partial<MarkdownExporterOptions> = {}): string {
|
|
49
|
+
const {
|
|
50
|
+
title = 'Schema Documentation',
|
|
51
|
+
locale = 'zh-CN',
|
|
52
|
+
includeExample = true,
|
|
53
|
+
includeDescription = true,
|
|
54
|
+
} = options
|
|
55
|
+
|
|
56
|
+
let markdown = `# ${title}\n\n`
|
|
57
|
+
|
|
58
|
+
if (includeDescription && schema.description) {
|
|
59
|
+
markdown += `${schema.description}\n\n`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
markdown += this._generateFieldsTable(schema, locale)
|
|
63
|
+
|
|
64
|
+
if (includeExample) {
|
|
65
|
+
markdown += '\n' + this._generateExample(schema, locale)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
markdown += '\n' + this._generateConstraintsSection(schema, locale)
|
|
69
|
+
|
|
70
|
+
return markdown
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ==================== Private static methods ====================
|
|
74
|
+
|
|
75
|
+
private static _i18nFields = {
|
|
76
|
+
'zh-CN': { fields: '字段列表', name: '字段名', type: '类型', required: '必填', constraints: '约束', description: '说明' },
|
|
77
|
+
'en-US': { fields: 'Fields', name: 'Field', type: 'Type', required: 'Required', constraints: 'Constraints', description: 'Description' },
|
|
78
|
+
'ja-JP': { fields: 'フィールド一覧', name: 'フィールド名', type: 'タイプ', required: '必須', constraints: '制約', description: '説明' },
|
|
79
|
+
'fr-FR': { fields: 'Liste des champs', name: 'Champ', type: 'Type', required: 'Obligatoire', constraints: 'Contraintes', description: 'Description' },
|
|
80
|
+
'es-ES': { fields: 'Lista de campos', name: 'Campo', type: 'Tipo', required: 'Requerido', constraints: 'Restricciones', description: 'Descripción' },
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static _i18nTypes: Record<Locale, Record<string, string>> = {
|
|
84
|
+
'zh-CN': { string: '字符串', number: '数字', integer: '整数', boolean: '布尔值', array: '数组', object: '对象', email: '邮箱', url: '网址', date: '日期', uuid: 'UUID' },
|
|
85
|
+
'en-US': { string: 'String', number: 'Number', integer: 'Integer', boolean: 'Boolean', array: 'Array', object: 'Object', email: 'Email', url: 'URL', date: 'Date', uuid: 'UUID' },
|
|
86
|
+
'ja-JP': { string: '文字列', number: '数値', integer: '整数', boolean: 'ブール値', array: '配列', object: 'オブジェクト', email: 'メールアドレス', url: 'URL', date: '日付', uuid: 'UUID' },
|
|
87
|
+
'fr-FR': { string: 'Chaîne', number: 'Nombre', integer: 'Entier', boolean: 'Booléen', array: 'Tableau', object: 'Objet', email: 'E-mail', url: 'URL', date: 'Date', uuid: 'UUID' },
|
|
88
|
+
'es-ES': { string: 'Cadena', number: 'Número', integer: 'Entero', boolean: 'Booleano', array: 'Array', object: 'Objeto', email: 'Correo', url: 'URL', date: 'Fecha', uuid: 'UUID' },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static _i18nConstraints: Record<Locale, Record<string, string>> = {
|
|
92
|
+
'zh-CN': { length: '长度', range: '范围', pattern: '正则', enum: '枚举', items: '元素数' },
|
|
93
|
+
'en-US': { length: 'Length', range: 'Range', pattern: 'Pattern', enum: 'Enum', items: 'Items' },
|
|
94
|
+
'ja-JP': { length: '長さ', range: '範囲', pattern: '正規表現', enum: '列挙', items: '要素数' },
|
|
95
|
+
'fr-FR': { length: 'Longueur', range: 'Plage', pattern: 'Modèle', enum: 'Énumération', items: 'Éléments' },
|
|
96
|
+
'es-ES': { length: 'Longitud', range: 'Rango', pattern: 'Patrón', enum: 'Enumeración', items: 'Elementos' },
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static _i18nRules: Record<Locale, Record<string, string>> = {
|
|
100
|
+
'zh-CN': { rules: '约束规则', required: '必填字段', optional: '可选字段' },
|
|
101
|
+
'en-US': { rules: 'Validation Rules', required: 'Required Fields', optional: 'Optional Fields' },
|
|
102
|
+
'ja-JP': { rules: '検証ルール', required: '必須フィールド', optional: 'オプションフィールド' },
|
|
103
|
+
'fr-FR': { rules: 'Règles de validation', required: 'Champs obligatoires', optional: 'Champs facultatifs' },
|
|
104
|
+
'es-ES': { rules: 'Reglas de validación', required: 'Campos requeridos', optional: 'Campos opcionales' },
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static _generateFieldsTable(schema: JSONSchema, locale: Locale): string {
|
|
108
|
+
const t = this._i18nFields[locale] ?? this._i18nFields['en-US']
|
|
109
|
+
|
|
110
|
+
let table = `## ${t.fields}\n\n`
|
|
111
|
+
table += `| ${t.name} | ${t.type} | ${t.required} | ${t.constraints} | ${t.description} |\n`
|
|
112
|
+
table += `|--------|------|------|------|------|\n`
|
|
113
|
+
|
|
114
|
+
if (schema.properties) {
|
|
115
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
116
|
+
const type = this._escapeTableCell(this._formatType(prop, locale))
|
|
117
|
+
// EX-01 fix: prefer _required flag, then fall back to schema.required
|
|
118
|
+
const isRequired = !!(
|
|
119
|
+
(prop as Record<string, unknown>)['_required'] === true ||
|
|
120
|
+
schema.required?.includes(key)
|
|
121
|
+
)
|
|
122
|
+
const required = isRequired ? '✅' : '❌'
|
|
123
|
+
const constraints = this._escapeTableCell(this._formatConstraints(prop, locale))
|
|
124
|
+
const description = this._escapeTableCell(this._getDescription(prop, locale))
|
|
125
|
+
const fieldName = this._escapeTableCell(key)
|
|
126
|
+
|
|
127
|
+
table += `| ${fieldName} | ${type} | ${required} | ${constraints} | ${description} |\n`
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return table
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private static _escapeTableCell(value: string): string {
|
|
135
|
+
return value
|
|
136
|
+
.replace(/\|/g, '\\|')
|
|
137
|
+
.replace(/\r\n|\r|\n/g, '<br>')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private static _formatType(prop: JSONSchema, locale: Locale): string {
|
|
141
|
+
const t = this._i18nTypes[locale] ?? this._i18nTypes['en-US']
|
|
142
|
+
|
|
143
|
+
if (prop.format) {
|
|
144
|
+
return t[prop.format] ?? prop.format
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (prop.type === 'array' && prop.items) {
|
|
148
|
+
const itemType = this._formatType(prop.items as JSONSchema, locale)
|
|
149
|
+
return `${t['array'] ?? 'array'}<${itemType}>`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return t[prop.type as string] ?? String(prop.type ?? 'any')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private static _formatConstraints(prop: JSONSchema, locale: Locale): string {
|
|
156
|
+
const constraints: string[] = []
|
|
157
|
+
const t = this._i18nConstraints[locale] ?? this._i18nConstraints['en-US']
|
|
158
|
+
|
|
159
|
+
if (prop.minLength !== undefined || prop.maxLength !== undefined) {
|
|
160
|
+
if (prop.minLength !== undefined && prop.maxLength !== undefined) {
|
|
161
|
+
constraints.push(`${t['length']}: ${prop.minLength}-${prop.maxLength}`)
|
|
162
|
+
} else if (prop.minLength !== undefined) {
|
|
163
|
+
constraints.push(`${t['length']}: ≥${prop.minLength}`)
|
|
164
|
+
} else if (prop.maxLength !== undefined) {
|
|
165
|
+
constraints.push(`${t['length']}: ≤${prop.maxLength}`)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (prop.minimum !== undefined || prop.maximum !== undefined) {
|
|
170
|
+
if (prop.minimum !== undefined && prop.maximum !== undefined) {
|
|
171
|
+
constraints.push(`${t['range']}: ${prop.minimum}-${prop.maximum}`)
|
|
172
|
+
} else if (prop.minimum !== undefined) {
|
|
173
|
+
constraints.push(`${t['range']}: ≥${prop.minimum}`)
|
|
174
|
+
} else if (prop.maximum !== undefined) {
|
|
175
|
+
constraints.push(`${t['range']}: ≤${prop.maximum}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (prop.minItems !== undefined || prop.maxItems !== undefined) {
|
|
180
|
+
if (prop.minItems !== undefined && prop.maxItems !== undefined) {
|
|
181
|
+
constraints.push(`${t['items']}: ${prop.minItems}-${prop.maxItems}`)
|
|
182
|
+
} else if (prop.minItems !== undefined) {
|
|
183
|
+
constraints.push(`${t['items']}: ≥${prop.minItems}`)
|
|
184
|
+
} else if (prop.maxItems !== undefined) {
|
|
185
|
+
constraints.push(`${t['items']}: ≤${prop.maxItems}`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (prop.pattern) {
|
|
190
|
+
constraints.push(`${t['pattern']}: \`${prop.pattern}\``)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (prop.enum) {
|
|
194
|
+
const enumStr = (prop.enum as unknown[]).map(v => `\`${String(v)}\``).join(', ')
|
|
195
|
+
constraints.push(`${t['enum']}: ${enumStr}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return constraints.length > 0 ? constraints.join('<br>') : '-'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private static _getDescription(prop: JSONSchema, locale: Locale): string {
|
|
202
|
+
const p = prop as Record<string, unknown>
|
|
203
|
+
|
|
204
|
+
if (p['_labelI18n'] && typeof p['_labelI18n'] === 'object') {
|
|
205
|
+
const i18n = p['_labelI18n'] as Record<string, string>
|
|
206
|
+
if (i18n[locale]) return i18n[locale]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (p['_label']) return p['_label'] as string
|
|
210
|
+
if (prop.description) return prop.description
|
|
211
|
+
|
|
212
|
+
return '-'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private static _generateExample(schema: JSONSchema, locale: Locale): string {
|
|
216
|
+
const i18n: Record<Locale, { example: string }> = {
|
|
217
|
+
'zh-CN': { example: '示例数据' },
|
|
218
|
+
'en-US': { example: 'Example Data' },
|
|
219
|
+
'ja-JP': { example: 'サンプルデータ' },
|
|
220
|
+
'fr-FR': { example: "Données d'exemple" },
|
|
221
|
+
'es-ES': { example: 'Datos de ejemplo' },
|
|
222
|
+
}
|
|
223
|
+
const t = i18n[locale] ?? i18n['en-US']
|
|
224
|
+
|
|
225
|
+
let example = `## ${t.example}\n\n\`\`\`json\n`
|
|
226
|
+
example += JSON.stringify(this._buildExample(schema), null, 2)
|
|
227
|
+
example += '\n```\n'
|
|
228
|
+
return example
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private static _buildExample(schema: JSONSchema): unknown {
|
|
232
|
+
if (schema.properties) {
|
|
233
|
+
const obj: Record<string, unknown> = {}
|
|
234
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
235
|
+
const isRequired = !!(
|
|
236
|
+
(prop as Record<string, unknown>)['_required'] === true ||
|
|
237
|
+
schema.required?.includes(key)
|
|
238
|
+
)
|
|
239
|
+
if (isRequired) {
|
|
240
|
+
obj[key] = this._getExampleValue(prop)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return obj
|
|
244
|
+
}
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static _getExampleValue(prop: JSONSchema): unknown {
|
|
249
|
+
if (prop.default !== undefined) return prop.default
|
|
250
|
+
if (prop.enum) return (prop.enum as unknown[])[0]
|
|
251
|
+
|
|
252
|
+
switch (prop.type) {
|
|
253
|
+
case 'string':
|
|
254
|
+
if (prop.format === 'email') return 'user@example.com'
|
|
255
|
+
if (prop.format === 'uri' || prop.format === 'url') return 'https://example.com'
|
|
256
|
+
if (prop.format === 'date') return '2025-12-29'
|
|
257
|
+
if (prop.format === 'uuid') return '550e8400-e29b-41d4-a716-446655440000'
|
|
258
|
+
return 'example'
|
|
259
|
+
case 'number':
|
|
260
|
+
case 'integer':
|
|
261
|
+
if (prop.minimum !== undefined) return prop.minimum
|
|
262
|
+
if (prop.maximum !== undefined) return Math.floor(prop.maximum / 2)
|
|
263
|
+
return 0
|
|
264
|
+
case 'boolean':
|
|
265
|
+
return true
|
|
266
|
+
case 'array':
|
|
267
|
+
if (prop.items) return [this._getExampleValue(prop.items as JSONSchema)]
|
|
268
|
+
return []
|
|
269
|
+
case 'object':
|
|
270
|
+
if (prop.properties) return this._buildExample(prop)
|
|
271
|
+
return {}
|
|
272
|
+
default:
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static _generateConstraintsSection(schema: JSONSchema, locale: Locale): string {
|
|
278
|
+
if (!schema.properties) return ''
|
|
279
|
+
|
|
280
|
+
const t = this._i18nRules[locale] ?? this._i18nRules['en-US']
|
|
281
|
+
const requiredFields: string[] = []
|
|
282
|
+
const optionalFields: string[] = []
|
|
283
|
+
|
|
284
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
285
|
+
const isRequired = !!(
|
|
286
|
+
(prop as Record<string, unknown>)['_required'] === true ||
|
|
287
|
+
schema.required?.includes(key)
|
|
288
|
+
)
|
|
289
|
+
if (isRequired) requiredFields.push(key)
|
|
290
|
+
else optionalFields.push(key)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let section = `## ${t['rules']}\n\n`
|
|
294
|
+
|
|
295
|
+
if (requiredFields.length > 0) {
|
|
296
|
+
section += `**${t['required']}**: ${requiredFields.map(f => `\`${f}\``).join(', ')}\n\n`
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (optionalFields.length > 0) {
|
|
300
|
+
section += `**${t['optional']}**: ${optionalFields.map(f => `\`${f}\``).join(', ')}\n`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return section
|
|
304
|
+
}
|
|
305
|
+
}
|