kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
ea4b96be45
commit
68d3934a72
|
@ -28,5 +28,9 @@
|
|||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hono/node-server": "^1.14.1",
|
||||
"hono": "^4.7.9"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import { readFile } from 'node:fs/promises'
|
|||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { serve } from '@hono/node-server'
|
||||
import { Hono } from 'hono'
|
||||
import { afterAll, assert, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { validateOpenAPISpec } from './validate-openapi-spec'
|
||||
|
||||
|
@ -11,7 +13,7 @@ const fixtures = [
|
|||
'firecrawl.json',
|
||||
'github.json',
|
||||
'notion.json',
|
||||
// 'open-meteo.yaml', // TODO
|
||||
'open-meteo.yaml',
|
||||
'pet-store.json',
|
||||
'petstore-expanded.json',
|
||||
'security.json',
|
||||
|
@ -19,20 +21,95 @@ const fixtures = [
|
|||
'tic-tac-toe.json'
|
||||
]
|
||||
|
||||
const dirname = path.join(fileURLToPath(import.meta.url), '..', '..')
|
||||
const fixturesDir = path.join(
|
||||
fileURLToPath(import.meta.url),
|
||||
'..',
|
||||
'..',
|
||||
'fixtures'
|
||||
)
|
||||
|
||||
let server: ReturnType<typeof serve> | undefined
|
||||
let port: number | undefined
|
||||
|
||||
// Setup a simple HTTP server to test loading remote versions of the fixtures
|
||||
beforeAll(async () => {
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/fixtures/*', async (c) => {
|
||||
const fixtureFile = c.req.path.split('/').at(-1)!
|
||||
assert(fixtureFile, `Missing fixture file: ${c.req.path}`)
|
||||
|
||||
const fixturePath = path.join(fixturesDir, fixtureFile)
|
||||
const spec = await readFile(fixturePath, 'utf8')
|
||||
|
||||
return c.json(spec)
|
||||
})
|
||||
|
||||
await new Promise((resolve) => {
|
||||
port = 6039
|
||||
server = serve(
|
||||
{
|
||||
fetch: app.fetch,
|
||||
port
|
||||
},
|
||||
resolve
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Close the HTTP server
|
||||
afterAll(async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (server) {
|
||||
server.close((err) => (err ? reject(err) : resolve()))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateOpenAPISpec', () => {
|
||||
for (const fixture of fixtures) {
|
||||
test(
|
||||
fixture,
|
||||
`${fixture} (string)`,
|
||||
{
|
||||
timeout: 60_000
|
||||
},
|
||||
async () => {
|
||||
const fixturePath = path.join(dirname, 'fixtures', fixture)
|
||||
const spec = await readFile(fixturePath, 'utf8')
|
||||
const fixturePath = path.join(fixturesDir, fixture)
|
||||
const source = await readFile(fixturePath, 'utf8')
|
||||
|
||||
const result = await validateOpenAPISpec(spec)
|
||||
const result = await validateOpenAPISpec(source)
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`${fixture} (file url)`,
|
||||
{
|
||||
timeout: 60_000
|
||||
},
|
||||
async () => {
|
||||
const source = new URL(`file://${path.join(fixturesDir, fixture)}`)
|
||||
|
||||
const result = await validateOpenAPISpec(source)
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`${fixture} (http url)`,
|
||||
{
|
||||
timeout: 60_000
|
||||
},
|
||||
// eslint-disable-next-line no-loop-func
|
||||
async () => {
|
||||
assert(server)
|
||||
assert(port)
|
||||
|
||||
const source = new URL(`http://localhost:${port}/fixtures/${fixture}`)
|
||||
|
||||
const result = await validateOpenAPISpec(source)
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { parseJson } from '@agentic/platform-core'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { assert, parseJson } from '@agentic/platform-core'
|
||||
import {
|
||||
BaseResolver,
|
||||
bundle,
|
||||
type Config as RedoclyConfig,
|
||||
type Document,
|
||||
lintDocument,
|
||||
makeDocumentFromString,
|
||||
type NormalizedProblem,
|
||||
Source
|
||||
} from '@redocly/openapi-core'
|
||||
|
@ -12,6 +15,11 @@ import {
|
|||
import type { Logger, LooseOpenAPI3Spec } from './types'
|
||||
import { getDefaultRedoclyConfig } from './redocly-config'
|
||||
|
||||
interface ParseSchemaOptions {
|
||||
absoluteRef: string
|
||||
resolver: BaseResolver
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an OpenAPI spec and bundles it into a single, normalized schema.
|
||||
*
|
||||
|
@ -20,12 +28,14 @@ import { getDefaultRedoclyConfig } from './redocly-config'
|
|||
* Adapted from https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-typescript/src/lib/redoc.ts
|
||||
*/
|
||||
export async function validateOpenAPISpec(
|
||||
source: string,
|
||||
source: string | URL | Buffer | Record<string, unknown>,
|
||||
{
|
||||
cwd,
|
||||
redoclyConfig,
|
||||
logger = console,
|
||||
silent = false
|
||||
}: {
|
||||
cwd?: URL
|
||||
redoclyConfig?: RedoclyConfig
|
||||
logger?: Logger
|
||||
silent?: boolean
|
||||
|
@ -35,18 +45,24 @@ export async function validateOpenAPISpec(
|
|||
redoclyConfig = await getDefaultRedoclyConfig()
|
||||
}
|
||||
|
||||
const resolver = new BaseResolver(redoclyConfig.resolve)
|
||||
|
||||
let parsed: any
|
||||
try {
|
||||
parsed = parseJson(source)
|
||||
} catch (err: any) {
|
||||
throw new Error(`Invalid OpenAPI spec: ${err.message}`)
|
||||
let absoluteRef: string
|
||||
if (source instanceof URL) {
|
||||
absoluteRef =
|
||||
source.protocol === 'file:' ? fileURLToPath(source) : source.href
|
||||
} else {
|
||||
absoluteRef = fileURLToPath(new URL(cwd ?? `file://${process.cwd()}/`))
|
||||
}
|
||||
|
||||
const document: Document = {
|
||||
source: new Source('', source, 'application/json'),
|
||||
parsed
|
||||
const resolver = new BaseResolver(redoclyConfig.resolve)
|
||||
let document: Document
|
||||
|
||||
try {
|
||||
document = await parseSchema(source, {
|
||||
absoluteRef,
|
||||
resolver
|
||||
})
|
||||
} catch (err: any) {
|
||||
throw new Error(`Invalid OpenAPI spec: ${err.message}`)
|
||||
}
|
||||
|
||||
// Check for OpenAPI 3 or greater
|
||||
|
@ -88,6 +104,91 @@ export async function validateOpenAPISpec(
|
|||
return bundled.bundle.parsed
|
||||
}
|
||||
|
||||
async function parseSchema(
|
||||
schema: unknown,
|
||||
{ absoluteRef, resolver }: ParseSchemaOptions
|
||||
): Promise<Document> {
|
||||
if (!schema) {
|
||||
throw new Error('Invalid schema: empty')
|
||||
}
|
||||
|
||||
if (schema instanceof URL) {
|
||||
const result = await resolver.resolveDocument(null, absoluteRef, true)
|
||||
|
||||
if ('parsed' in result) {
|
||||
const { parsed } = result
|
||||
if (typeof parsed === 'object') {
|
||||
return result
|
||||
} else if (typeof parsed === 'string') {
|
||||
// Result is a string that we need to parse down below
|
||||
schema = parsed
|
||||
} else {
|
||||
throw new Error('Invalid OpenAPI spec: failed to parse remote schema')
|
||||
}
|
||||
} else {
|
||||
throw result.originalError
|
||||
}
|
||||
}
|
||||
|
||||
if (schema instanceof Buffer) {
|
||||
return parseSchema(schema.toString('utf8'), { absoluteRef, resolver })
|
||||
}
|
||||
|
||||
if (typeof schema === 'string') {
|
||||
schema = schema.trim()
|
||||
assert(typeof schema === 'string')
|
||||
|
||||
// URL
|
||||
if (
|
||||
schema.startsWith('http://') ||
|
||||
schema.startsWith('https://') ||
|
||||
schema.startsWith('file://')
|
||||
) {
|
||||
const url = new URL(schema)
|
||||
|
||||
return parseSchema(url, {
|
||||
absoluteRef: url.protocol === 'file:' ? fileURLToPath(url) : url.href,
|
||||
resolver
|
||||
})
|
||||
}
|
||||
|
||||
// JSON
|
||||
if (schema[0] === '{') {
|
||||
return {
|
||||
source: new Source(absoluteRef, schema, 'application/json'),
|
||||
parsed: parseJson(schema)
|
||||
}
|
||||
}
|
||||
|
||||
// YAML
|
||||
const result = makeDocumentFromString(schema, absoluteRef)
|
||||
if (
|
||||
typeof result !== 'object' ||
|
||||
!('parsed' in result) ||
|
||||
typeof result.parsed !== 'object'
|
||||
) {
|
||||
throw new Error('Invalid OpenAPI spec: failed to parse schema')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (typeof schema === 'object' && !Array.isArray(schema)) {
|
||||
return {
|
||||
source: new Source(
|
||||
absoluteRef,
|
||||
JSON.stringify(schema),
|
||||
'application/json'
|
||||
),
|
||||
parsed: schema
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Error parsing OpenAPI spec: Expected string, object, or Buffer. Got ${Array.isArray(schema) ? 'Array' : typeof schema}`
|
||||
)
|
||||
}
|
||||
|
||||
function _processProblems(
|
||||
problems: NormalizedProblem[],
|
||||
{
|
||||
|
|
|
@ -312,6 +312,13 @@ importers:
|
|||
'@redocly/openapi-core':
|
||||
specifier: ^1.34.3
|
||||
version: 1.34.3(supports-color@10.0.0)
|
||||
devDependencies:
|
||||
'@hono/node-server':
|
||||
specifier: ^1.14.1
|
||||
version: 1.14.1(hono@4.7.9)
|
||||
hono:
|
||||
specifier: ^4.7.9
|
||||
version: 4.7.9
|
||||
|
||||
packages/schemas:
|
||||
dependencies:
|
||||
|
|
Ładowanie…
Reference in New Issue