chatgpt-api/packages/openapi-to-ts-client/src/utils.ts

333 wiersze
6.9 KiB
TypeScript

import type SwaggerParser from '@apidevtools/swagger-parser'
import type { IJsonSchema, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
import { assert } from '@agentic/core'
import camelcase from 'camelcase'
import {
type JsonSchema,
jsonSchemaToZod as jsonSchemaToZodImpl,
type ParserOverride
} from 'json-schema-to-zod'
import * as prettier from 'prettier'
export function prettify(source: string): Promise<string> {
return prettier.format(source, {
parser: 'typescript',
semi: false,
singleQuote: true,
jsxSingleQuote: true,
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'always',
trailingComma: 'none'
})
}
export function pascalCase(identifier: string): string {
return camelcase(identifier, { pascalCase: true })
}
export function unTitleCase(identifier: string): string {
return `${identifier.slice(0, 1).toLowerCase()}${identifier.slice(1)}`
}
export function getAndResolve<T extends object = object>(
obj: any,
keys: string[],
refs: SwaggerParser.$Refs,
resolved?: Set<string>,
depth = 0,
maxDepth = 0
): T | null {
if (obj === undefined) return null
if (typeof obj !== 'object') return null
if (!keys.length) {
return dereference(obj, refs, resolved, depth, maxDepth) as T
}
if (obj.$ref) {
const derefed = refs.get(obj.$ref)
resolved?.add(obj.$ref)
if (!derefed) {
return null
}
obj = derefed
}
const key = keys[0]!
const value = obj[key]
keys = keys.slice(1)
if (value === undefined) {
return null
}
return getAndResolve<T>(value, keys, refs, resolved, depth, maxDepth)
}
export function dereferenceFull<T extends object = object>(
obj: T,
refs: SwaggerParser.$Refs,
resolved?: Set<string>
): Exclude<T, OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject> {
return dereference(obj, refs, resolved, 0, Number.POSITIVE_INFINITY) as any
}
export function dereference<T extends object = object>(
obj: T,
refs: SwaggerParser.$Refs,
resolved?: Set<string>,
depth = 0,
maxDepth = 1,
visited = new Set<string>()
): T {
if (!obj) return obj
if (depth >= maxDepth) {
return obj
}
if (Array.isArray(obj)) {
return obj.map((item) =>
dereference(item, refs, resolved, depth + 1, maxDepth, visited)
) as T
} else if (typeof obj === 'object') {
if ('$ref' in obj) {
const ref = obj.$ref as string
if (visited?.has(ref)) {
return obj
}
visited?.add(ref)
const derefed = refs.get(ref)
assert(derefed, `Invalid schema: $ref not found for ${ref}`)
resolved?.add(ref)
derefed.title ??= ref.split('/').pop()!
return dereference(derefed, refs, resolved, depth + 1, maxDepth, visited)
} else {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key,
dereference(value, refs, resolved, depth + 1, maxDepth, visited)
])
) as T
}
} else {
return obj
}
}
function createParserOverride({
type
}: {
type?: string
} = {}): ParserOverride {
const jsonSchemaToZodParserOverride: ParserOverride = (schema, _refs) => {
if ('$ref' in schema) {
const ref = schema.$ref as string
if (!ref) return
const name = getComponentName(ref)
if (!name) return
if (type === name) {
// TODO: Support recursive types.
return `\n// TODO: Support recursive types for \`${name}Schema\`.\nz.any()`
}
return `${name}Schema`
} else if (schema.oneOf) {
const { oneOf, ...partialSchema } = schema
// Replace oneOf with anyOf because `json-schema-to-zod` treats oneOf
// with a complicated `z.any().superRefine(...)` which we'd like messes
// up the resulting types.
const newSchema = {
...partialSchema,
anyOf: oneOf
}
const res = jsonSchemaToZodImpl(newSchema, {
parserOverride: jsonSchemaToZodParserOverride
})
return res
}
}
return jsonSchemaToZodParserOverride
}
export function jsonSchemaToZod(
schema: JsonSchema,
{
name,
type
}: {
name?: string
type?: string
} = {}
): string {
return jsonSchemaToZodImpl(schema, {
name,
module: 'esm',
withJsdocs: true,
type: type ?? true,
noImport: true,
parserOverride: createParserOverride({ type })
})
}
const reservedWords = new Set([
'Error',
'Class',
'Object',
'interface',
'type',
'default',
'switch',
'for',
'break',
'case',
'if',
'else',
'while',
'do',
'for',
'return',
'continue',
'function',
'console',
'window',
'global',
'import',
'export',
'namespace',
'using',
'require',
'module',
'process',
'async',
'await',
'const',
'let',
'var',
'void',
'undefined',
'abstract',
'extends',
'implements',
'private',
'protected',
'public',
'Infinity',
'Nan',
'Math',
'Date',
'RegExp',
'JSON',
'Buffer',
'Promise',
'Symbol',
'Map',
'Set',
'WeakMap',
'WeakSet',
'Array',
'Object',
'Boolean',
'Number',
'String',
'Function',
'Symbol'
])
export function getComponentName(ref: string) {
const name = ref.split('/').pop()!
assert(name, `Invalid ref name ${ref}`)
return name
}
export function getComponentDisplayName(ref: string) {
const name0 = getComponentName(ref)
const name1 = pascalCase(name0)
assert(name1, `Invalid ref name ${ref}`)
if (reservedWords.has(name1)) {
return `${name1}Type`
}
return name1
}
export function getOperationParamsName(
operationName: string,
schemas?: Record<string, string>
) {
const name = `${pascalCase(operationName)}Params`
if (!schemas) return name
let tempName = name
let index = 2
while (schemas[tempName]) {
tempName = `${name}${index}`
++index
}
return tempName
}
export function getOperationResponseName(
operationName: string,
schemas?: Record<string, string>
) {
const name = `${pascalCase(operationName)}Response`
if (!schemas) return name
let tempName = name
let index = 2
while (schemas[tempName]) {
tempName = `${name}${index}`
++index
}
return tempName
}
export function naiveMergeJSONSchemas(...schemas: IJsonSchema[]): IJsonSchema {
const result: any = {}
for (const ischema of schemas) {
const schema = ischema as any
const arrayKeys: string[] = []
const objectKeys: string[] = []
for (const [key, value] of Object.entries(schema)) {
if (Array.isArray(value)) {
arrayKeys.push(key)
} else if (typeof value === 'object') {
objectKeys.push(key)
} else {
result[key] = value
}
}
for (const key of arrayKeys) {
result[key] = [...(result[key] ?? []), ...(schema[key] ?? [])]
}
for (const key of objectKeys) {
result[key] = { ...result[key], ...schema[key] }
}
}
return result as IJsonSchema
}
export function getDescription(description?: string): string | undefined {
if (description && !/[!.?]$/.test(description)) {
description += '.'
}
return description
}