UNPKG

@alexop/openapi-zod-client

Version:

[![Screenshot 2022-11-12 at 18 52 25](https://user-images.githubusercontent.com/47224540/201487856-ffc4c862-6f31-4de1-8ef1-3981fabf3416.png)](https://openapi-zod-client.vercel.app/)

315 lines (261 loc) 11.9 kB
import type { ReferenceObject, SchemaObject } from "openapi3-ts"; import { t, ts } from "tanu"; import type { TypeDefinition, TypeDefinitionObject } from "tanu/dist/type"; import { isReferenceObject } from "./isReferenceObject"; import type { DocumentResolver } from "./makeSchemaResolver"; import type { TemplateContext } from "./template-context"; import { wrapWithQuotesIfNeeded } from "./utils"; type TsConversionArgs = { schema: SchemaObject | ReferenceObject; ctx?: TsConversionContext | undefined; meta?: { name?: string; $ref?: string; isInline?: boolean } | undefined; options?: TemplateContext["options"]; }; export type TsConversionContext = { nodeByRef: Record<string, ts.Node>; resolver: DocumentResolver; rootRef?: string; visitedsRefs?: Record<string, boolean>; }; type MaybeWrapReadOnlyType = | ts.TypeNode | { [k: string]: | number | bigint | boolean | TypeDefinitionObject | ts.TypeNode | ts.TypeAliasDeclaration | ts.InterfaceDeclaration | ts.EnumDeclaration; }; const wrapReadOnly = (options: TemplateContext["options"]) => (theType: MaybeWrapReadOnlyType): MaybeWrapReadOnlyType => { if (options?.allReadonly) { return t.readonly(theType); } return theType; }; export const getTypescriptFromOpenApi = ({ schema, meta: inheritedMeta, ctx, options, }: // eslint-disable-next-line sonarjs/cognitive-complexity TsConversionArgs): ts.Node | TypeDefinitionObject | string => { const meta = {} as TsConversionArgs["meta"]; const isInline = !inheritedMeta?.name; const doWrapReadOnly = wrapReadOnly(options); if (ctx?.visitedsRefs && inheritedMeta?.$ref) { ctx.rootRef = inheritedMeta.$ref; ctx.visitedsRefs[inheritedMeta.$ref] = true; } if (!schema) { throw new Error("Schema is required"); } let canBeWrapped = !isInline; const getTs = (): ts.Node | TypeDefinitionObject | string => { if (isReferenceObject(schema)) { if (!ctx?.visitedsRefs || !ctx?.resolver) throw new Error("Context is required for OpenAPI $ref"); let result = ctx.nodeByRef[schema.$ref]; let schemaName = ctx.resolver.resolveRef(schema.$ref)?.normalized; if (ctx.visitedsRefs[schema.$ref]) { return t.reference(schemaName); } if (!result) { const actualSchema = ctx.resolver.getSchemaByRef(schema.$ref); if (!actualSchema) { throw new Error(`Schema ${schema.$ref} not found`); } ctx.visitedsRefs[schema.$ref] = true; result = getTypescriptFromOpenApi({ schema: actualSchema, meta, ctx, options }) as ts.Node; } if (!schemaName) { schemaName = ctx.resolver.resolveRef(schema.$ref)?.normalized; } return t.reference(schemaName); } if (Array.isArray(schema.type)) { if (schema.type.length === 1) { return getTypescriptFromOpenApi({ schema: { ...schema, type: schema.type[0]! }, ctx, meta, options }); } const types = schema.type.map( (prop) => getTypescriptFromOpenApi({ schema: { ...schema, type: prop }, ctx, meta, options, }) as TypeDefinition ); return schema.nullable ? t.union([...types, t.reference("null")]) : t.union(types); } if (schema.type === "null") { return t.reference("null"); } if (schema.oneOf) { if (schema.oneOf.length === 1) { return getTypescriptFromOpenApi({ schema: schema.oneOf[0]!, ctx, meta, options }); } const types = schema.oneOf.map( (prop) => getTypescriptFromOpenApi({ schema: prop, ctx, meta, options }) as TypeDefinition ); return schema.nullable ? t.union([...types, t.reference("null")]) : t.union(types); } // anyOf = oneOf but with 1 or more = `T extends oneOf ? T | T[] : never` if (schema.anyOf) { if (schema.anyOf.length === 1) { return getTypescriptFromOpenApi({ schema: schema.anyOf[0]!, ctx, meta, options }); } const oneOf = t.union( schema.anyOf.map( (prop) => getTypescriptFromOpenApi({ schema: prop, ctx, meta, options }) as TypeDefinition ) ); return schema.nullable ? t.union([oneOf, doWrapReadOnly(t.array(oneOf)), t.reference("null")]) : t.union([oneOf, doWrapReadOnly(t.array(oneOf))]); } if (schema.allOf) { if (schema.allOf.length === 1) { return getTypescriptFromOpenApi({ schema: schema.allOf[0]!, ctx, meta, options }); } const types = schema.allOf.map( (prop) => getTypescriptFromOpenApi({ schema: prop, ctx, meta, options }) as TypeDefinition ); return schema.nullable ? t.union([t.intersection(types), t.reference("null")]) : t.intersection(types); } const schemaType = schema.type ? (schema.type.toLowerCase() as NonNullable<typeof schema.type>) : undefined; if (schemaType && isPrimitiveType(schemaType)) { if (schema.enum) { if (schemaType !== "string" && schema.enum.some((e) => typeof e === "string")) { return schema.nullable ? t.union([t.never(), t.reference("null")]) : t.never(); } return schema.nullable ? t.union([...schema.enum, t.reference("null")]) : t.union(schema.enum); } if (schemaType === "string") return schema.nullable ? t.union([t.string(), t.reference("null")]) : t.string(); if (schemaType === "boolean") return schema.nullable ? t.union([t.boolean(), t.reference("null")]) : t.boolean(); if (schemaType === "number" || schemaType === "integer") return schema.nullable ? t.union([t.number(), t.reference("null")]) : t.number(); } if (schemaType === "array") { if (schema.items) { let arrayOfType = getTypescriptFromOpenApi({ schema: schema.items, ctx, meta, options, }) as TypeDefinition; if (typeof arrayOfType === "string") { if (!ctx) throw new Error("Context is required for circular $ref (recursive schemas)"); arrayOfType = t.reference(arrayOfType); } return schema.nullable ? t.union([doWrapReadOnly(t.array(arrayOfType)), t.reference("null")]) : doWrapReadOnly(t.array(arrayOfType)); } return schema.nullable ? t.union([doWrapReadOnly(t.array(t.any())), t.reference("null")]) : doWrapReadOnly(t.array(t.any())); } if (schemaType === "object" || schema.properties || schema.additionalProperties) { if (!schema.properties) { return {}; } canBeWrapped = false; const isPartial = !schema.required?.length; let additionalProperties; if (schema.additionalProperties) { let additionalPropertiesType; if ( (typeof schema.additionalProperties === "boolean" && schema.additionalProperties) || (typeof schema.additionalProperties === "object" && Object.keys(schema.additionalProperties).length === 0) ) { additionalPropertiesType = t.any(); } else if (typeof schema.additionalProperties === "object") { additionalPropertiesType = getTypescriptFromOpenApi({ schema: schema.additionalProperties, ctx, meta, options, }); } additionalProperties = ts.factory.createTypeLiteralNode([ ts.factory.createIndexSignature( undefined, [ ts.factory.createParameterDeclaration( undefined, undefined, ts.factory.createIdentifier("key"), undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) ), ], additionalPropertiesType as ts.TypeNode ), ]); } const props = Object.fromEntries( Object.entries(schema.properties).map(([prop, propSchema]) => { let propType = getTypescriptFromOpenApi({ schema: propSchema, ctx, meta, options, }) as TypeDefinition; if (typeof propType === "string") { if (!ctx) throw new Error("Context is required for circular $ref (recursive schemas)"); // TODO Partial ? propType = t.reference(propType); } const isRequired = Boolean(isPartial ? true : schema.required?.includes(prop)); return [`${wrapWithQuotesIfNeeded(prop)}`, isRequired ? propType : t.optional(propType)]; }) ); const objectType = additionalProperties ? t.intersection([props, additionalProperties]) : props; if (isInline) { return isPartial ? t.reference("Partial", [doWrapReadOnly(objectType)]) : doWrapReadOnly(objectType); } if (!inheritedMeta?.name) { throw new Error("Name is required to convert an object schema to a type reference"); } const base = t.type(inheritedMeta.name, doWrapReadOnly(objectType)); if (!isPartial) return base; return t.type(inheritedMeta.name, t.reference("Partial", [doWrapReadOnly(objectType)])); } if (!schemaType) return t.unknown(); // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unsupported schema type: ${schemaType}`); }; const tsResult = getTs(); return canBeWrapped ? wrapTypeIfInline({ isInline, name: inheritedMeta?.name, typeDef: tsResult as TypeDefinition }) : tsResult; }; type SingleType = Exclude<SchemaObject["type"], any[] | undefined>; const isPrimitiveType = (type: SingleType): type is PrimitiveType => primitiveTypeList.includes(type as any); const primitiveTypeList = ["string", "number", "integer", "boolean", "null"] as const; type PrimitiveType = typeof primitiveTypeList[number]; const wrapTypeIfInline = ({ isInline, name, typeDef, }: { isInline: boolean; name: string | undefined; typeDef: t.TypeDefinition; }) => { if (!isInline) { if (!name) { throw new Error("Name is required to convert a schema to a type reference"); } return t.type(name, typeDef); } return typeDef as ts.Node; };