@hy_ong/zod-kit 0.0.4 → 0.0.6

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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,471 @@
1
+ /**
2
+ * @fileoverview ID validator for Zod Kit
3
+ *
4
+ * Provides comprehensive ID validation with support for multiple ID formats,
5
+ * auto-detection, custom patterns, and flexible validation options.
6
+ *
7
+ * @author Ong Hoe Yuan
8
+ * @version 0.0.5
9
+ */
10
+
11
+ import { z, ZodNullable, ZodString } from "zod"
12
+ import { t } from "../../i18n"
13
+ import { getLocale, type Locale } from "../../config"
14
+
15
+ /**
16
+ * Type definition for ID validation error messages
17
+ *
18
+ * @interface IdMessages
19
+ * @property {string} [required] - Message when field is required but empty
20
+ * @property {string} [invalid] - Message when ID format is invalid
21
+ * @property {string} [minLength] - Message when ID is too short
22
+ * @property {string} [maxLength] - Message when ID is too long
23
+ * @property {string} [numeric] - Message when numeric ID format is invalid
24
+ * @property {string} [uuid] - Message when UUID format is invalid
25
+ * @property {string} [objectId] - Message when MongoDB ObjectId format is invalid
26
+ * @property {string} [nanoid] - Message when Nano ID format is invalid
27
+ * @property {string} [snowflake] - Message when Snowflake ID format is invalid
28
+ * @property {string} [cuid] - Message when CUID format is invalid
29
+ * @property {string} [ulid] - Message when ULID format is invalid
30
+ * @property {string} [shortid] - Message when ShortId format is invalid
31
+ * @property {string} [customFormat] - Message when custom regex format is invalid
32
+ * @property {string} [includes] - Message when ID doesn't contain required string
33
+ * @property {string} [excludes] - Message when ID contains forbidden string
34
+ * @property {string} [startsWith] - Message when ID doesn't start with required string
35
+ * @property {string} [endsWith] - Message when ID doesn't end with required string
36
+ */
37
+ export type IdMessages = {
38
+ required?: string
39
+ invalid?: string
40
+ minLength?: string
41
+ maxLength?: string
42
+ numeric?: string
43
+ uuid?: string
44
+ objectId?: string
45
+ nanoid?: string
46
+ snowflake?: string
47
+ cuid?: string
48
+ ulid?: string
49
+ shortid?: string
50
+ customFormat?: string
51
+ includes?: string
52
+ excludes?: string
53
+ startsWith?: string
54
+ endsWith?: string
55
+ }
56
+
57
+ /**
58
+ * Supported ID types for validation
59
+ *
60
+ * @typedef {string} IdType
61
+ *
62
+ * Available types:
63
+ * - numeric: Pure numeric IDs (1, 123, 999999)
64
+ * - uuid: UUID v4 format (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
65
+ * - objectId: MongoDB ObjectId (24-character hexadecimal)
66
+ * - nanoid: Nano ID format (21-character URL-safe)
67
+ * - snowflake: Twitter Snowflake (19-digit number)
68
+ * - cuid: CUID format (25-character starting with 'c')
69
+ * - ulid: ULID format (26-character case-insensitive)
70
+ * - shortid: ShortId format (7-14 character URL-safe)
71
+ * - auto: Auto-detect format from the value
72
+ */
73
+ export type IdType =
74
+ | "numeric" // Pure numeric IDs (1, 123, 999999)
75
+ | "uuid" // UUID v4 format
76
+ | "objectId" // MongoDB ObjectId (24-character hexadecimal)
77
+ | "nanoid" // Nano ID
78
+ | "snowflake" // Twitter Snowflake (19-digit number)
79
+ | "cuid" // CUID format
80
+ | "ulid" // ULID format
81
+ | "shortid" // ShortId format
82
+ | "auto" // Auto-detect format
83
+
84
+ /**
85
+ * Configuration options for ID validation
86
+ *
87
+ * @template IsRequired - Whether the field is required (affects return type)
88
+ *
89
+ * @interface IdOptions
90
+ * @property {IsRequired} [required=true] - Whether the field is required
91
+ * @property {IdType} [type="auto"] - Expected ID type or auto-detection
92
+ * @property {number} [minLength] - Minimum length of ID
93
+ * @property {number} [maxLength] - Maximum length of ID
94
+ * @property {IdType[]} [allowedTypes] - Multiple allowed ID types (overrides type)
95
+ * @property {RegExp} [customRegex] - Custom regex pattern (overrides type validation)
96
+ * @property {string} [includes] - String that must be included in ID
97
+ * @property {string | string[]} [excludes] - String(s) that must not be included
98
+ * @property {string} [startsWith] - String that ID must start with
99
+ * @property {string} [endsWith] - String that ID must end with
100
+ * @property {boolean} [caseSensitive=true] - Whether validation is case-sensitive
101
+ * @property {Function} [transform] - Custom transformation function for ID
102
+ * @property {string | null} [defaultValue] - Default value when input is empty
103
+ * @property {Record<Locale, IdMessages>} [i18n] - Custom error messages for different locales
104
+ */
105
+ export type IdOptions<IsRequired extends boolean = true> = {
106
+ required?: IsRequired
107
+ type?: IdType
108
+ minLength?: number
109
+ maxLength?: number
110
+ allowedTypes?: IdType[]
111
+ customRegex?: RegExp
112
+ includes?: string
113
+ excludes?: string | string[]
114
+ startsWith?: string
115
+ endsWith?: string
116
+ caseSensitive?: boolean
117
+ transform?: (value: string) => string
118
+ defaultValue?: IsRequired extends true ? string : string | null
119
+ i18n?: Record<Locale, IdMessages>
120
+ }
121
+
122
+ /**
123
+ * Type alias for ID validation schema based on required flag
124
+ *
125
+ * @template IsRequired - Whether the field is required
126
+ * @typedef IdSchema
127
+ * @description Returns ZodString if required, ZodNullable<ZodString> if optional
128
+ */
129
+ export type IdSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
130
+
131
+ /**
132
+ * Regular expression patterns for different ID formats
133
+ *
134
+ * @constant {Record<string, RegExp>} ID_PATTERNS
135
+ * @description Maps each ID type to its corresponding regex pattern
136
+ */
137
+ const ID_PATTERNS = {
138
+ numeric: /^\d+$/,
139
+ uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
140
+ objectId: /^[0-9a-f]{24}$/i,
141
+ nanoid: /^[A-Za-z0-9_-]{21}$/,
142
+ snowflake: /^\d{19}$/,
143
+ cuid: /^c[a-z0-9]{24}$/,
144
+ ulid: /^[0-9A-HJKMNP-TV-Z]{26}$/,
145
+ shortid: /^[A-Za-z0-9_-]{7,14}$/,
146
+ } as const
147
+
148
+ /**
149
+ * Detects the ID type of a given value using pattern matching
150
+ *
151
+ * @param {string} value - The ID value to analyze
152
+ * @returns {IdType | null} The detected ID type or null if no pattern matches
153
+ *
154
+ * @description
155
+ * Attempts to identify the ID type by testing against known patterns.
156
+ * Patterns are ordered by specificity to avoid false positives.
157
+ * More specific patterns (UUID, ObjectId) are tested before generic ones (numeric, shortid).
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * detectIdType("550e8400-e29b-41d4-a716-446655440000") // "uuid"
162
+ * detectIdType("507f1f77bcf86cd799439011") // "objectId"
163
+ * detectIdType("V1StGXR8_Z5jdHi6B-myT") // "nanoid"
164
+ * detectIdType("123456789") // "numeric"
165
+ * detectIdType("invalid-id") // null
166
+ * ```
167
+ */
168
+ const detectIdType = (value: string): IdType | null => {
169
+ // 按優先順序檢查(從最具體到最通用)
170
+ const orderedTypes: Array<[IdType, RegExp]> = [
171
+ ["uuid", ID_PATTERNS.uuid],
172
+ ["objectId", ID_PATTERNS.objectId],
173
+ ["snowflake", ID_PATTERNS.snowflake],
174
+ ["cuid", ID_PATTERNS.cuid],
175
+ ["ulid", ID_PATTERNS.ulid],
176
+ ["nanoid", ID_PATTERNS.nanoid],
177
+ ["numeric", ID_PATTERNS.numeric],
178
+ ["shortid", ID_PATTERNS.shortid], // 放最後,因為最通用
179
+ ]
180
+
181
+ for (const [type, pattern] of orderedTypes) {
182
+ if (pattern.test(value)) {
183
+ return type
184
+ }
185
+ }
186
+ return null
187
+ }
188
+
189
+ /**
190
+ * Validates if a value matches the specified ID type
191
+ *
192
+ * @param {string} value - The ID value to validate
193
+ * @param {IdType} type - The expected ID type
194
+ * @returns {boolean} True if the value matches the specified type
195
+ *
196
+ * @description
197
+ * Validates a specific ID type using regex patterns or auto-detection.
198
+ * For "auto" type, uses detectIdType to check if any known pattern matches.
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * validateIdType("123456", "numeric") // true
203
+ * validateIdType("abc123", "numeric") // false
204
+ * validateIdType("550e8400-e29b-41d4-a716-446655440000", "uuid") // true
205
+ * validateIdType("invalid-uuid", "uuid") // false
206
+ * ```
207
+ */
208
+ const validateIdType = (value: string, type: IdType): boolean => {
209
+ if (type === "auto") {
210
+ return detectIdType(value) !== null
211
+ }
212
+ const pattern = ID_PATTERNS[type as keyof typeof ID_PATTERNS]
213
+ return pattern ? pattern.test(value) : false
214
+ }
215
+
216
+ /**
217
+ * Creates a Zod schema for ID validation with comprehensive format support
218
+ *
219
+ * @template IsRequired - Whether the field is required (affects return type)
220
+ * @param {IdOptions<IsRequired>} [options] - Configuration options for ID validation
221
+ * @returns {IdSchema<IsRequired>} Zod schema for ID validation
222
+ *
223
+ * @description
224
+ * Creates a comprehensive ID validator with support for multiple ID formats,
225
+ * auto-detection, custom patterns, and flexible validation options.
226
+ *
227
+ * Features:
228
+ * - Multiple ID format support (UUID, ObjectId, Snowflake, etc.)
229
+ * - Auto-detection of ID types
230
+ * - Custom regex pattern support
231
+ * - Length validation
232
+ * - Content validation (includes, excludes, startsWith, endsWith)
233
+ * - Case sensitivity control
234
+ * - Multiple allowed types
235
+ * - Custom transformation functions
236
+ * - Comprehensive internationalization
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * // Auto-detect ID format
241
+ * const autoSchema = id()
242
+ * autoSchema.parse("550e8400-e29b-41d4-a716-446655440000") // ✓ Valid (UUID)
243
+ * autoSchema.parse("507f1f77bcf86cd799439011") // ✓ Valid (ObjectId)
244
+ * autoSchema.parse("123456") // ✓ Valid (numeric)
245
+ *
246
+ * // Specific ID type
247
+ * const uuidSchema = id({ type: "uuid" })
248
+ * uuidSchema.parse("550e8400-e29b-41d4-a716-446655440000") // ✓ Valid
249
+ * uuidSchema.parse("invalid-uuid") // ✗ Invalid
250
+ *
251
+ * // Multiple allowed types
252
+ * const multiSchema = id({ allowedTypes: ["uuid", "objectId"] })
253
+ * multiSchema.parse("550e8400-e29b-41d4-a716-446655440000") // ✓ Valid (UUID)
254
+ * multiSchema.parse("507f1f77bcf86cd799439011") // ✓ Valid (ObjectId)
255
+ * multiSchema.parse("123456") // ✗ Invalid (numeric not allowed)
256
+ *
257
+ * // Custom regex pattern
258
+ * const customSchema = id({ customRegex: /^CUST_\d{6}$/ })
259
+ * customSchema.parse("CUST_123456") // ✓ Valid
260
+ * customSchema.parse("invalid") // ✗ Invalid
261
+ *
262
+ * // Content validation
263
+ * const prefixSchema = id({
264
+ * type: "auto",
265
+ * startsWith: "user_",
266
+ * minLength: 10
267
+ * })
268
+ * prefixSchema.parse("user_123456") // ✓ Valid
269
+ *
270
+ * // Case insensitive
271
+ * const caseInsensitiveSchema = id({
272
+ * type: "uuid",
273
+ * caseSensitive: false
274
+ * })
275
+ * caseInsensitiveSchema.parse("550E8400-E29B-41D4-A716-446655440000") // ✓ Valid
276
+ *
277
+ * // Optional with default
278
+ * const optionalSchema = id({
279
+ * required: false,
280
+ * defaultValue: null
281
+ * })
282
+ * ```
283
+ *
284
+ * @throws {z.ZodError} When validation fails with specific error messages
285
+ * @see {@link IdOptions} for all available configuration options
286
+ * @see {@link IdType} for supported ID types
287
+ * @see {@link detectIdType} for auto-detection logic
288
+ * @see {@link validateIdType} for type-specific validation
289
+ */
290
+ export function id<IsRequired extends boolean = true>(options?: IdOptions<IsRequired>): IdSchema<IsRequired> {
291
+ const {
292
+ required = true,
293
+ type = "auto",
294
+ minLength,
295
+ maxLength,
296
+ allowedTypes,
297
+ customRegex,
298
+ includes,
299
+ excludes,
300
+ startsWith,
301
+ endsWith,
302
+ caseSensitive = true,
303
+ transform,
304
+ defaultValue,
305
+ i18n,
306
+ } = options ?? {}
307
+
308
+ // Set appropriate default value based on required flag
309
+ const actualDefaultValue = defaultValue ?? (required ? "" : null)
310
+
311
+ // Helper function to get custom message or fallback to default i18n
312
+ const getMessage = (key: keyof IdMessages, params?: Record<string, any>) => {
313
+ if (i18n) {
314
+ const currentLocale = getLocale()
315
+ const customMessages = i18n[currentLocale]
316
+ if (customMessages && customMessages[key]) {
317
+ const template = customMessages[key]!
318
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
319
+ }
320
+ }
321
+ return t(`common.id.${key}`, params)
322
+ }
323
+
324
+ // Preprocessing function
325
+ const preprocessFn = (val: unknown) => {
326
+ if (val === "" || val === null || val === undefined) {
327
+ return actualDefaultValue
328
+ }
329
+
330
+ let processed = String(val)
331
+
332
+ if (transform) {
333
+ processed = transform(processed)
334
+ }
335
+
336
+ return processed
337
+ }
338
+
339
+ const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
340
+
341
+ const schema = baseSchema
342
+ .refine((val) => {
343
+ if (val === null) return true
344
+
345
+ // Required check
346
+ if (required && (val === "" || val === "null" || val === "undefined")) {
347
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
348
+ }
349
+
350
+ // Create comparison value for case-insensitive checks
351
+ const comparisonVal = !caseSensitive ? val.toLowerCase() : val
352
+
353
+ // Length checks
354
+ if (val !== null && minLength !== undefined && val.length < minLength) {
355
+ throw new z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }])
356
+ }
357
+ if (val !== null && maxLength !== undefined && val.length > maxLength) {
358
+ throw new z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }])
359
+ }
360
+
361
+ // Check if we have content-based validations that override format checking
362
+ const hasContentValidations = customRegex !== undefined || startsWith !== undefined || endsWith !== undefined || includes !== undefined || excludes !== undefined
363
+
364
+ // Custom regex validation (overrides ID format validation)
365
+ if (val !== null && customRegex !== undefined) {
366
+ if (!customRegex.test(val)) {
367
+ throw new z.ZodError([{ code: "custom", message: getMessage("customFormat"), path: [] }])
368
+ }
369
+ } else if (val !== null && !hasContentValidations) {
370
+ // ID type validation (only if no custom regex or content validations)
371
+ let isValidId: boolean
372
+
373
+ if (allowedTypes && allowedTypes.length > 0) {
374
+ // Check if ID matches any of the allowed types
375
+ isValidId = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
376
+ if (!isValidId) {
377
+ const typeNames = allowedTypes.join(", ")
378
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
379
+ }
380
+ } else if (type !== "auto") {
381
+ // Validate specific type
382
+ isValidId = validateIdType(val, type)
383
+ if (!isValidId) {
384
+ throw new z.ZodError([{ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid"), path: [] }])
385
+ }
386
+ } else {
387
+ // Auto-detection - must match at least one known pattern
388
+ isValidId = detectIdType(val) !== null
389
+ if (!isValidId) {
390
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
391
+ }
392
+ }
393
+ } else if (val !== null && hasContentValidations && type !== "auto" && !customRegex) {
394
+ // Still validate specific types even with content validations (but not auto)
395
+ if (allowedTypes && allowedTypes.length > 0) {
396
+ const isValidType = allowedTypes.some((allowedType) => validateIdType(val, allowedType))
397
+ if (!isValidType) {
398
+ const typeNames = allowedTypes.join(", ")
399
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid") + ` (allowed types: ${typeNames})`, path: [] }])
400
+ }
401
+ } else {
402
+ if (!validateIdType(val, type)) {
403
+ throw new z.ZodError([{ code: "custom", message: getMessage(type as keyof IdMessages) || getMessage("invalid"), path: [] }])
404
+ }
405
+ }
406
+ }
407
+
408
+ // String content checks (using comparisonVal for case sensitivity)
409
+ const searchStartsWith = !caseSensitive && startsWith ? startsWith.toLowerCase() : startsWith
410
+ const searchEndsWith = !caseSensitive && endsWith ? endsWith.toLowerCase() : endsWith
411
+ const searchIncludes = !caseSensitive && includes ? includes.toLowerCase() : includes
412
+
413
+ if (val !== null && startsWith !== undefined && !comparisonVal.startsWith(searchStartsWith!)) {
414
+ throw new z.ZodError([{ code: "custom", message: getMessage("startsWith", { startsWith }), path: [] }])
415
+ }
416
+ if (val !== null && endsWith !== undefined && !comparisonVal.endsWith(searchEndsWith!)) {
417
+ throw new z.ZodError([{ code: "custom", message: getMessage("endsWith", { endsWith }), path: [] }])
418
+ }
419
+ if (val !== null && includes !== undefined && !comparisonVal.includes(searchIncludes!)) {
420
+ throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
421
+ }
422
+ if (val !== null && excludes !== undefined) {
423
+ const excludeList = Array.isArray(excludes) ? excludes : [excludes]
424
+ for (const exclude of excludeList) {
425
+ const searchExclude = !caseSensitive ? exclude.toLowerCase() : exclude
426
+ if (comparisonVal.includes(searchExclude)) {
427
+ throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
428
+ }
429
+ }
430
+ }
431
+
432
+ return true
433
+ })
434
+ .transform((val) => {
435
+ if (val === null) return val
436
+
437
+ // Handle case transformations
438
+ const shouldPreserveCase = type === "uuid" || type === "objectId"
439
+
440
+ if (!caseSensitive && !shouldPreserveCase) {
441
+ return val.toLowerCase()
442
+ }
443
+
444
+ return val // preserve the original case for UUID/ObjectId or when case-sensitive
445
+ })
446
+
447
+ return schema as unknown as IdSchema<IsRequired>
448
+ }
449
+
450
+ /**
451
+ * Utility functions and constants exported for external use
452
+ *
453
+ * @description
454
+ * These utilities can be used independently for ID validation, type detection,
455
+ * and pattern matching without creating a full Zod schema.
456
+ *
457
+ * @example
458
+ * ```typescript
459
+ * import { detectIdType, validateIdType, ID_PATTERNS } from './id'
460
+ *
461
+ * // Detect ID type
462
+ * const type = detectIdType("550e8400-e29b-41d4-a716-446655440000") // "uuid"
463
+ *
464
+ * // Validate specific type
465
+ * const isValid = validateIdType("123456", "numeric") // true
466
+ *
467
+ * // Access regex patterns
468
+ * const uuidPattern = ID_PATTERNS.uuid
469
+ * ```
470
+ */
471
+ export { detectIdType, validateIdType, ID_PATTERNS }