schema-dsl 1.0.0
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/.eslintignore +10 -0
- package/.eslintrc.json +27 -0
- package/.github/CODE_OF_CONDUCT.md +45 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
- package/.github/ISSUE_TEMPLATE/question.md +31 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
- package/.github/SECURITY.md +184 -0
- package/.github/workflows/ci.yml +33 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1184 -0
- package/STATUS.md +101 -0
- package/docs/FEATURE-INDEX.md +519 -0
- package/docs/INDEX.md +253 -0
- package/docs/api-reference.md +1096 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +601 -0
- package/docs/dsl-syntax.md +653 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +462 -0
- package/docs/export-limitations.md +551 -0
- package/docs/faq.md +577 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +476 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +397 -0
- package/docs/mongodb-exporter.md +295 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +273 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +304 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils-chaining.md +143 -0
- package/docs/schema-utils.md +490 -0
- package/docs/string-extensions.md +480 -0
- package/docs/troubleshooting.md +471 -0
- package/docs/type-converter.md +319 -0
- package/docs/type-reference.md +219 -0
- package/docs/validate-async.md +480 -0
- package/docs/validate.md +486 -0
- package/docs/validation-guide.md +484 -0
- package/examples/array-dsl-example.js +227 -0
- package/examples/custom-extension.js +85 -0
- package/examples/dsl-match-example.js +74 -0
- package/examples/dsl-style.js +118 -0
- package/examples/dynamic-locale-configuration.js +348 -0
- package/examples/dynamic-locale-example.js +287 -0
- package/examples/export-demo.js +130 -0
- package/examples/express-integration.js +376 -0
- package/examples/i18n-full-demo.js +310 -0
- package/examples/i18n-memory-safety.examples.js +268 -0
- package/examples/markdown-export.js +71 -0
- package/examples/middleware-usage.js +93 -0
- package/examples/new-features-comparison.js +315 -0
- package/examples/password-reset/README.md +153 -0
- package/examples/password-reset/schema.js +26 -0
- package/examples/password-reset/test.js +101 -0
- package/examples/plugin-system.examples.js +205 -0
- package/examples/schema-utils-chaining.examples.js +250 -0
- package/examples/simple-example.js +122 -0
- package/examples/string-extensions.js +297 -0
- package/examples/user-registration/README.md +156 -0
- package/examples/user-registration/routes.js +92 -0
- package/examples/user-registration/schema.js +150 -0
- package/examples/user-registration/server.js +74 -0
- package/index.d.ts +1999 -0
- package/index.js +282 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +699 -0
- package/lib/adapters/index.js +20 -0
- package/lib/config/constants.js +286 -0
- package/lib/config/patterns/creditCard.js +9 -0
- package/lib/config/patterns/idCard.js +9 -0
- package/lib/config/patterns/index.js +8 -0
- package/lib/config/patterns/licensePlate.js +4 -0
- package/lib/config/patterns/passport.js +4 -0
- package/lib/config/patterns/phone.js +9 -0
- package/lib/config/patterns/postalCode.js +5 -0
- package/lib/core/CacheManager.js +376 -0
- package/lib/core/DslBuilder.js +740 -0
- package/lib/core/ErrorCodes.js +233 -0
- package/lib/core/ErrorFormatter.js +342 -0
- package/lib/core/JSONSchemaCore.js +347 -0
- package/lib/core/Locale.js +119 -0
- package/lib/core/MessageTemplate.js +89 -0
- package/lib/core/PluginManager.js +448 -0
- package/lib/core/StringExtensions.js +209 -0
- package/lib/core/Validator.js +376 -0
- package/lib/errors/ValidationError.js +191 -0
- package/lib/exporters/MarkdownExporter.js +420 -0
- package/lib/exporters/MongoDBExporter.js +162 -0
- package/lib/exporters/MySQLExporter.js +212 -0
- package/lib/exporters/PostgreSQLExporter.js +289 -0
- package/lib/exporters/index.js +24 -0
- package/lib/locales/en-US.js +65 -0
- package/lib/locales/es-ES.js +66 -0
- package/lib/locales/fr-FR.js +66 -0
- package/lib/locales/index.js +8 -0
- package/lib/locales/ja-JP.js +66 -0
- package/lib/locales/zh-CN.js +93 -0
- package/lib/utils/LRUCache.js +174 -0
- package/lib/utils/SchemaHelper.js +240 -0
- package/lib/utils/SchemaUtils.js +445 -0
- package/lib/utils/TypeConverter.js +245 -0
- package/lib/utils/index.js +13 -0
- package/lib/validators/CustomKeywords.js +203 -0
- package/lib/validators/index.js +11 -0
- package/package.json +70 -0
- package/plugins/custom-format.js +101 -0
- package/plugins/custom-validator.js +200 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n 内存安全示例
|
|
3
|
+
*
|
|
4
|
+
* 演示如何使用 LRU 缓存防止内存泄漏
|
|
5
|
+
* 正确实现:缓存完整的语言包对象,而非单个消息
|
|
6
|
+
*
|
|
7
|
+
* @version 2.2.1
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Validator, Locale } = require('../index');
|
|
11
|
+
const LRUCache = require('../lib/utils/LRUCache');
|
|
12
|
+
|
|
13
|
+
console.log('========== i18n 内存安全示例 ==========\n');
|
|
14
|
+
|
|
15
|
+
// ========== 1. 问题:无限制缓存导致内存泄漏 ==========
|
|
16
|
+
|
|
17
|
+
console.log('❌ 问题场景:无限制语言包缓存');
|
|
18
|
+
console.log('-----------------------------------');
|
|
19
|
+
|
|
20
|
+
class UnsafeLocale {
|
|
21
|
+
static languagePackCache = new Map(); // ⚠️ 无容量限制
|
|
22
|
+
|
|
23
|
+
static getLanguagePack(locale) {
|
|
24
|
+
if (!this.languagePackCache.has(locale)) {
|
|
25
|
+
// 模拟加载整个语言包(包含所有错误消息)
|
|
26
|
+
const pack = this.loadLanguagePack(locale);
|
|
27
|
+
this.languagePackCache.set(locale, pack); // ⚠️ 永远不清理
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return this.languagePackCache.get(locale);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static loadLanguagePack(locale) {
|
|
34
|
+
// 模拟加载完整语言包(10个错误类型)
|
|
35
|
+
const pack = {};
|
|
36
|
+
const errorTypes = ['required', 'minLength', 'maxLength', 'pattern', 'email',
|
|
37
|
+
'url', 'type', 'enum', 'minimum', 'maximum'];
|
|
38
|
+
errorTypes.forEach(type => {
|
|
39
|
+
pack[type] = `[${locale}] ${type} error message`;
|
|
40
|
+
});
|
|
41
|
+
return pack;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static getMessage(locale, key) {
|
|
45
|
+
const pack = this.getLanguagePack(locale);
|
|
46
|
+
return pack[key] || `Unknown error: ${key}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 模拟多租户场景:100个租户使用50种不同语言
|
|
51
|
+
console.log('\n模拟 100 个租户,使用 50 种不同语言...');
|
|
52
|
+
const locales = [];
|
|
53
|
+
for (let i = 1; i <= 50; i++) {
|
|
54
|
+
locales.push(`lang-${i}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (let tenant = 1; tenant <= 100; tenant++) {
|
|
58
|
+
const locale = locales[tenant % 50]; // 每个租户使用不同语言
|
|
59
|
+
UnsafeLocale.getMessage(locale, 'required');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(`缓存语言包数: ${UnsafeLocale.languagePackCache.size}`);
|
|
63
|
+
console.log('⚠️ 风险:50种语言 = 50个语言包缓存,无限增长');
|
|
64
|
+
|
|
65
|
+
// ========== 2. 解决方案:LRU 缓存语言包 ==========
|
|
66
|
+
|
|
67
|
+
console.log('\n\n✅ 解决方案:LRU 缓存(推荐)');
|
|
68
|
+
console.log('-----------------------------------');
|
|
69
|
+
console.log('关键:一个语言只有一个缓存(缓存完整语言包对象)\n');
|
|
70
|
+
|
|
71
|
+
class SafeLocale {
|
|
72
|
+
// ✅ 使用 LRU 缓存,最多缓存 10 个语言包
|
|
73
|
+
static languagePackCache = new LRUCache({
|
|
74
|
+
maxSize: 10,
|
|
75
|
+
enableStats: true
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
static getLanguagePack(locale) {
|
|
79
|
+
// 尝试从缓存获取
|
|
80
|
+
let pack = this.languagePackCache.get(locale);
|
|
81
|
+
|
|
82
|
+
if (!pack) {
|
|
83
|
+
// 缓存未命中,加载语言包
|
|
84
|
+
pack = this.loadLanguagePack(locale);
|
|
85
|
+
this.languagePackCache.set(locale, pack); // ✅ 自动清理最少使用的语言包
|
|
86
|
+
console.log(` [加载] 语言包: ${locale}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return pack;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static loadLanguagePack(locale) {
|
|
93
|
+
// 模拟加载完整语言包
|
|
94
|
+
const pack = {};
|
|
95
|
+
const errorTypes = ['required', 'minLength', 'maxLength', 'pattern', 'email',
|
|
96
|
+
'url', 'type', 'enum', 'minimum', 'maximum'];
|
|
97
|
+
errorTypes.forEach(type => {
|
|
98
|
+
pack[type] = `[${locale}] ${type} error message`;
|
|
99
|
+
});
|
|
100
|
+
return pack;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static getMessage(locale, key) {
|
|
104
|
+
const pack = this.getLanguagePack(locale);
|
|
105
|
+
return pack[key] || `Unknown error: ${key}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static getStats() {
|
|
109
|
+
return this.languagePackCache.getStats();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 模拟相同场景:100个租户使用50种语言
|
|
114
|
+
console.log('模拟 100 个租户,使用 50 种不同语言...');
|
|
115
|
+
for (let tenant = 1; tenant <= 100; tenant++) {
|
|
116
|
+
const locale = locales[tenant % 50];
|
|
117
|
+
SafeLocale.getMessage(locale, 'required');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const stats = SafeLocale.getStats();
|
|
121
|
+
console.log(`\n缓存语言包数: ${SafeLocale.languagePackCache.size} / 10 (maxSize)`);
|
|
122
|
+
console.log(`加载次数: ${stats.sets}`);
|
|
123
|
+
console.log(`命中次数: ${stats.hits}`);
|
|
124
|
+
console.log(`未命中次数: ${stats.misses}`);
|
|
125
|
+
console.log(`驱逐次数: ${stats.evictions}`);
|
|
126
|
+
const hitRate = stats.hits + stats.misses > 0
|
|
127
|
+
? (stats.hits / (stats.hits + stats.misses) * 100).toFixed(2)
|
|
128
|
+
: '0.00';
|
|
129
|
+
console.log(`命中率: ${hitRate}%`);
|
|
130
|
+
console.log('✅ 优势:最多只缓存10个语言包,内存恒定可控');
|
|
131
|
+
console.log('💡 说明:50种语言但只缓存最常用的10种,冷门语言自动清理');
|
|
132
|
+
|
|
133
|
+
// ========== 3. 并发场景测试 ==========
|
|
134
|
+
|
|
135
|
+
console.log('\n\n🔄 并发场景测试');
|
|
136
|
+
console.log('-----------------------------------');
|
|
137
|
+
|
|
138
|
+
class ConcurrentSafeLocale {
|
|
139
|
+
static languagePackCache = new LRUCache({ maxSize: 5, enableStats: true });
|
|
140
|
+
|
|
141
|
+
static getLanguagePack(locale) {
|
|
142
|
+
let pack = this.languagePackCache.get(locale);
|
|
143
|
+
if (!pack) {
|
|
144
|
+
pack = { required: `[${locale}] required`, email: `[${locale}] email` };
|
|
145
|
+
this.languagePackCache.set(locale, pack);
|
|
146
|
+
}
|
|
147
|
+
return pack;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 模拟并发请求:10个并发请求,使用不同语言
|
|
152
|
+
console.log('模拟 10 个并发请求...');
|
|
153
|
+
const concurrentRequests = [];
|
|
154
|
+
const testLocales = ['zh-CN', 'en-US', 'ja-JP', 'es-ES', 'fr-FR', 'ko-KR', 'de-DE', 'it-IT'];
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < 10; i++) {
|
|
157
|
+
const locale = testLocales[i % testLocales.length];
|
|
158
|
+
concurrentRequests.push(
|
|
159
|
+
Promise.resolve().then(() => {
|
|
160
|
+
const pack = ConcurrentSafeLocale.getLanguagePack(locale);
|
|
161
|
+
return { requestId: i + 1, locale, message: pack.required };
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Promise.all(concurrentRequests).then(results => {
|
|
167
|
+
console.log('\n并发请求结果:');
|
|
168
|
+
results.forEach(r => {
|
|
169
|
+
console.log(` 请求${r.requestId}: ${r.locale} → ${r.message}`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const concStats = ConcurrentSafeLocale.languagePackCache.getStats();
|
|
173
|
+
console.log(`\n缓存状态: ${ConcurrentSafeLocale.languagePackCache.size} 个语言包`);
|
|
174
|
+
console.log(`最多缓存: 5 个语言包 (maxSize)`);
|
|
175
|
+
console.log('✅ 验证:8种语言访问,只缓存最近使用的5种');
|
|
176
|
+
|
|
177
|
+
continueDemo();
|
|
178
|
+
}).catch(err => {
|
|
179
|
+
console.error('并发测试错误:', err);
|
|
180
|
+
continueDemo();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function continueDemo() {
|
|
184
|
+
// ========== 4. 内存监控示例 ==========
|
|
185
|
+
|
|
186
|
+
console.log('\n\n📊 内存监控示例');
|
|
187
|
+
console.log('-----------------------------------');
|
|
188
|
+
|
|
189
|
+
function formatBytes(bytes) {
|
|
190
|
+
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const memBefore = process.memoryUsage();
|
|
194
|
+
console.log(`初始内存: ${formatBytes(memBefore.heapUsed)}`);
|
|
195
|
+
|
|
196
|
+
// 创建大量语言包测试内存占用
|
|
197
|
+
const testCache = new LRUCache({ maxSize: 10 });
|
|
198
|
+
for (let i = 0; i < 1000; i++) {
|
|
199
|
+
// 模拟语言包对象(包含50个错误消息)
|
|
200
|
+
const languagePack = {};
|
|
201
|
+
for (let j = 0; j < 50; j++) {
|
|
202
|
+
languagePack[`error_${j}`] = `Language ${i} error message ${j}`;
|
|
203
|
+
}
|
|
204
|
+
testCache.set(`lang-${i}`, languagePack);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const memAfter = process.memoryUsage();
|
|
208
|
+
console.log(`处理后内存: ${formatBytes(memAfter.heapUsed)}`);
|
|
209
|
+
console.log(`内存增长: ${formatBytes(memAfter.heapUsed - memBefore.heapUsed)}`);
|
|
210
|
+
console.log(`缓存语言包: ${testCache.size} / 10 (maxSize)`);
|
|
211
|
+
console.log('✅ 验证:1000种语言加载,只缓存最近的10种');
|
|
212
|
+
|
|
213
|
+
// ========== 5. 最佳实践建议 ==========
|
|
214
|
+
|
|
215
|
+
console.log('\n\n📚 最佳实践建议');
|
|
216
|
+
console.log('-----------------------------------');
|
|
217
|
+
console.log(`
|
|
218
|
+
🔑 核心原则:一个语言只有一个缓存(缓存完整语言包对象)
|
|
219
|
+
|
|
220
|
+
1️⃣ 使用 LRU 缓存防止内存泄漏
|
|
221
|
+
✅ 缓存语言包对象,不是单个消息
|
|
222
|
+
✅ 设置合理的 maxSize(推荐 10-20种语言)
|
|
223
|
+
✅ 启用统计功能监控命中率
|
|
224
|
+
✅ 生产环境定期检查内存使用
|
|
225
|
+
|
|
226
|
+
2️⃣ 正确的缓存策略
|
|
227
|
+
✅ 缓存键:locale(如 "zh-CN")
|
|
228
|
+
✅ 缓存值:完整语言包 { required: "...", minLength: "..." }
|
|
229
|
+
❌ 错误:缓存 "zh-CN:required" 这样的组合键
|
|
230
|
+
|
|
231
|
+
示例:
|
|
232
|
+
// ✅ 正确
|
|
233
|
+
cache.set('zh-CN', { required: '必填', email: '邮箱格式' });
|
|
234
|
+
|
|
235
|
+
// ❌ 错误(浪费内存)
|
|
236
|
+
cache.set('zh-CN:required', '必填');
|
|
237
|
+
cache.set('zh-CN:email', '邮箱格式');
|
|
238
|
+
|
|
239
|
+
3️⃣ 多租户场景推荐方案
|
|
240
|
+
选项 A:实例级配置(每个请求创建独立 Validator)⭐⭐⭐⭐⭐
|
|
241
|
+
选项 B:请求级传参(validator.validate(data, { locale }))⭐⭐⭐⭐
|
|
242
|
+
选项 C:中间件统一处理(req.locale)⭐⭐⭐
|
|
243
|
+
|
|
244
|
+
4️⃣ 前端动态切换语言
|
|
245
|
+
⚠️ 避免使用全局 Locale.setLocale()
|
|
246
|
+
✅ 通过请求头传递 Accept-Language
|
|
247
|
+
✅ 每个请求创建独立验证器实例
|
|
248
|
+
|
|
249
|
+
5️⃣ 内存监控
|
|
250
|
+
• 开发环境:启用 enableStats 追踪缓存效率
|
|
251
|
+
• 生产环境:定期检查 process.memoryUsage()
|
|
252
|
+
• 告警阈值:堆内存超过容器限制的 80%
|
|
253
|
+
|
|
254
|
+
6️⃣ 性能优化
|
|
255
|
+
• 热门语言保持在缓存中(中文、英文)
|
|
256
|
+
• 冷门语言按需加载,自动驱逐
|
|
257
|
+
• 命中率目标:> 90%(取决于语言分布)
|
|
258
|
+
• maxSize 建议:支持语言数 × 0.5 ~ 1.0
|
|
259
|
+
|
|
260
|
+
详细文档:
|
|
261
|
+
- docs/i18n-analysis.md - 完整架构分析
|
|
262
|
+
- docs/frontend-i18n-guide.md - 前端集成指南
|
|
263
|
+
`);
|
|
264
|
+
|
|
265
|
+
console.log('\n✅ i18n 内存安全示例完成!');
|
|
266
|
+
console.log('💡 提示:运行 npm test 验证所有功能');
|
|
267
|
+
}
|
|
268
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 导出器示例
|
|
3
|
+
*
|
|
4
|
+
* 演示如何将 Schema 导出为 Markdown 文档
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { dsl, exporters } = require('../index');
|
|
8
|
+
|
|
9
|
+
console.log('========== Markdown 导出器示例 ==========\n');
|
|
10
|
+
|
|
11
|
+
// 示例 1: 简单用户 Schema
|
|
12
|
+
console.log('【示例 1】用户注册 API\n');
|
|
13
|
+
|
|
14
|
+
const userSchema = dsl({
|
|
15
|
+
username: 'string:3-32!',
|
|
16
|
+
email: 'email!',
|
|
17
|
+
age: 'number:18-120',
|
|
18
|
+
role: 'admin|user|guest'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const zhMarkdown = exporters.MarkdownExporter.export(userSchema, {
|
|
22
|
+
title: '用户注册 API',
|
|
23
|
+
locale: 'zh-CN',
|
|
24
|
+
includeExample: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
console.log(zhMarkdown);
|
|
28
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
29
|
+
|
|
30
|
+
// 示例 2: 带标签的 Schema
|
|
31
|
+
console.log('【示例 2】带标签的产品 Schema\n');
|
|
32
|
+
|
|
33
|
+
const productSchema = dsl({
|
|
34
|
+
'name': 'string:1-100!'.label('产品名称'),
|
|
35
|
+
'price': 'number:0.01-!'.label('价格'),
|
|
36
|
+
'description': 'string:500'.label('产品描述'),
|
|
37
|
+
'category': 'electronics|clothing|books'.label('类别'),
|
|
38
|
+
'tags': 'array:1-10<string:1-20>'.label('标签')
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const productMarkdown = exporters.MarkdownExporter.export(productSchema, {
|
|
42
|
+
title: '产品信息 Schema',
|
|
43
|
+
locale: 'zh-CN'
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(productMarkdown);
|
|
47
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
48
|
+
|
|
49
|
+
// 示例 3: 英文文档
|
|
50
|
+
console.log('【示例 3】英文文档\n');
|
|
51
|
+
|
|
52
|
+
const enMarkdown = exporters.MarkdownExporter.export(userSchema, {
|
|
53
|
+
title: 'User Registration API',
|
|
54
|
+
locale: 'en-US',
|
|
55
|
+
includeExample: true
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
console.log(enMarkdown);
|
|
59
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
60
|
+
|
|
61
|
+
// 示例 4: 日文文档
|
|
62
|
+
console.log('【示例 4】日文文档\n');
|
|
63
|
+
|
|
64
|
+
const jaMarkdown = exporters.MarkdownExporter.export(userSchema, {
|
|
65
|
+
title: 'ユーザー登録 API',
|
|
66
|
+
locale: 'ja-JP',
|
|
67
|
+
includeExample: true
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(jaMarkdown);
|
|
71
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中间件使用示例
|
|
3
|
+
*
|
|
4
|
+
* 演示如何在 Express/Koa 中使用中间件动态配置 Validator
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const { dsl, Validator, Locale } = require('../index');
|
|
9
|
+
|
|
10
|
+
// 确保加载所有语言包
|
|
11
|
+
require('../lib/locales/index');
|
|
12
|
+
|
|
13
|
+
// 扩展语言包以支持 Label 翻译 (可选)
|
|
14
|
+
Locale.addLocale('zh-CN', {
|
|
15
|
+
'label.username': '用户名',
|
|
16
|
+
'label.email': '邮箱',
|
|
17
|
+
'label.age': '年龄'
|
|
18
|
+
});
|
|
19
|
+
Locale.addLocale('en-US', {
|
|
20
|
+
'label.username': 'Username',
|
|
21
|
+
'label.email': 'Email',
|
|
22
|
+
'label.age': 'Age'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const app = express();
|
|
26
|
+
const validator = new Validator();
|
|
27
|
+
|
|
28
|
+
// ========== 中间件定义 ==========
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SchemaIO 验证中间件
|
|
32
|
+
*
|
|
33
|
+
* 1. 从请求头获取语言
|
|
34
|
+
* 2. 创建绑定了语言的 validate 方法
|
|
35
|
+
* 3. 挂载到 req 对象上
|
|
36
|
+
*/
|
|
37
|
+
const schemaIoMiddleware = (req, res, next) => {
|
|
38
|
+
// 获取语言 (支持 accept-language 头或 query 参数)
|
|
39
|
+
const lang = req.query.lang || req.headers['accept-language'] || 'en-US';
|
|
40
|
+
|
|
41
|
+
// 简单的语言匹配逻辑 (例如取前两个字符或完整匹配)
|
|
42
|
+
// 这里假设完整匹配,实际项目中可能需要更复杂的解析
|
|
43
|
+
const locale = lang.includes('zh') ? 'zh-CN' :
|
|
44
|
+
lang.includes('ja') ? 'ja-JP' :
|
|
45
|
+
lang.includes('es') ? 'es-ES' :
|
|
46
|
+
lang.includes('fr') ? 'fr-FR' : 'en-US';
|
|
47
|
+
|
|
48
|
+
// 挂载 validate 方法
|
|
49
|
+
req.validate = (schema, data) => {
|
|
50
|
+
return validator.validate(schema, data, { locale });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
next();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
app.use(schemaIoMiddleware);
|
|
57
|
+
app.use(express.json());
|
|
58
|
+
|
|
59
|
+
// ========== 路由定义 ==========
|
|
60
|
+
|
|
61
|
+
const userSchema = dsl({
|
|
62
|
+
username: 'string:3-32!'.username(), // 自动查找 label.username
|
|
63
|
+
email: 'email!', // 自动查找 label.email
|
|
64
|
+
age: 'integer:0-150' // 自动查找 label.age
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
app.post('/users', (req, res) => {
|
|
68
|
+
const result = req.validate(userSchema, req.body);
|
|
69
|
+
|
|
70
|
+
if (!result.valid) {
|
|
71
|
+
return res.status(400).json({
|
|
72
|
+
error: 'Validation Failed',
|
|
73
|
+
details: result.errors.map(e => e.message)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
res.json({ message: 'User created', data: result.data });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ========== 启动服务器 (仅用于演示) ==========
|
|
81
|
+
|
|
82
|
+
if (require.main === module) {
|
|
83
|
+
const port = 3000;
|
|
84
|
+
app.listen(port, () => {
|
|
85
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
86
|
+
console.log('Try sending POST /users with different Accept-Language headers');
|
|
87
|
+
console.log('Example: curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -H "Accept-Language: zh-CN" -d "{}"');
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = app;
|
|
92
|
+
|
|
93
|
+
|