From b980dcb56331c1f24d326e7a852012a68096a819 Mon Sep 17 00:00:00 2001 From: NAGUMO Toshiaki Date: Sun, 25 May 2025 09:03:07 +0900 Subject: [PATCH] refactor(valibot): extract string schema validation logic and enhance Record key validation - Refactored string schema (format, pattern, minLength, maxLength) validation logic into dedicated functions - Improved Record type to generate key validators based on pattern - Added support for format validations (email, uuid, url, date-time, etc.) - Improved code readability and maintainability --- src/model/model-to-valibot.ts | 91 +++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/src/model/model-to-valibot.ts b/src/model/model-to-valibot.ts index 1add416..a256edb 100644 --- a/src/model/model-to-valibot.ts +++ b/src/model/model-to-valibot.ts @@ -91,8 +91,8 @@ export namespace ModelToValibot { return Type(`v.intersect`, `[${inner.join(', ')}]`, []) } function Literal(schema: Types.TLiteral) { - return typeof schema.const === `string` - ? Type(`v.literal`, `'${schema.const}'`, []) + return typeof schema.const === `string` + ? Type(`v.literal`, `'${schema.const}'`, []) : Type(`v.literal`, `${schema.const}`, []) } function Never(schema: Types.TNever) { @@ -101,11 +101,82 @@ export namespace ModelToValibot { function Null(schema: Types.TNull) { return Type(`v.null`, null, []) } - function String(schema: Types.TString) { + + function processStringSchema(options: { + format?: string + pattern?: string + minLength?: number + maxLength?: number + }): string { + const { format, pattern, minLength, maxLength } = options + const constraints: string[] = [] + + const lengthConstraints = getLengthConstraints(minLength, maxLength) + constraints.push(...lengthConstraints) + + const patternConstraints = getPatternConstraints(pattern) + constraints.push(...patternConstraints) + + const formatValidator = getFormatValidator(format) + const constraintsStr = constraints.length > 0 ? constraints.join(', ') : '' + + return createStringValidatorWithFormat(formatValidator, constraintsStr) + } + + function createStringValidatorWithFormat(formatValidator: string | null, constraintsStr: string): string { + if (formatValidator) { + if (constraintsStr) { + return `v.pipe(v.string(), ${formatValidator}, ${constraintsStr})` + } + return `v.pipe(v.string(), ${formatValidator})` + } + if (constraintsStr) { + return `v.pipe(v.string(), ${constraintsStr})` + } + return 'v.string()' + } + + function getLengthConstraints(minLength?: number, maxLength?: number): string[] { + const constraints: string[] = [] + if (IsDefined(maxLength)) constraints.push(`v.maxLength(${maxLength})`) + if (IsDefined(minLength)) constraints.push(`v.minLength(${minLength})`) + return constraints + } + + function getPatternConstraints(pattern?: string): string[] { const constraints: string[] = [] - if (IsDefined(schema.maxLength)) constraints.push(`v.maxLength(${schema.maxLength})`) - if (IsDefined(schema.minLength)) constraints.push(`v.minLength(${schema.minLength})`) - return Type(`v.string`, null, constraints) + if (pattern) { + const escapedPattern = pattern.replace(/\//g, '\\/') + constraints.push(`v.regex(/${escapedPattern}/)`) + } + return constraints + } + + function getFormatValidator(format?: string): string | null { + if (!format) return null + + const formatMap: Record = { + 'date-time': 'v.isoTimestamp()', + email: 'v.email()', + uri: 'v.url()', + url: 'v.url()', + uuid: 'v.uuid()', + date: 'v.isoDate()', + time: 'v.isoTime()', + ipv4: 'v.ipv4()', + ipv6: 'v.ipv6()', + } + + return formatMap[format] || null + } + + function String(schema: Types.TString) { + return processStringSchema({ + format: schema.format, + pattern: schema.pattern, + minLength: schema.minLength, + maxLength: schema.maxLength, + }) } function Number(schema: Types.TNumber) { const constraints: string[] = [] @@ -134,13 +205,13 @@ export namespace ModelToValibot { function Record(schema: Types.TRecord) { for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { const type = Visit(value) - if (key === `^(0|[1-9][0-9]*)$`) { + if (key === '^(0|[1-9][0-9]*)$') { return Type('v.record', `v.number(), ${type}`, []) - } else { - return Type(`v.record`, `v.string(), ${type}`, []) } + const keyValidator = processStringSchema({ pattern: key }) + return Type('v.record', `${keyValidator}, ${type}`, []) } - throw Error(`Unreachable`) + throw Error('Unreachable') } function Ref(schema: Types.TRef) { if (!reference_map.has(schema.$ref!)) return UnsupportedType(schema)