diff --git a/package-lock.json b/package-lock.json index 3bc68c5..eb8811c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@sinclair/hammer": "^0.17.2", + "@types/inquirer": "^9.0.7", "@types/node": "^18.15.11", "@types/prettier": "^2.7.2" } @@ -68,6 +69,16 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.6.tgz", "integrity": "sha512-mgcPAfLZEL2B/hrF+vPDdwwZ1MR0UuALvz+tI2zx7IYHfREmua3C82XsYgkwxCCJKpO3ibTje4QrHYrOAHWhxA==" }, + "node_modules/@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "dependencies": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "node_modules/@types/node": { "version": "18.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", @@ -80,6 +91,15 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", @@ -451,6 +471,21 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -493,6 +528,16 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.6.tgz", "integrity": "sha512-mgcPAfLZEL2B/hrF+vPDdwwZ1MR0UuALvz+tI2zx7IYHfREmua3C82XsYgkwxCCJKpO3ibTje4QrHYrOAHWhxA==" }, + "@types/inquirer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz", + "integrity": "sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==", + "dev": true, + "requires": { + "@types/through": "*", + "rxjs": "^7.2.0" + } + }, "@types/node": { "version": "18.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz", @@ -505,6 +550,15 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, + "@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "esbuild": { "version": "0.15.18", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", @@ -682,6 +736,21 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==" }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", diff --git a/package.json b/package.json index 0a0b595..6d06fec 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "homepage": "https://github.com/sinclairzx81/typebox-codegen#readme", "devDependencies": { "@sinclair/hammer": "^0.17.2", + "@types/inquirer": "^9.0.7", "@types/node": "^18.15.11", "@types/prettier": "^2.7.2" }, diff --git a/src/model/model-to-json-schema.ts b/src/model/model-to-json-schema.ts index c65fd72..ee6781a 100644 --- a/src/model/model-to-json-schema.ts +++ b/src/model/model-to-json-schema.ts @@ -32,6 +32,10 @@ import * as Types from '@sinclair/typebox' // ModelToJsonSchema // -------------------------------------------------------------------------- export namespace ModelToJsonSchema { + interface JsonSchemaOptions { + format?: 'js' | 'json' + } + function Any(schema: Types.TAny): Types.TSchema { return schema } @@ -155,13 +159,43 @@ export namespace ModelToJsonSchema { if (Types.TypeGuard.IsVoid(schema)) return Void(schema) return UnsupportedType(schema) } - export function Generate(model: TypeBoxModel): string { - const buffer: string[] = [] + + interface Formatter { + push(type: Types.TSchema, schema: Types.TSchema): void + finalize(): string + } + class JsFormatter implements Formatter { + buffer: string[] = [] + + push(type: Types.TSchema, schema: Types.TSchema) { + const encode = JSON.stringify(schema, null, 2) + this.buffer.push(`export const ${type.$id} = ${encode}`) + } + finalize() { + return Formatter.Format(this.buffer.join('\n')) + } + } + + class JsonFormatter implements Formatter { + objs: Record = {} + + push(type: Types.TSchema, schema: Types.TSchema) { + if (type.$id !== undefined) this.objs[type.$id] = schema + } + finalize(): string { + return JSON.stringify(this.objs, null, 2) + } + } + + export function Generate(model: TypeBoxModel, options?: JsonSchemaOptions): string { + const format = options?.format ?? 'js' + const formatter = format === 'js' ? new JsFormatter() : new JsonFormatter() + for (const type of model.types.filter((type) => Types.TypeGuard.IsSchema(type))) { const schema = Visit(type) - const encode = JSON.stringify(schema, null, 2) - buffer.push(`export const ${type.$id} = ${encode}`) + formatter.push(type, schema) } - return Formatter.Format(buffer.join('\n')) + + return formatter.finalize() } } diff --git a/src/model/model-to-valibot.ts b/src/model/model-to-valibot.ts index 05c3a7e..ead02f0 100644 --- a/src/model/model-to-valibot.ts +++ b/src/model/model-to-valibot.ts @@ -33,202 +33,261 @@ import * as Types from '@sinclair/typebox' // ModelToValibot // -------------------------------------------------------------------------- export namespace ModelToValibot { - function IsDefined(value: unknown): value is T { - return value !== undefined + /* + * These Dialect classes may not be accurate. + */ + type ValibotVersions = 'initial' | '0.29' | '0.30' | '0.31' | 'latest' + + interface ValibotDialect { + InferOutput: string + typeWithConstraints: (type: string, parameter: string | null, constraints: string[]) => string } - function Type(type: string, parameter: string | null, constraints: string[]) { - if (constraints.length > 0) { + const ValibotDialect_Initial: ValibotDialect = { + InferOutput: 'Output', + typeWithConstraints: (type: string, parameter: string | null, constraints: string[]) => { if (typeof parameter === 'string') { return `${type}(${parameter}, [${constraints.join(', ')}])` } else { return `${type}([${constraints.join(', ')}])` } - } else { - if (typeof parameter === 'string') { - return `${type}(${parameter})` - } else { - return `${type}()` - } - } - } - function Any(schema: Types.TAny) { - return Type(`v.any`, null, []) - } - function Array(schema: Types.TArray) { - const items = Visit(schema.items) - const constraints: string[] = [] - if (IsDefined(schema.minItems)) constraints.push(`v.minLength(${schema.minItems})`) - if (IsDefined(schema.maxItems)) constraints.push(`v.maxLength(${schema.maxItems})`) - return Type(`v.array`, items, constraints) - } - function BigInt(schema: Types.TBigInt) { - return Type(`v.bigint`, null, []) - } - function Boolean(schema: Types.TBoolean) { - return Type(`v.boolean`, null, []) + }, } - function Date(schema: Types.TDate) { - return Type(`v.date`, null, []) + const ValibotDialect_0_29: ValibotDialect = { + ...ValibotDialect_Initial, } - function Constructor(schema: Types.TConstructor): string { - return UnsupportedType(schema) + const ValibotDialect_0_30: ValibotDialect = { + ...ValibotDialect_0_29, } - function Function(schema: Types.TFunction) { - return UnsupportedType(schema) + const ValibotDialect_0_31: ValibotDialect = { + ...ValibotDialect_0_30, + InferOutput: 'InferOutput', + typeWithConstraints: (type: string, parameter: string | null, constraints: string[]) => `v.pipe(${type}(${parameter ?? ''}), ${constraints.join(', ')})`, } - function Integer(schema: Types.TInteger) { - return Type(`v.number`, null, [`v.integer()`]) + const VALIBOT_DIALECTS: Record = { + initial: ValibotDialect_Initial, + '0.29': ValibotDialect_0_29, + '0.30': ValibotDialect_0_30, + '0.31': ValibotDialect_0_31, + latest: ValibotDialect_0_31, } - function Intersect(schema: Types.TIntersect) { - const inner = schema.allOf.map((inner) => Visit(inner)) - return Type(`v.intersect`, `[${inner.join(', ')}]`, []) - } - function Literal(schema: Types.TLiteral) { - // prettier-ignore - return typeof schema.const === `string` - ? Type(`v.literal`, `'${schema.const}'`, []) - : Type(`v.literal`, `${schema.const}`, []) - } - function Never(schema: Types.TNever) { - return Type(`v.never`, null, []) - } - function Null(schema: Types.TNull) { - return Type(`v.null_`, null, []) - } - function String(schema: Types.TString) { - 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) - } - function Number(schema: Types.TNumber) { - const constraints: string[] = [] - if (IsDefined(schema.minimum)) constraints.push(`v.minValue(${schema.minimum})`) - if (IsDefined(schema.maximum)) constraints.push(`v.maxValue(${schema.maximum})`) - if (IsDefined(schema.exclusiveMinimum)) constraints.push(`v.minValue(${schema.exclusiveMinimum + 1})`) - if (IsDefined(schema.exclusiveMaximum)) constraints.push(`v.maxValue(${schema.exclusiveMaximum - 1})`) - return Type('v.number', null, constraints) - } - function Object(schema: Types.TObject) { - // prettier-ignore - const properties = globalThis.Object.entries(schema.properties).map(([key, value]) => { - const optional = Types.TypeGuard.IsOptional(value) - const property = PropertyEncoder.Encode(key) - return optional ? `${property}: v.optional(${Visit(value)})` : `${property}: ${Visit(value)}` - }).join(`,`) - const constraints: string[] = [] - return Type(`v.object`, `{\n${properties}\n}`, constraints) - } - function Promise(schema: Types.TPromise) { - return UnsupportedType(schema) + + interface ValibotOptions { + version?: ValibotVersions } - 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]*)$`) { - return UnsupportedType(schema) + + class ValibotVisitor { + dialect: ValibotDialect + reference_map: Map + recursive_set: Set + emitted_set: Set + + constructor(version: ValibotVersions) { + this.dialect = VALIBOT_DIALECTS[version] + this.reference_map = new Map() + this.recursive_set = new Set() + this.emitted_set = new Set() + } + + IsDefined(value: unknown): value is T { + return value !== undefined + } + Type(type: string, parameter: string | null, constraints: string[]) { + if (constraints.length > 0) { + return this.dialect.typeWithConstraints(type, parameter, constraints) } else { - return Type(`v.record`, type, []) + if (typeof parameter === 'string') { + return `${type}(${parameter})` + } else { + return `${type}()` + } } } - throw Error(`Unreachable`) - } - function Ref(schema: Types.TRef) { - if (!reference_map.has(schema.$ref!)) return UnsupportedType(schema) - return schema.$ref - } - function This(schema: Types.TThis) { - return UnsupportedType(schema) - } - function Tuple(schema: Types.TTuple) { - if (schema.items === undefined) return `[]` - const items = schema.items.map((schema) => Visit(schema)).join(`, `) - return Type(`v.tuple`, `[${items}]`, []) - } - function TemplateLiteral(schema: Types.TTemplateLiteral) { - const constaint = Type(`v.regex`, `/${schema.pattern}/`, []) - return Type(`v.string`, null, [constaint]) - } - function UInt8Array(schema: Types.TUint8Array): string { - return UnsupportedType(schema) - } - function Undefined(schema: Types.TUndefined) { - return Type(`v.undefinedType`, null, []) - } - function Union(schema: Types.TUnion) { - const inner = schema.anyOf.map((schema) => Visit(schema)).join(`, `) - return Type(`v.union`, `[${inner}]`, []) - } - function Unknown(schema: Types.TUnknown) { - return Type(`v.unknown`, null, []) - } - function Void(schema: Types.TVoid) { - return Type(`v.voidType`, null, []) - } - function UnsupportedType(schema: Types.TSchema) { - return `v.any(/* unsupported */)` - } - function Visit(schema: Types.TSchema): string { - if (schema.$id !== undefined) reference_map.set(schema.$id, schema) - if (schema.$id !== undefined && emitted_set.has(schema.$id!)) return schema.$id! - if (Types.TypeGuard.IsAny(schema)) return Any(schema) - if (Types.TypeGuard.IsArray(schema)) return Array(schema) - if (Types.TypeGuard.IsBigInt(schema)) return BigInt(schema) - if (Types.TypeGuard.IsBoolean(schema)) return Boolean(schema) - if (Types.TypeGuard.IsDate(schema)) return Date(schema) - if (Types.TypeGuard.IsConstructor(schema)) return Constructor(schema) - if (Types.TypeGuard.IsFunction(schema)) return Function(schema) - if (Types.TypeGuard.IsInteger(schema)) return Integer(schema) - if (Types.TypeGuard.IsIntersect(schema)) return Intersect(schema) - if (Types.TypeGuard.IsLiteral(schema)) return Literal(schema) - if (Types.TypeGuard.IsNever(schema)) return Never(schema) - if (Types.TypeGuard.IsNull(schema)) return Null(schema) - if (Types.TypeGuard.IsNumber(schema)) return Number(schema) - if (Types.TypeGuard.IsObject(schema)) return Object(schema) - if (Types.TypeGuard.IsPromise(schema)) return Promise(schema) - if (Types.TypeGuard.IsRecord(schema)) return Record(schema) - if (Types.TypeGuard.IsRef(schema)) return Ref(schema) - if (Types.TypeGuard.IsString(schema)) return String(schema) - if (Types.TypeGuard.IsTemplateLiteral(schema)) return TemplateLiteral(schema) - if (Types.TypeGuard.IsThis(schema)) return This(schema) - if (Types.TypeGuard.IsTuple(schema)) return Tuple(schema) - if (Types.TypeGuard.IsUint8Array(schema)) return UInt8Array(schema) - if (Types.TypeGuard.IsUndefined(schema)) return Undefined(schema) - if (Types.TypeGuard.IsUnion(schema)) return Union(schema) - if (Types.TypeGuard.IsUnknown(schema)) return Unknown(schema) - if (Types.TypeGuard.IsVoid(schema)) return Void(schema) - return UnsupportedType(schema) - } - function Collect(schema: Types.TSchema) { - return [...Visit(schema)].join(``) - } - function GenerateType(model: TypeBoxModel, schema: Types.TSchema, references: Types.TSchema[]) { - const output: string[] = [] - for (const reference of references) { - if (reference.$id === undefined) return UnsupportedType(schema) - reference_map.set(reference.$id, reference) - } - const type = Collect(schema) - if (recursive_set.has(schema.$id!)) { - output.push(`export ${ModelToTypeScript.GenerateType(model, schema.$id!)}`) - output.push(`export const ${schema.$id || `T`}: v.Output<${schema.$id}> = v.lazy(() => ${Formatter.Format(type)})`) - } else { - output.push(`export type ${schema.$id} = v.Output`) - output.push(`export const ${schema.$id || `T`} = ${Formatter.Format(type)}`) - } - if (schema.$id) emitted_set.add(schema.$id) - return output.join('\n') + Any(schema: Types.TAny) { + return this.Type(`v.any`, null, []) + } + Array(schema: Types.TArray) { + const items = this.Visit(schema.items) + const constraints: string[] = [] + if (this.IsDefined(schema.minItems)) constraints.push(`v.minLength(${schema.minItems})`) + if (this.IsDefined(schema.maxItems)) constraints.push(`v.maxLength(${schema.maxItems})`) + return this.Type(`v.array`, items, constraints) + } + BigInt(schema: Types.TBigInt) { + return this.Type(`v.bigint`, null, []) + } + Boolean(schema: Types.TBoolean) { + return this.Type(`v.boolean`, null, []) + } + Date(schema: Types.TDate) { + return this.Type(`v.date`, null, []) + } + Constructor(schema: Types.TConstructor): string { + return this.UnsupportedType(schema) + } + Function(schema: Types.TFunction) { + return this.UnsupportedType(schema) + } + Integer(schema: Types.TInteger) { + return this.Type(`v.number`, null, [`v.integer()`]) + } + Intersect(schema: Types.TIntersect) { + const inner = schema.allOf.map((inner) => this.Visit(inner)) + return this.Type(`v.intersect`, `[${inner.join(', ')}]`, []) + } + Literal(schema: Types.TLiteral) { + // prettier-ignore + return typeof schema.const === `string` + ? this.Type(`v.literal`, `'${schema.const}'`, []) + : this.Type(`v.literal`, `${schema.const}`, []) + } + Never(schema: Types.TNever) { + return this.Type(`v.never`, null, []) + } + Null(schema: Types.TNull) { + return this.Type(`v.null_`, null, []) + } + String(schema: Types.TString) { + const constraints: string[] = [] + if (this.IsDefined(schema.maxLength)) constraints.push(`v.maxLength(${schema.maxLength})`) + if (this.IsDefined(schema.minLength)) constraints.push(`v.minLength(${schema.minLength})`) + if (this.IsDefined(schema.format)) { + switch (schema.format) { + case 'date': + constraints.push(`v.isoDate()`) + break + case 'date-time': + constraints.push(`v.isoDateTime()`) + break + default: + throw new Error(`Unsupported format: ${schema.format}`) + } + } + return this.Type(`v.string`, null, constraints) + } + Number(schema: Types.TNumber) { + const constraints: string[] = [] + if (this.IsDefined(schema.minimum)) constraints.push(`v.minValue(${schema.minimum})`) + if (this.IsDefined(schema.maximum)) constraints.push(`v.maxValue(${schema.maximum})`) + if (this.IsDefined(schema.exclusiveMinimum)) constraints.push(`v.minValue(${schema.exclusiveMinimum + 1})`) + if (this.IsDefined(schema.exclusiveMaximum)) constraints.push(`v.maxValue(${schema.exclusiveMaximum - 1})`) + return this.Type('v.number', null, constraints) + } + Object(schema: Types.TObject) { + // prettier-ignore + const properties = globalThis.Object.entries(schema.properties).map(([key, value]) => { + const optional = Types.TypeGuard.IsOptional(value) + const property = PropertyEncoder.Encode(key) + return optional ? `${property}: v.optional(${this.Visit(value)})` : `${property}: ${this.Visit(value)}` + }).join(`,`) + const constraints: string[] = [] + return this.Type(`v.object`, `{\n${properties}\n}`, constraints) + } + Promise(schema: Types.TPromise) { + return this.UnsupportedType(schema) + } + Record(schema: Types.TRecord) { + for (const [key, value] of globalThis.Object.entries(schema.patternProperties)) { + const type = this.Visit(value) + if (key === `^(0|[1-9][0-9]*)$`) { + return this.UnsupportedType(schema) + } else { + return this.Type(`v.record`, type, []) + } + } + throw Error(`Unreachable`) + } + Ref(schema: Types.TRef) { + if (!this.reference_map.has(schema.$ref!)) return this.UnsupportedType(schema) + return schema.$ref + } + This(schema: Types.TThis) { + return this.UnsupportedType(schema) + } + Tuple(schema: Types.TTuple) { + if (schema.items === undefined) return `[]` + const items = schema.items.map((schema) => this.Visit(schema)).join(`, `) + return this.Type(`v.tuple`, `[${items}]`, []) + } + TemplateLiteral(schema: Types.TTemplateLiteral) { + const constraint = this.Type(`v.regex`, `/${schema.pattern}/`, []) + return this.Type(`v.string`, null, [constraint]) + } + UInt8Array(schema: Types.TUint8Array): string { + return this.UnsupportedType(schema) + } + Undefined(schema: Types.TUndefined) { + return this.Type(`v.undefinedType`, null, []) + } + Union(schema: Types.TUnion) { + const inner = schema.anyOf.map((schema) => this.Visit(schema)).join(`, `) + return this.Type(`v.union`, `[${inner}]`, []) + } + Unknown(schema: Types.TUnknown) { + return this.Type(`v.unknown`, null, []) + } + Void(schema: Types.TVoid) { + return this.Type(`v.voidType`, null, []) + } + UnsupportedType(schema: Types.TSchema) { + return `v.any(/* unsupported */)` + } + Visit(schema: Types.TSchema): string { + if (schema.$id !== undefined) this.reference_map.set(schema.$id, schema) + if (schema.$id !== undefined && this.emitted_set.has(schema.$id!)) return schema.$id! + if (Types.TypeGuard.IsAny(schema)) return this.Any(schema) + if (Types.TypeGuard.IsArray(schema)) return this.Array(schema) + if (Types.TypeGuard.IsBigInt(schema)) return this.BigInt(schema) + if (Types.TypeGuard.IsBoolean(schema)) return this.Boolean(schema) + if (Types.TypeGuard.IsDate(schema)) return this.Date(schema) + if (Types.TypeGuard.IsConstructor(schema)) return this.Constructor(schema) + if (Types.TypeGuard.IsFunction(schema)) return this.Function(schema) + if (Types.TypeGuard.IsInteger(schema)) return this.Integer(schema) + if (Types.TypeGuard.IsIntersect(schema)) return this.Intersect(schema) + if (Types.TypeGuard.IsLiteral(schema)) return this.Literal(schema) + if (Types.TypeGuard.IsNever(schema)) return this.Never(schema) + if (Types.TypeGuard.IsNull(schema)) return this.Null(schema) + if (Types.TypeGuard.IsNumber(schema)) return this.Number(schema) + if (Types.TypeGuard.IsObject(schema)) return this.Object(schema) + if (Types.TypeGuard.IsPromise(schema)) return this.Promise(schema) + if (Types.TypeGuard.IsRecord(schema)) return this.Record(schema) + if (Types.TypeGuard.IsRef(schema)) return this.Ref(schema) + if (Types.TypeGuard.IsString(schema)) return this.String(schema) + if (Types.TypeGuard.IsTemplateLiteral(schema)) return this.TemplateLiteral(schema) + if (Types.TypeGuard.IsThis(schema)) return this.This(schema) + if (Types.TypeGuard.IsTuple(schema)) return this.Tuple(schema) + if (Types.TypeGuard.IsUint8Array(schema)) return this.UInt8Array(schema) + if (Types.TypeGuard.IsUndefined(schema)) return this.Undefined(schema) + if (Types.TypeGuard.IsUnion(schema)) return this.Union(schema) + if (Types.TypeGuard.IsUnknown(schema)) return this.Unknown(schema) + if (Types.TypeGuard.IsVoid(schema)) return this.Void(schema) + return this.UnsupportedType(schema) + } + Collect(schema: Types.TSchema) { + return [...this.Visit(schema)].join(``) + } + GenerateType(model: TypeBoxModel, schema: Types.TSchema, references: Types.TSchema[]) { + const output: string[] = [] + for (const reference of references) { + if (reference.$id === undefined) return this.UnsupportedType(schema) + this.reference_map.set(reference.$id, reference) + } + const type = this.Collect(schema) + if (this.recursive_set.has(schema.$id!)) { + output.push(`export ${ModelToTypeScript.GenerateType(model, schema.$id!)}`) + output.push(`export const ${schema.$id || `T`}: v.${this.dialect.InferOutput}<${schema.$id}> = v.lazy(() => ${Formatter.Format(type)})`) + } else { + output.push(`export type ${schema.$id} = v.${this.dialect.InferOutput}`) + output.push(`export const ${schema.$id || `T`} = ${Formatter.Format(type)}`) + } + if (schema.$id) this.emitted_set.add(schema.$id) + return output.join('\n') + } } - const reference_map = new Map() - const recursive_set = new Set() - const emitted_set = new Set() - export function Generate(model: TypeBoxModel): string { - reference_map.clear() - recursive_set.clear() - emitted_set.clear() + export function Generate(model: TypeBoxModel, options?: ValibotOptions): string { + const visitor = new ValibotVisitor(options?.version || 'latest') const buffer: string[] = [`import * as v from 'valibot'`, ''] for (const type of model.types.filter((type) => Types.TypeGuard.IsSchema(type))) { - buffer.push(GenerateType(model, type, model.types)) + buffer.push(visitor.GenerateType(model, type, model.types)) } return Formatter.Format(buffer.join('\n')) }