oazapfts
Version:
OpenApi TypeScript client generator
1 lines • 101 kB
Source Map (JSON)
{"version":3,"file":"tscodegen-DV-Rd5p1.cjs","sources":["../src/generateServers.ts","../src/generate.ts","../src/tscodegen.ts"],"sourcesContent":["import _ from \"lodash\";\nimport * as cg from \"./tscodegen\";\nimport ts from \"typescript\";\nimport { OpenAPIV3 } from \"openapi-types\";\n\nconst factory = ts.factory;\n\nfunction createTemplate(url: string) {\n const tokens = url.split(/{([\\s\\S]+?)}/g);\n const chunks = _.chunk(tokens.slice(1), 2);\n return cg.createTemplateString(\n tokens[0],\n chunks.map(([expression, literal]) => ({\n expression: factory.createIdentifier(expression),\n literal,\n })),\n );\n}\n\nfunction createServerFunction(\n template: string,\n vars: Record<string, OpenAPIV3.ServerVariableObject>,\n) {\n const params = [\n cg.createParameter(\n cg.createObjectBinding(\n Object.entries(vars || {}).map(([name, value]) => {\n return {\n name,\n initializer: cg.createLiteral(value.default),\n };\n }),\n ),\n {\n type: factory.createTypeLiteralNode(\n Object.entries(vars || {}).map(([name, value]) => {\n return cg.createPropertySignature({\n name,\n type: value.enum\n ? cg.createEnumTypeNode(value.enum)\n : factory.createUnionTypeNode([\n cg.keywordType.string,\n cg.keywordType.number,\n cg.keywordType.boolean,\n ]),\n });\n }),\n ),\n },\n ),\n ];\n\n return cg.createArrowFunction(params, createTemplate(template));\n}\n\nfunction generateServerExpression(server: OpenAPIV3.ServerObject) {\n return server.variables\n ? createServerFunction(server.url, server.variables)\n : factory.createStringLiteral(server.url);\n}\n\nfunction defaultUrl(server?: OpenAPIV3.ServerObject) {\n if (!server) return \"/\";\n const { url, variables } = server;\n if (!variables) return url;\n return url.replace(/\\{(.+?)\\}/g, (m, name) =>\n variables[name] ? String(variables[name].default) : m,\n );\n}\n\nexport function defaultBaseUrl(servers: OpenAPIV3.ServerObject[]) {\n return factory.createStringLiteral(defaultUrl(servers[0]));\n}\n\nfunction serverName(server: OpenAPIV3.ServerObject, index: number) {\n return server.description\n ? _.camelCase(server.description.replace(/\\W+/, \" \"))\n : `server${index + 1}`;\n}\n\nexport default function generateServers(\n servers: OpenAPIV3.ServerObject[],\n): ts.ObjectLiteralExpression {\n return cg.createObjectLiteral(\n servers.map((server, i) => [\n serverName(server, i),\n generateServerExpression(server),\n ]),\n );\n}\n","import _ from \"lodash\";\nimport ts from \"typescript\";\nimport { OpenAPIV3, OpenAPIV3_1 } from \"openapi-types\";\nimport * as cg from \"./tscodegen\";\nimport generateServers, { defaultBaseUrl } from \"./generateServers\";\nimport { Opts } from \".\";\n\nexport * from \"./tscodegen\";\nexport * from \"./generateServers\";\n\nconst factory = ts.factory;\n\nexport const verbs = [\n \"GET\",\n \"PUT\",\n \"POST\",\n \"DELETE\",\n \"OPTIONS\",\n \"HEAD\",\n \"PATCH\",\n \"TRACE\",\n];\n\ntype ContentType = \"json\" | \"form\" | \"multipart\";\ntype OnlyMode = \"readOnly\" | \"writeOnly\";\ntype OnlyModes = Record<OnlyMode, boolean>;\n\n// Use union of OAS 3.0 and 3.1 types throughout\n// openapi-types does not define boolean json schemas (https://json-schema.org/draft/2020-12/json-schema-core#section-4.3.2)\ntype OpenAPISchemaObject =\n | OpenAPIV3.SchemaObject\n | OpenAPIV3_1.SchemaObject\n | boolean;\ntype OpenAPIReferenceObject =\n | OpenAPIV3.ReferenceObject\n | OpenAPIV3_1.ReferenceObject;\ntype OpenAPIParameterObject =\n | OpenAPIV3.ParameterObject\n | OpenAPIV3_1.ParameterObject;\nexport type OpenAPIDocument = OpenAPIV3.Document | OpenAPIV3_1.Document;\ntype OpenAPIDiscriminatorObject =\n | OpenAPIV3.DiscriminatorObject\n | OpenAPIV3_1.DiscriminatorObject;\ntype OpenAPIResponseObject =\n | OpenAPIV3.ResponseObject\n | OpenAPIV3_1.ResponseObject;\ntype OpenAPIResponsesObject =\n | OpenAPIV3.ResponsesObject\n | OpenAPIV3_1.ResponsesObject;\ntype OpenAPIRequestBodyObject =\n | OpenAPIV3.RequestBodyObject\n | OpenAPIV3_1.RequestBodyObject;\ntype OpenAPIMediaTypeObject =\n | OpenAPIV3.MediaTypeObject\n | OpenAPIV3_1.MediaTypeObject;\ntype OpenAPIOperationObject =\n | OpenAPIV3.OperationObject\n | OpenAPIV3_1.OperationObject;\n\nconst contentTypes: Record<string, ContentType> = {\n \"*/*\": \"json\",\n \"application/json\": \"json\",\n \"application/x-www-form-urlencoded\": \"form\",\n \"multipart/form-data\": \"multipart\",\n};\n\nexport function isMimeType(s: unknown) {\n return typeof s === \"string\" && /^[^/]+\\/[^/]+$/.test(s);\n}\n\nexport function isJsonMimeType(mime: string) {\n return contentTypes[mime] === \"json\" || /\\bjson\\b/i.test(mime);\n}\n\nexport function getBodyFormatter(body?: OpenAPIRequestBodyObject) {\n if (body?.content) {\n for (const contentType of Object.keys(body.content)) {\n const formatter = contentTypes[contentType];\n if (formatter) return formatter;\n if (isJsonMimeType(contentType)) return \"json\";\n }\n }\n}\n\n// Augment SchemaObject type to allow slowly adopting new OAS3.1+ features\n// and support custom vendor extensions.\nexport type SchemaObject = OpenAPISchemaObject & {\n const?: unknown;\n \"x-enumNames\"?: string[];\n \"x-enum-varnames\"?: string[];\n \"x-component-ref-path\"?: string;\n prefixItems?: (OpenAPIReferenceObject | SchemaObject)[];\n};\n\nexport type DiscriminatingSchemaObject = Exclude<SchemaObject, boolean> & {\n discriminator: NonNullable<Exclude<SchemaObject, boolean>[\"discriminator\"]>;\n};\n\n/**\n * Get the name of a formatter function for a given parameter.\n */\nexport function getFormatter({\n style = \"form\",\n explode = true,\n content,\n}: OpenAPIParameterObject) {\n if (content) {\n const medias = Object.keys(content);\n if (medias.length !== 1) {\n throw new Error(\n \"Parameters with content property must specify one media type\",\n );\n }\n if (!isJsonMimeType(medias[0])) {\n throw new Error(\n \"Parameters with content property must specify a JSON compatible media type\",\n );\n }\n return \"json\";\n }\n if (explode && style === \"deepObject\") return \"deep\";\n if (explode) return \"explode\";\n if (style === \"spaceDelimited\") return \"space\";\n if (style === \"pipeDelimited\") return \"pipe\";\n return \"form\";\n}\n\nexport function getOperationIdentifier(id?: string) {\n if (!id) return;\n if (id.match(/[^\\w\\s]/)) return;\n id = _.camelCase(id);\n if (cg.isValidIdentifier(id)) return id;\n}\n\n/**\n * Create a method name for a given operation, either from its operationId or\n * the HTTP verb and path.\n */\nexport function getOperationName(\n verb: string,\n path: string,\n operationId?: string,\n) {\n const id = getOperationIdentifier(operationId);\n if (id) return id;\n path = path.replace(/\\{(.+?)\\}/, \"by $1\").replace(/\\{(.+?)\\}/, \"and $1\");\n return toIdentifier(`${verb} ${path}`);\n}\n\nexport function isNullable(schema?: SchemaObject | OpenAPIReferenceObject) {\n if (typeof schema === \"boolean\") return schema;\n\n if (schema && \"nullable\" in schema)\n return !isReference(schema) && schema.nullable;\n\n return false;\n}\n\nexport function isReference(obj: unknown): obj is OpenAPIReferenceObject {\n return typeof obj === \"object\" && obj !== null && \"$ref\" in obj;\n}\n\n/**\n * Converts a local reference path into an array of property names.\n */\nexport function refPathToPropertyPath(ref: string) {\n if (!ref.startsWith(\"#/\")) {\n throw new Error(\n `External refs are not supported (${ref}). Make sure to call SwaggerParser.bundle() first.`,\n );\n }\n return ref\n .slice(2)\n .split(\"/\")\n .map((s) => decodeURI(s.replace(/~1/g, \"/\").replace(/~0/g, \"~\")));\n}\n\n/**\n * Get the last path component of the given ref.\n */\nfunction getRefBasename(ref: string) {\n return ref.replace(/.+\\//, \"\");\n}\n\n/**\n * Returns a name for the given ref that can be used as basis for a type\n * alias. This usually is the baseName, unless the ref starts with a number,\n * in which case the whole ref is returned, with slashes turned into\n * underscores.\n */\nfunction getRefName(ref: string) {\n const base = getRefBasename(ref);\n if (/^\\d+/.test(base)) {\n return refPathToPropertyPath(ref).join(\"_\");\n }\n return base;\n}\n\n/**\n * If the given object is a ReferenceObject, return the last part of its path.\n */\nexport function getReferenceName(obj: unknown) {\n if (isReference(obj)) {\n return getRefBasename(obj.$ref);\n }\n}\n\nconst onlyModeSuffixes: Record<OnlyMode, string> = {\n readOnly: \"Read\",\n writeOnly: \"Write\",\n};\n\nfunction getOnlyModeSuffix(onlyMode?: OnlyMode) {\n if (!onlyMode) return \"\";\n return onlyModeSuffixes[onlyMode];\n}\n\nexport function toIdentifier(\n s: string,\n upperFirst = false,\n onlyMode?: OnlyMode,\n) {\n let cc = _.camelCase(s) + getOnlyModeSuffix(onlyMode);\n if (upperFirst) cc = _.upperFirst(cc);\n if (cg.isValidIdentifier(cc)) return cc;\n return \"$\" + cc;\n}\n\n/**\n * Create a template string literal from the given OpenAPI urlTemplate.\n * Curly braces in the path are turned into identifier expressions,\n * which are read from the local scope during runtime.\n */\nexport function createUrlExpression(path: string, qs?: ts.Expression) {\n const spans: Array<{ expression: ts.Expression; literal: string }> = [];\n // Use a replacer function to collect spans as a side effect:\n const head = path.replace(\n /(.*?)\\{(.+?)\\}(.*?)(?=\\{|$)/g,\n (_substr, head, name, literal) => {\n const expression = toIdentifier(name);\n spans.push({\n expression: cg.createCall(\n factory.createIdentifier(\"encodeURIComponent\"),\n { args: [factory.createIdentifier(expression)] },\n ),\n literal,\n });\n return head;\n },\n );\n\n if (qs) {\n // add the query string as last span\n spans.push({ expression: qs, literal: \"\" });\n }\n return cg.createTemplateString(head, spans);\n}\n\n/**\n * Create a call expression for one of the QS runtime functions.\n */\nexport function callQsFunction(name: string, args: ts.Expression[]) {\n return cg.createCall(\n factory.createPropertyAccessExpression(\n factory.createIdentifier(\"QS\"),\n name,\n ),\n { args },\n );\n}\n\n/**\n * Create a call expression for one of the oazapfts runtime functions.\n */\nexport function callOazapftsFunction(\n name: string,\n args: ts.Expression[],\n typeArgs?: ts.TypeNode[],\n) {\n return cg.createCall(\n factory.createPropertyAccessExpression(\n factory.createIdentifier(\"oazapfts\"),\n name,\n ),\n { args, typeArgs },\n );\n}\n\n/**\n * Despite its name, OpenApi's `deepObject` serialization does not support\n * deeply nested objects. As a workaround we detect parameters that contain\n * square brackets and merge them into a single object.\n */\nexport function supportDeepObjects(params: OpenAPIParameterObject[]) {\n const res: OpenAPIParameterObject[] = [];\n const merged: any = {};\n params.forEach((p) => {\n const m = /^(.+?)\\[(.*?)\\]/.exec(p.name);\n if (!m) {\n res.push(p);\n return;\n }\n const [, name, prop] = m;\n let obj = merged[name];\n if (!obj) {\n obj = merged[name] = {\n name,\n in: p.in,\n style: \"deepObject\",\n schema: {\n type: \"object\",\n properties: {},\n },\n };\n res.push(obj);\n }\n obj.schema.properties[prop] = p.schema;\n });\n return res;\n}\n\nfunction isKeyOfKeywordType(key: string): key is keyof typeof cg.keywordType {\n return key in cg.keywordType;\n}\n\n/**\n * Main entry point that generates TypeScript code from a given API spec.\n */\nexport default class ApiGenerator {\n constructor(\n public readonly spec: OpenAPIDocument,\n public readonly opts: Opts = {},\n /** Indicates if the document was converted from an older version of the OpenAPI specification. */\n public readonly isConverted = false,\n ) {\n if (this.spec.components?.schemas) {\n this.preprocessComponents(this.spec.components.schemas);\n }\n }\n\n // see `preprocessComponents` for the definition of a discriminating schema\n discriminatingSchemas: Set<string> = new Set();\n\n aliases: (ts.TypeAliasDeclaration | ts.InterfaceDeclaration)[] = [];\n\n enumAliases: ts.Statement[] = [];\n enumRefs: Record<string, { values: string; type: ts.TypeReferenceNode }> = {};\n\n // Collect the types of all referenced schemas so we can export them later\n // Referenced schemas can be pointing at the following versions:\n // - \"base\": The regular type/interface e.g. ExampleSchema\n // - \"readOnly\": The readOnly version e.g. ExampleSchemaRead\n // - \"writeOnly\": The writeOnly version e.g. ExampleSchemaWrite\n refs: Record<\n string,\n {\n base: ts.TypeReferenceNode;\n readOnly?: ts.TypeReferenceNode;\n writeOnly?: ts.TypeReferenceNode;\n }\n > = {};\n\n // Maps a referenced schema to its readOnly/writeOnly status\n // This field should be used exclusively within the `checkSchemaOnlyMode` method\n refsOnlyMode: Map<string, OnlyModes> = new Map();\n\n // Keep track of already used type aliases\n typeAliases: Record<string, number> = {};\n\n reset() {\n this.aliases = [];\n this.enumAliases = [];\n this.refs = {};\n this.typeAliases = {};\n }\n\n resolve<T>(obj: T | OpenAPIReferenceObject) {\n if (!isReference(obj)) return obj;\n const ref = obj.$ref;\n const path = refPathToPropertyPath(ref);\n const resolved = _.get(this.spec, path);\n if (typeof resolved === \"undefined\") {\n throw new Error(`Can't find ${path}`);\n }\n return resolved as T;\n }\n\n resolveArray<T>(array?: Array<T | OpenAPIReferenceObject>) {\n return array ? array.map((el) => this.resolve(el)) : [];\n }\n\n skip(tags?: string[]) {\n const excluded = tags && tags.some((t) => this.opts?.exclude?.includes(t));\n if (excluded) {\n return true;\n }\n if (this.opts?.include) {\n const included = tags && tags.some((t) => this.opts.include?.includes(t));\n return !included;\n }\n return false;\n }\n\n findAvailableRef(ref: string) {\n const available = (ref: string) => {\n try {\n this.resolve({ $ref: ref });\n return false;\n } catch (error) {\n return true;\n }\n };\n\n if (available(ref)) return ref;\n\n let i = 2;\n while (true) {\n const key = ref + String(i);\n if (available(key)) return key;\n i += 1;\n }\n }\n\n getUniqueAlias(name: string) {\n let used = this.typeAliases[name] || 0;\n if (used) {\n this.typeAliases[name] = ++used;\n name += used;\n }\n this.typeAliases[name] = 1;\n return name;\n }\n\n getEnumUniqueAlias(name: string, values: string) {\n // If enum name already exists and have the same values\n if (this.enumRefs[name] && this.enumRefs[name].values == values) {\n return name;\n }\n\n return this.getUniqueAlias(name);\n }\n\n /**\n * Create a type alias for the schema referenced by the given ReferenceObject\n */\n getRefAlias(\n obj: OpenAPIReferenceObject,\n onlyMode?: OnlyMode,\n // If true, the discriminator property of the schema referenced by `obj` will be ignored.\n // This is meant to be used when getting the type of a discriminating schema in an `allOf`\n // construct.\n ignoreDiscriminator?: boolean,\n ) {\n const $ref = ignoreDiscriminator\n ? this.findAvailableRef(obj.$ref + \"Base\")\n : obj.$ref;\n\n if (!this.refs[$ref]) {\n let schema = this.resolve<SchemaObject>(obj);\n\n if (typeof schema !== \"boolean\" && ignoreDiscriminator) {\n schema = _.cloneDeep(schema);\n delete schema.discriminator;\n }\n const name =\n (typeof schema !== \"boolean\" && schema.title) || getRefName($ref);\n const identifier = toIdentifier(name, true);\n\n // When this is a true enum we can reference it directly,\n // no need to create a type alias\n if (this.isTrueEnum(schema, name)) {\n return this.getTypeFromSchema(schema, name);\n }\n\n const alias = this.getUniqueAlias(identifier);\n\n this.refs[$ref] = {\n base: factory.createTypeReferenceNode(alias, undefined),\n readOnly: undefined,\n writeOnly: undefined,\n };\n\n const type = this.getTypeFromSchema(schema, undefined);\n this.aliases.push(\n cg.createTypeAliasDeclaration({\n modifiers: [cg.modifier.export],\n name: alias,\n type,\n }),\n );\n\n const { readOnly, writeOnly } = this.checkSchemaOnlyMode(schema);\n\n if (readOnly) {\n const readOnlyAlias = this.getUniqueAlias(\n toIdentifier(name, true, \"readOnly\"),\n );\n this.refs[$ref][\"readOnly\"] = factory.createTypeReferenceNode(\n readOnlyAlias,\n undefined,\n );\n\n const readOnlyType = this.getTypeFromSchema(schema, name, \"readOnly\");\n this.aliases.push(\n cg.createTypeAliasDeclaration({\n modifiers: [cg.modifier.export],\n name: readOnlyAlias,\n type: readOnlyType,\n }),\n );\n }\n\n if (writeOnly) {\n const writeOnlyAlias = this.getUniqueAlias(\n toIdentifier(name, true, \"writeOnly\"),\n );\n this.refs[$ref][\"writeOnly\"] = factory.createTypeReferenceNode(\n writeOnlyAlias,\n undefined,\n );\n const writeOnlyType = this.getTypeFromSchema(schema, name, \"writeOnly\");\n this.aliases.push(\n cg.createTypeAliasDeclaration({\n modifiers: [cg.modifier.export],\n name: writeOnlyAlias,\n type: writeOnlyType,\n }),\n );\n }\n }\n\n // If not ref fallback to the regular reference\n return this.refs[$ref][onlyMode || \"base\"] ?? this.refs[$ref].base;\n }\n\n getUnionType(\n variants: (OpenAPIReferenceObject | SchemaObject)[],\n discriminator?: OpenAPIDiscriminatorObject,\n onlyMode?: OnlyMode,\n ) {\n if (discriminator) {\n // oneOf + discriminator -> tagged union (polymorphism)\n if (discriminator.propertyName === undefined) {\n throw new Error(\"Discriminators require a propertyName\");\n }\n\n // By default, the last component of the ref name (i.e., after the last trailing slash) is\n // used as the discriminator value for each variant. This can be overridden using the\n // discriminator.mapping property.\n const mappedValues = new Set(\n Object.values(discriminator.mapping || {}).map(getRefBasename),\n );\n\n return factory.createUnionTypeNode(\n (\n [\n ...Object.entries(discriminator.mapping || {}).map(\n ([discriminatorValue, variantRef]) => [\n discriminatorValue,\n { $ref: variantRef },\n ],\n ),\n ...variants\n .filter((variant) => {\n if (!isReference(variant)) {\n // From the Swagger spec: \"When using the discriminator, inline schemas will not be\n // considered.\"\n throw new Error(\n \"Discriminators require references, not inline schemas\",\n );\n }\n return !mappedValues.has(getRefBasename(variant.$ref));\n })\n .map((schema) => {\n const schemaBaseName = getRefBasename(\n (schema as OpenAPIV3.ReferenceObject).$ref,\n );\n const resolvedSchema = this.resolve(\n schema,\n ) as OpenAPIV3.SchemaObject;\n const discriminatorProperty =\n resolvedSchema.properties?.[discriminator.propertyName];\n const variantName =\n discriminatorProperty && \"enum\" in discriminatorProperty\n ? discriminatorProperty?.enum?.[0]\n : \"\";\n return [variantName || schemaBaseName, schema];\n }),\n ] as [string, OpenAPIReferenceObject][]\n ).map(([discriminatorValue, variant]) =>\n // Yields: { [discriminator.propertyName]: discriminatorValue } & variant\n factory.createIntersectionTypeNode([\n factory.createTypeLiteralNode([\n cg.createPropertySignature({\n name: discriminator.propertyName,\n type: factory.createLiteralTypeNode(\n factory.createStringLiteral(discriminatorValue),\n ),\n }),\n ]),\n this.getTypeFromSchema(variant, undefined, onlyMode),\n ]),\n ),\n );\n } else {\n // oneOf -> untagged union\n return factory.createUnionTypeNode(\n variants.map((schema) =>\n this.getTypeFromSchema(schema, undefined, onlyMode),\n ),\n );\n }\n }\n\n /**\n * Creates a type node from a given schema.\n * Delegates to getBaseTypeFromSchema internally and\n * optionally adds a union with null.\n */\n getTypeFromSchema(\n schema?: SchemaObject | OpenAPIReferenceObject,\n name?: string,\n onlyMode?: OnlyMode,\n ) {\n const type = this.getBaseTypeFromSchema(schema, name, onlyMode);\n return isNullable(schema)\n ? factory.createUnionTypeNode([type, cg.keywordType.null])\n : type;\n }\n\n /**\n * This is the very core of the OpenAPI to TS conversion - it takes a\n * schema and returns the appropriate type.\n */\n getBaseTypeFromSchema(\n schema?: SchemaObject | OpenAPIReferenceObject,\n name?: string,\n onlyMode?: OnlyMode,\n ): ts.TypeNode {\n if (!schema && typeof schema !== \"boolean\") return cg.keywordType.any;\n if (isReference(schema)) {\n return this.getRefAlias(schema, onlyMode) as ts.TypeReferenceNode;\n }\n\n if (schema === true) {\n return cg.keywordType.any;\n }\n\n if (schema === false) {\n return cg.keywordType.never;\n }\n\n if (schema.oneOf) {\n const clone = { ...schema };\n delete clone.oneOf;\n // oneOf -> union\n return this.getUnionType(\n schema.oneOf.map((variant) =>\n // ensure that base properties from the schema are included in the oneOf variants\n _.mergeWith({}, clone, variant, (objValue, srcValue) => {\n if (_.isArray(objValue)) {\n return objValue.concat(srcValue);\n }\n }),\n ),\n schema.discriminator,\n onlyMode,\n );\n }\n if (schema.anyOf) {\n // anyOf -> union\n return this.getUnionType(schema.anyOf, undefined, onlyMode);\n }\n if (schema.discriminator?.mapping) {\n // discriminating schema -> union\n const mapping = schema.discriminator.mapping;\n return this.getUnionType(\n Object.values(mapping).map((ref) => ({ $ref: ref })),\n undefined,\n onlyMode,\n );\n }\n if (schema.allOf) {\n // allOf -> intersection\n const types = [];\n for (const childSchema of schema.allOf) {\n if (\n isReference(childSchema) &&\n this.discriminatingSchemas.has(childSchema.$ref)\n ) {\n const discriminatingSchema =\n this.resolve<DiscriminatingSchemaObject>(childSchema);\n const discriminator = discriminatingSchema.discriminator;\n const matched = Object.entries(discriminator.mapping || {}).find(\n ([, ref]) => ref === schema[\"x-component-ref-path\"],\n );\n if (matched) {\n const [discriminatorValue] = matched;\n types.push(\n factory.createTypeLiteralNode([\n cg.createPropertySignature({\n name: discriminator.propertyName,\n type: factory.createLiteralTypeNode(\n factory.createStringLiteral(discriminatorValue),\n ),\n }),\n ]),\n );\n }\n types.push(\n this.getRefAlias(\n childSchema,\n onlyMode,\n /* ignoreDiscriminator */ true,\n ),\n );\n } else {\n types.push(\n this.getTypeFromSchema(\n {\n required: schema.required,\n ...childSchema,\n },\n undefined,\n onlyMode,\n ),\n );\n }\n }\n\n if (schema.properties || schema.additionalProperties) {\n // properties -> literal type\n types.push(\n this.getTypeFromProperties(\n schema.properties || {},\n schema.required,\n schema.additionalProperties,\n onlyMode,\n ),\n );\n }\n return factory.createIntersectionTypeNode(types);\n }\n // Union types defined by an array in schema.type\n if (Array.isArray(schema.type)) {\n return factory.createUnionTypeNode(\n schema.type.map((type) => {\n const subSchema = { ...schema, type } as SchemaObject;\n // Remove items if the type isn't array since it's not relevant\n if (\"items\" in subSchema && type !== \"array\") {\n delete subSchema.items;\n }\n if (\"properties\" in subSchema && type !== \"object\") {\n delete subSchema.properties;\n }\n\n return this.getBaseTypeFromSchema(subSchema, name, onlyMode);\n }),\n );\n }\n if (\"items\" in schema) {\n const schemaItems = schema.items as OpenAPIV3.BaseSchemaObject;\n\n // items -> array of enums or unions\n if (schemaItems.enum) {\n const enumType = this.isTrueEnum(schemaItems, name)\n ? this.getTrueEnum(schemaItems, name)\n : cg.createEnumTypeNode(schemaItems.enum);\n\n return factory.createArrayTypeNode(enumType);\n }\n\n // items -> array\n return factory.createArrayTypeNode(\n this.getTypeFromSchema(schema.items, undefined, onlyMode),\n );\n }\n if (\"prefixItems\" in schema && schema.prefixItems) {\n // prefixItems -> typed tuple\n return factory.createTupleTypeNode(\n schema.prefixItems.map((schema) => this.getTypeFromSchema(schema)),\n );\n }\n if (schema.properties || schema.additionalProperties) {\n // properties -> literal type\n return this.getTypeFromProperties(\n schema.properties || {},\n schema.required,\n schema.additionalProperties,\n onlyMode,\n );\n }\n if (schema.enum) {\n // enum -> enum or union\n return this.isTrueEnum(schema, name)\n ? this.getTrueEnum(schema, name)\n : cg.createEnumTypeNode(schema.enum);\n }\n if (schema.format == \"binary\") {\n return factory.createTypeReferenceNode(\"Blob\", []);\n }\n if (schema.const) {\n return this.getTypeFromEnum([schema.const]);\n }\n if (schema.type !== undefined) {\n if (schema.type === null) return cg.keywordType.null;\n if (isKeyOfKeywordType(schema.type)) return cg.keywordType[schema.type];\n return cg.keywordType.any;\n }\n\n return cg.keywordType.any;\n }\n\n isTrueEnum(schema: SchemaObject, name?: string): name is string {\n return Boolean(\n typeof schema !== \"boolean\" &&\n schema.enum &&\n this.opts.useEnumType &&\n name &&\n schema.type !== \"boolean\",\n );\n }\n\n /**\n * Creates literal type (or union) from an array of values\n */\n getTypeFromEnum(values: unknown[]) {\n const types = values.map((s) => {\n if (s === null) return cg.keywordType.null;\n if (typeof s === \"boolean\")\n return s\n ? factory.createLiteralTypeNode(\n ts.factory.createToken(ts.SyntaxKind.TrueKeyword),\n )\n : factory.createLiteralTypeNode(\n ts.factory.createToken(ts.SyntaxKind.FalseKeyword),\n );\n if (typeof s === \"number\")\n return factory.createLiteralTypeNode(factory.createNumericLiteral(s));\n if (typeof s === \"string\")\n return factory.createLiteralTypeNode(factory.createStringLiteral(s));\n throw new Error(`Unexpected ${String(s)} of type ${typeof s} in enum`);\n });\n return types.length > 1 ? factory.createUnionTypeNode(types) : types[0];\n }\n\n getEnumValuesString(values: string[]) {\n return values.join(\"_\");\n }\n\n /*\n Creates a enum \"ref\" if not used, reuse existing if values and name matches or creates a new one\n with a new name adding a number\n */\n getTrueEnum(schema: SchemaObject, propName: string) {\n if (typeof schema === \"boolean\") {\n // this should never be thrown, since the only `getTrueEnum` call is\n // behind an `isTrueEnum` check, which returns false for boolean schemas.\n throw new Error(\n \"cannot get enum from boolean schema. schema must be an object\",\n );\n }\n const baseName = schema.title || _.upperFirst(propName);\n // TODO: use _.camelCase in future major version\n // (currently we allow _ and $ for backwards compatibility)\n const proposedName = baseName\n .split(/[^A-Za-z0-9$_]/g)\n .map((n) => _.upperFirst(n))\n .join(\"\");\n const stringEnumValue = this.getEnumValuesString(\n schema.enum ? schema.enum : [],\n );\n\n const name = this.getEnumUniqueAlias(proposedName, stringEnumValue);\n\n if (this.enumRefs[proposedName] && proposedName === name) {\n return this.enumRefs[proposedName].type;\n }\n\n const values = schema.enum ? schema.enum : [];\n\n const names = schema[\"x-enumNames\"] ?? schema[\"x-enum-varnames\"];\n if (names) {\n if (!Array.isArray(names)) {\n throw new Error(\"enum names must be an array\");\n }\n if (names.length !== values.length) {\n throw new Error(\"enum names must have the same length as enum values\");\n }\n }\n\n const members = values.map((s, index) => {\n if (schema.type === \"number\" || schema.type === \"integer\") {\n const name = names ? names[index] : String(s);\n return factory.createEnumMember(\n factory.createIdentifier(toIdentifier(name, true)),\n cg.createLiteral(s),\n );\n }\n return factory.createEnumMember(\n factory.createIdentifier(toIdentifier(s, true)),\n cg.createLiteral(s),\n );\n });\n this.enumAliases.push(\n factory.createEnumDeclaration([cg.modifier.export], name, members),\n );\n\n const type = factory.createTypeReferenceNode(name, undefined);\n\n this.enumRefs[proposedName] = {\n values: stringEnumValue,\n type: factory.createTypeReferenceNode(name, undefined),\n };\n\n return type;\n }\n\n /**\n * Checks if readOnly/writeOnly properties are present in the given schema.\n * Returns a tuple of booleans; the first one is about readOnly, the second\n * one is about writeOnly.\n */\n checkSchemaOnlyMode(\n schema: SchemaObject | OpenAPIReferenceObject,\n resolveRefs = true,\n ): OnlyModes {\n if (this.opts.mergeReadWriteOnly) {\n return { readOnly: false, writeOnly: false };\n }\n\n const check = (\n schema: SchemaObject | OpenAPIReferenceObject,\n history: Set<string>,\n ): OnlyModes => {\n if (isReference(schema)) {\n if (!resolveRefs) return { readOnly: false, writeOnly: false };\n\n // history is used to prevent infinite recursion\n if (history.has(schema.$ref))\n return { readOnly: false, writeOnly: false };\n\n // check if the result is cached in `this.refsOnlyMode`\n const cached = this.refsOnlyMode.get(schema.$ref);\n if (cached) return cached;\n\n history.add(schema.$ref);\n const ret = check(this.resolve(schema), history);\n history.delete(schema.$ref);\n\n // cache the result\n this.refsOnlyMode.set(schema.$ref, ret);\n\n return ret;\n }\n\n if (typeof schema === \"boolean\") {\n return { readOnly: false, writeOnly: false };\n }\n\n let readOnly = schema.readOnly ?? false;\n let writeOnly = schema.writeOnly ?? false;\n\n const subSchemas: (OpenAPIReferenceObject | SchemaObject)[] = [];\n if (\"items\" in schema && schema.items) {\n subSchemas.push(schema.items);\n } else {\n subSchemas.push(...Object.values(schema.properties ?? {}));\n subSchemas.push(...(schema.allOf ?? []));\n subSchemas.push(...(schema.anyOf ?? []));\n subSchemas.push(...(schema.oneOf ?? []));\n }\n\n for (const schema of subSchemas) {\n // `readOnly` and `writeOnly` do not change once they become true,\n // so you can exit early if both are true.\n if (readOnly && writeOnly) break;\n\n const result = check(schema, history);\n readOnly = readOnly || result.readOnly;\n writeOnly = writeOnly || result.writeOnly;\n }\n\n return { readOnly, writeOnly };\n };\n\n return check(schema, new Set<string>());\n }\n\n /**\n * Recursively creates a type literal with the given props.\n */\n getTypeFromProperties(\n props: {\n [prop: string]: SchemaObject | OpenAPIReferenceObject;\n },\n required?: string[],\n additionalProperties?:\n | boolean\n | OpenAPISchemaObject\n | OpenAPIReferenceObject,\n onlyMode?: OnlyMode,\n ): ts.TypeLiteralNode {\n // Check if any of the props are readOnly or writeOnly schemas\n const propertyNames = Object.keys(props);\n const filteredPropertyNames = propertyNames.filter((name) => {\n const schema = props[name];\n const { readOnly, writeOnly } = this.checkSchemaOnlyMode(schema, false);\n\n switch (onlyMode) {\n case \"readOnly\":\n return readOnly || !writeOnly;\n case \"writeOnly\":\n return writeOnly || !readOnly;\n default:\n return !readOnly && !writeOnly;\n }\n });\n\n const members: ts.TypeElement[] = filteredPropertyNames.map((name) => {\n const schema = props[name];\n const isRequired = required && required.includes(name);\n let type = this.getTypeFromSchema(schema, name, onlyMode);\n if (!isRequired && this.opts.unionUndefined) {\n type = factory.createUnionTypeNode([type, cg.keywordType.undefined]);\n }\n\n const signature = cg.createPropertySignature({\n questionToken: !isRequired,\n name,\n type,\n });\n\n if (\n typeof schema !== \"boolean\" &&\n \"description\" in schema &&\n schema.description\n ) {\n // Escape any JSDoc comment closing tags in description\n const description = schema.description.replace(\"*/\", \"*\\\\/\");\n\n ts.addSyntheticLeadingComment(\n signature,\n ts.SyntaxKind.MultiLineCommentTrivia,\n // Ensures it is formatted like a JSDoc comment: /** description here */\n `* ${description} `,\n true,\n );\n }\n\n return signature;\n });\n if (additionalProperties) {\n const type =\n additionalProperties === true\n ? cg.keywordType.any\n : this.getTypeFromSchema(additionalProperties, undefined, onlyMode);\n\n members.push(cg.createIndexSignature(type));\n }\n return factory.createTypeLiteralNode(members);\n }\n\n getTypeFromResponses(responses: OpenAPIResponsesObject, onlyMode?: OnlyMode) {\n return factory.createUnionTypeNode(\n Object.entries(responses).map(([code, res]) => {\n const statusType =\n code === \"default\"\n ? cg.keywordType.number\n : factory.createLiteralTypeNode(factory.createNumericLiteral(code));\n\n const props = [\n cg.createPropertySignature({\n name: \"status\",\n type: statusType,\n }),\n ];\n\n const dataType = this.getTypeFromResponse(res, onlyMode);\n if (dataType !== cg.keywordType.void) {\n props.push(\n cg.createPropertySignature({\n name: \"data\",\n type: dataType,\n }),\n );\n }\n return factory.createTypeLiteralNode(props);\n }),\n );\n }\n\n getTypeFromResponse(\n resOrRef: OpenAPIResponseObject | OpenAPIReferenceObject,\n onlyMode?: OnlyMode,\n ) {\n const res = this.resolve(resOrRef);\n if (!res || !res.content) return cg.keywordType.void;\n return this.getTypeFromSchema(\n this.getSchemaFromContent(res.content),\n undefined,\n onlyMode,\n );\n }\n\n getResponseType(\n responses?: OpenAPIResponsesObject,\n ): \"json\" | \"text\" | \"blob\" {\n // backwards-compatibility\n if (!responses) return \"text\";\n\n const resolvedResponses = Object.values(responses).map((response) =>\n this.resolve(response),\n );\n\n // if no content is specified, assume `text` (backwards-compatibility)\n if (\n !resolvedResponses.some(\n (res) => Object.keys(res.content ?? {}).length > 0,\n )\n ) {\n return \"text\";\n }\n\n const isJson = resolvedResponses.some((response) => {\n const responseMimeTypes = Object.keys(response.content ?? {});\n return responseMimeTypes.some(isJsonMimeType);\n });\n\n // if there’s `application/json` or `*/*`, assume `json`\n if (isJson) {\n return \"json\";\n }\n\n // if there’s `text/*`, assume `text`\n if (\n resolvedResponses.some((res) =>\n Object.keys(res.content ?? []).some((type) => type.startsWith(\"text/\")),\n )\n ) {\n return \"text\";\n }\n\n // for the rest, assume `blob`\n return \"blob\";\n }\n\n getSchemaFromContent(\n content: Record<string, OpenAPIMediaTypeObject>,\n ): OpenAPISchemaObject | OpenAPIReferenceObject {\n const contentType = Object.keys(content).find(isMimeType);\n if (contentType) {\n const { schema } = content[contentType];\n if (schema) {\n return schema;\n }\n }\n\n // if no content is specified -> string\n // `text/*` -> string\n if (\n Object.keys(content).length === 0 ||\n Object.keys(content).some((type) => type.startsWith(\"text/\"))\n ) {\n return { type: \"string\" };\n }\n\n // rest (e.g. `application/octet-stream`, `application/gzip`, …) -> binary\n return { type: \"string\", format: \"binary\" };\n }\n\n getTypeFromParameter(p: OpenAPIParameterObject) {\n if (p.content) {\n const schema = this.getSchemaFromContent(p.content);\n return this.getTypeFromSchema(schema);\n }\n return this.getTypeFromSchema(isReference(p) ? p : p.schema);\n }\n\n wrapResult(ex: ts.Expression) {\n return this.opts?.optimistic ? callOazapftsFunction(\"ok\", [ex]) : ex;\n }\n\n /**\n * Does three things:\n * 1. Add a `x-component-ref-path` property.\n * 2. Record discriminating schemas in `this.discriminatingSchemas`. A discriminating schema\n * refers to a schema that has a `discriminator` property which is neither used in conjunction\n * with `oneOf` nor `anyOf`.\n * 3. Make all mappings of discriminating schemas explicit to generate types immediately.\n */\n preprocessComponents(schemas: {\n [key: string]: OpenAPIReferenceObject | SchemaObject;\n }) {\n const prefix = \"#/components/schemas/\";\n\n // First scan: Add `x-component-ref-path` property and record discriminating schemas\n for (const name of Object.keys(schemas)) {\n const schema = schemas[name];\n if (isReference(schema) || typeof schema === \"boolean\") continue;\n\n schema[\"x-component-ref-path\"] = prefix + name;\n\n if (\n typeof schema !== \"boolean\" &&\n schema.discriminator &&\n !schema.oneOf &&\n !schema.anyOf\n ) {\n this.discriminatingSchemas.add(prefix + name);\n }\n }\n\n const isExplicit = (\n discriminator: OpenAPIDiscriminatorObject,\n ref: string,\n ) => {\n const refs = Object.values(discriminator.mapping || {});\n return refs.includes(ref);\n };\n\n // Second scan: Make all mappings of discriminating schemas explicit\n for (const name of Object.keys(schemas)) {\n const schema = schemas[name];\n\n if (isReference(schema) || typeof schema === \"boolean\" || !schema.allOf) {\n continue;\n }\n\n for (const childSchema of schema.allOf) {\n if (\n !isReference(childSchema) ||\n !this.discriminatingSchemas.has(childSchema.$ref)\n ) {\n continue;\n }\n\n const discriminatingSchema = schemas[\n getRefBasename(childSchema.$ref)\n ] as DiscriminatingSchemaObject;\n const discriminator = discriminatingSchema.discriminator!;\n\n if (isExplicit(discriminator, prefix + name)) continue;\n if (!discriminator.mapping) {\n discriminator.mapping = {};\n }\n discriminator.mapping[name] = prefix + name;\n }\n }\n }\n\n generateApi() {\n this.reset();\n\n // Parse ApiStub.ts so that we don't have to generate everything manually\n const stub = ts.createSourceFile(\n \"ApiStub.ts\",\n __API_STUB_PLACEHOLDER__, // replaced with ApiStub.ts during build\n ts.ScriptTarget.Latest,\n /*setParentNodes*/ false,\n ts.ScriptKind.TS,\n );\n\n // ApiStub contains `const servers = {}`, find it ...\n const servers = cg.findFirstVariableDeclaration(stub.statements, \"servers\");\n // servers.initializer is readonly, this might break in a future TS version, but works fine for now.\n Object.assign(servers, {\n initializer: generateServers(this.spec.servers || []),\n });\n\n const { initializer } = cg.findFirstVariableDeclaration(\n stub.statements,\n \"defaults\",\n );\n if (!initializer || !ts.isObjectLiteralExpression(initializer)) {\n throw new Error(\"No object literal: defaults\");\n }\n\n cg.changePropertyValue(\n initializer,\n \"baseUrl\",\n defaultBaseUrl(this.spec.servers || []),\n );\n\n // Collect class functions to be added...\n const functions: ts.FunctionDeclaration[] = [];\n\n // Keep track of names to detect duplicates\n const names: Record<string, number> = {};\n\n if (this.spec.paths) {\n Object.keys(this.spec.paths).forEach((path) => {\n if (!this.spec.paths) return;\n\n const item = this.spec.paths[path];\n\n if (!item) {\n return;\n }\n\n Object.keys(this.resolve(item)).forEach((verb) => {\n const method = verb.toUpperCase();\n // skip summary/description/parameters etc...\n if (!verbs.includes(method)) return;\n\n const op: OpenAPIOperationObject = (item as any)[verb];\n const {\n operationId,\n requestBody,\n responses,\n summary,\n description,\n tags,\n } = op;\n\n if (this.skip(tags)) {\n return;\n }\n\n let name = getOperationName(verb, path, operationId);\n const count = (names[name] = (names[name] || 0) + 1);\n if (count > 1) {\n // The name is already taken, which means that the spec is probably\n // invalid as operationIds must be unique. Since this is quite common\n // nevertheless we append a counter:\n name += count;\n }\n\n // merge item and op parameters\n const resolvedParameters = this.resolveArray(item.parameters);\n for (const p of this.resolveArray(op.parameters)) {\n const existing = resolvedParameters.find(\n (r) => r.name === p.name && r.in === p.in,\n );\n if (!existing) {\n resolvedParameters.push(p);\n }\n }\n\n // expand older OpenAPI parameters into deepObject style where needed\n const parameters = this.isConverted\n ? supportDeepObjects(resolvedParameters)\n : resolvedParameters;\n\n // convert parameter names to argument names ...\n const argNames = new Map<OpenAPIParameterObject, string>();\n _.sortBy(parameters, \"name.length\").forEach((p) => {\n const identifier = toIdentifier(p.name);\n const existing = [...argNames.values()];\n const suffix = existing.includes(identifier)\n ? _.upperFirst(p.in)\n : \"\";\n argNames.set(p, identifier + suffix);\n });\n\n const getArgName = (param: OpenAPIParameterObject) => {\n const name = argNames.get(param);\n if (!name) throw new Error(`Can't find parameter: ${param.name}`);\n return name;\n };\n\n const methodParams: ts.ParameterDeclaration[] = [];\n let body: OpenAPIRequestBodyObject | undefined = undefined;\n let bodyVar: string | undefined = undefined;\n switch (this.opts.argumentStyle ?? \"positional\") {\n case \"positional\":\n // split into required/optional\n const [required, optional] = _.partition(parameters, \"required\");\n\n // build the method signature - first all the required parameters\n const requiredParams = required.map((p) =>\n cg.createParameter(getArgName(this.resolve(p)), {\n type: this.getTypeFromParameter(p),\n }),\n );\n methodParams.push(...requiredParams);\n\n // add body if present\n if (requestBody) {\n body = this.resolve(requestBody);\n const schema = this.getSchemaFromContent(body.content);\n const type = this.getTypeFromSchema(\n schema,\n undefined,\n \"writeOnly\",\n );\n bodyVar = toIdentifier(\n (type as any).name || getReferenceName(schema) || \"body\",\n );\n methodParams.push(\n cg.createParameter(bodyVar, {\n type,\n questionToken: !body.required,\n }),\n );\n }\n\n // add an object with all optional parameters\n if (optional.length) {\n methodParams.push(\n cg.createParameter(\n cg.createObjectBinding(\n optional\n .map((param) => this.resolve(param))\n .map((param) => ({ name: getArgName(param) })),\n ),\n {\n initializer: factory.createObjectLiteralExpression(),\n type: factory.createTypeLiteralNode(\n optional.map((p) =>\n cg.createPropertySignature({\n name: getArgName(this.resolve(p)),\n questionToken: true,\n type: this.getTypeFromParameter(p),\n }),\n ),\n ),\n },\n ),\n );\n }\n break;\n\n case \"object\":\n // build the method signature - first all the required/optional parameters\n const paramMembers = parameters.map((p) =>\n cg.createPropertySignature({\n name: getArgName(this.resolve(p)),\n questionToken: !p.required,\n type: this.getTypeFromParameter(p),\n }),\n );\n\n // add body if present\n if (requestBody) {\n body = this.resolve(requestBody);\n const schema = this.getSchemaFromContent(body.content);\n const type = this.getTypeFromSchema(\n schema,\n undefined,\n \"writeOnly\",\n );\n bodyVar = toIdentifier(\n (type as any).name || getReferenceName(schema) || \"body\",\n );\n paramMembers.push(\n cg.createPropertySignature({\n name: bodyVar,\n questionToken: !body.required,\n type,\n }),\n );\n }\n\n // if there's no params, leave methodParams as is and prevent empty object argument generation\n if (paramMembers.length === 0) {\n break;\n }\n\n methodParams.push(\n cg.createParameter(\n cg.createObjectBinding([\n ...parameters\n .map((param) => this.resolve(param))\n .map((param) => ({ name: getArgName(param) })),\n ...(bodyVar ? [{ name: bodyVar }] : []),\n