kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
ea4b96be45
commit
68d3934a72
|
@ -28,5 +28,9 @@
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"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 path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
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'
|
import { validateOpenAPISpec } from './validate-openapi-spec'
|
||||||
|
|
||||||
|
@ -11,7 +13,7 @@ const fixtures = [
|
||||||
'firecrawl.json',
|
'firecrawl.json',
|
||||||
'github.json',
|
'github.json',
|
||||||
'notion.json',
|
'notion.json',
|
||||||
// 'open-meteo.yaml', // TODO
|
'open-meteo.yaml',
|
||||||
'pet-store.json',
|
'pet-store.json',
|
||||||
'petstore-expanded.json',
|
'petstore-expanded.json',
|
||||||
'security.json',
|
'security.json',
|
||||||
|
@ -19,20 +21,95 @@ const fixtures = [
|
||||||
'tic-tac-toe.json'
|
'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', () => {
|
describe('validateOpenAPISpec', () => {
|
||||||
for (const fixture of fixtures) {
|
for (const fixture of fixtures) {
|
||||||
test(
|
test(
|
||||||
fixture,
|
`${fixture} (string)`,
|
||||||
{
|
{
|
||||||
timeout: 60_000
|
timeout: 60_000
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const fixturePath = path.join(dirname, 'fixtures', fixture)
|
const fixturePath = path.join(fixturesDir, fixture)
|
||||||
const spec = await readFile(fixturePath, 'utf8')
|
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()
|
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 {
|
import {
|
||||||
BaseResolver,
|
BaseResolver,
|
||||||
bundle,
|
bundle,
|
||||||
type Config as RedoclyConfig,
|
type Config as RedoclyConfig,
|
||||||
type Document,
|
type Document,
|
||||||
lintDocument,
|
lintDocument,
|
||||||
|
makeDocumentFromString,
|
||||||
type NormalizedProblem,
|
type NormalizedProblem,
|
||||||
Source
|
Source
|
||||||
} from '@redocly/openapi-core'
|
} from '@redocly/openapi-core'
|
||||||
|
@ -12,6 +15,11 @@ import {
|
||||||
import type { Logger, LooseOpenAPI3Spec } from './types'
|
import type { Logger, LooseOpenAPI3Spec } from './types'
|
||||||
import { getDefaultRedoclyConfig } from './redocly-config'
|
import { getDefaultRedoclyConfig } from './redocly-config'
|
||||||
|
|
||||||
|
interface ParseSchemaOptions {
|
||||||
|
absoluteRef: string
|
||||||
|
resolver: BaseResolver
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates an OpenAPI spec and bundles it into a single, normalized schema.
|
* 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
|
* Adapted from https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-typescript/src/lib/redoc.ts
|
||||||
*/
|
*/
|
||||||
export async function validateOpenAPISpec(
|
export async function validateOpenAPISpec(
|
||||||
source: string,
|
source: string | URL | Buffer | Record<string, unknown>,
|
||||||
{
|
{
|
||||||
|
cwd,
|
||||||
redoclyConfig,
|
redoclyConfig,
|
||||||
logger = console,
|
logger = console,
|
||||||
silent = false
|
silent = false
|
||||||
}: {
|
}: {
|
||||||
|
cwd?: URL
|
||||||
redoclyConfig?: RedoclyConfig
|
redoclyConfig?: RedoclyConfig
|
||||||
logger?: Logger
|
logger?: Logger
|
||||||
silent?: boolean
|
silent?: boolean
|
||||||
|
@ -35,18 +45,24 @@ export async function validateOpenAPISpec(
|
||||||
redoclyConfig = await getDefaultRedoclyConfig()
|
redoclyConfig = await getDefaultRedoclyConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolver = new BaseResolver(redoclyConfig.resolve)
|
let absoluteRef: string
|
||||||
|
if (source instanceof URL) {
|
||||||
let parsed: any
|
absoluteRef =
|
||||||
try {
|
source.protocol === 'file:' ? fileURLToPath(source) : source.href
|
||||||
parsed = parseJson(source)
|
} else {
|
||||||
} catch (err: any) {
|
absoluteRef = fileURLToPath(new URL(cwd ?? `file://${process.cwd()}/`))
|
||||||
throw new Error(`Invalid OpenAPI spec: ${err.message}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const document: Document = {
|
const resolver = new BaseResolver(redoclyConfig.resolve)
|
||||||
source: new Source('', source, 'application/json'),
|
let document: Document
|
||||||
parsed
|
|
||||||
|
try {
|
||||||
|
document = await parseSchema(source, {
|
||||||
|
absoluteRef,
|
||||||
|
resolver
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(`Invalid OpenAPI spec: ${err.message}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for OpenAPI 3 or greater
|
// Check for OpenAPI 3 or greater
|
||||||
|
@ -88,6 +104,91 @@ export async function validateOpenAPISpec(
|
||||||
return bundled.bundle.parsed
|
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(
|
function _processProblems(
|
||||||
problems: NormalizedProblem[],
|
problems: NormalizedProblem[],
|
||||||
{
|
{
|
||||||
|
|
|
@ -312,6 +312,13 @@ importers:
|
||||||
'@redocly/openapi-core':
|
'@redocly/openapi-core':
|
||||||
specifier: ^1.34.3
|
specifier: ^1.34.3
|
||||||
version: 1.34.3(supports-color@10.0.0)
|
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:
|
packages/schemas:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Ładowanie…
Reference in New Issue