Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

feat: add a migration layer for @apidevtools/swagger-parser #140

Merged
merged 5 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/openapi-parser/src/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const ERRORS = {
INVALID_REFERENCE: 'Can’t resolve reference: %s',
EXTERNAL_REFERENCE_NOT_FOUND: 'Can’t resolve external reference: %s',
FILE_DOES_NOT_EXIST: 'File does not exist: %s',
NO_CONTENT: 'No content found',
} as const

export type VALIDATOR_ERROR = keyof typeof ERRORS
8 changes: 8 additions & 0 deletions packages/openapi-parser/src/lib/Validator/Validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export class Validator {

// AnyObject is not supported
if (!version) {
if (options?.throwOnError) {
throw new Error(ERRORS.OPENAPI_VERSION_NOT_SUPPORTED)
}

return {
valid: false,
errors: transformErrors(
Expand All @@ -105,6 +109,10 @@ export class Validator {
// Error handling
if (validateSchema.errors) {
if (validateSchema.errors.length > 0) {
if (options?.throwOnError) {
throw new Error(validateSchema.errors[0])
}

return {
valid: false,
errors: transformErrors(entrypoint, validateSchema.errors),
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-parser/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type AnyObject = Record<string, any>

export type LoadResult = {
filesystem: Filesystem
errors?: ErrorObject[]
}

export type ValidateResult = {
Expand Down
22 changes: 22 additions & 0 deletions packages/openapi-parser/src/utils/load/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,4 +395,26 @@ describe('load', async () => {
},
})
})

it('returns an error', async () => {
const { errors } = await load('INVALID', {
plugins: [readFiles(), fetchUrls()],
})

expect(errors).toMatchObject([
{
code: 'EXTERNAL_REFERENCE_NOT_FOUND',
message: 'Can’t resolve external reference: INVALID',
},
])
})

it('throws an error', async () => {
expect(async () => {
await load('INVALID', {
plugins: [readFiles(), fetchUrls()],
throwOnError: true,
})
}).rejects.toThrowError('Can’t resolve external reference: INVALID')
})
})
70 changes: 61 additions & 9 deletions packages/openapi-parser/src/utils/load/load.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { Filesystem, LoadResult } from '../../types'
import { ERRORS } from '../../configuration'
import type {
AnyObject,
ErrorObject,
Filesystem,
LoadResult,
ThrowOnErrorOption,
} from '../../types'
import { getEntrypoint } from '../getEntrypoint'
import { getListOfReferences } from '../getListOfReferences'
import { makeFilesystem } from '../makeFilesystem'
Expand All @@ -18,26 +25,64 @@ export async function load(
plugins?: LoadPlugin[]
filename?: string
filesystem?: Filesystem
},
} & ThrowOnErrorOption,
): Promise<LoadResult> {
const errors: ErrorObject[] = []

// Don’t load a reference twice, check the filesystem before fetching something
if (
options?.filesystem &&
options?.filesystem.find((entry) => entry.filename === value)
) {
return {
filesystem: options.filesystem,
errors,
}
}

// Check whether the value is an URL or file path
const plugin = options?.plugins?.find((plugin) => plugin.check(value))
const content = normalize(plugin ? await plugin.get(value) : value)

let content: AnyObject

if (plugin) {
try {
content = normalize(await plugin.get(value))
} catch (error) {
if (options?.throwOnError) {
throw new Error(
ERRORS.EXTERNAL_REFERENCE_NOT_FOUND.replace('%s', value),
)
}

errors.push({
code: 'EXTERNAL_REFERENCE_NOT_FOUND',
message: ERRORS.EXTERNAL_REFERENCE_NOT_FOUND.replace('%s', value),
})

return {
filesystem: [],
errors,
}
}
} else {
content = normalize(value)
}

// No content
if (content === undefined) {
if (options?.throwOnError) {
throw new Error('No content to load')
}

errors.push({
code: 'NO_CONTENT',
message: ERRORS.NO_CONTENT,
})

return {
filesystem: [],
errors,
}
}

Expand All @@ -56,6 +101,7 @@ export async function load(
if (listOfReferences.length === 0) {
return {
filesystem,
errors,
}
}

Expand All @@ -79,12 +125,17 @@ export async function load(
continue
}

const { filesystem: referencedFiles } = await load(target, {
...options,
// Make the filename the exact same value as the $ref
// TODO: This leads to problems, if there are multiple references with the same file name but in different folders
filename: reference,
})
const { filesystem: referencedFiles, errors: newErrors } = await load(
target,
{
...options,
// Make the filename the exact same value as the $ref
// TODO: This leads to problems, if there are multiple references with the same file name but in different folders
filename: reference,
},
)

errors.push(...newErrors)

filesystem = [
...filesystem,
Expand All @@ -99,5 +150,6 @@ export async function load(

return {
filesystem,
errors,
}
}
6 changes: 4 additions & 2 deletions packages/openapi-parser/src/utils/workThroughQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('workThroughQueue', () => {
})

expect(await result).toStrictEqual({
errors: [],
filesystem: [
{
dir: './',
Expand Down Expand Up @@ -65,9 +66,9 @@ describe('workThroughQueue', () => {
})

expect(await result).toStrictEqual({
errors: [],
valid: true,
version: '3.1',
errors: [],
filesystem: [
{
dir: './',
Expand Down Expand Up @@ -124,10 +125,10 @@ describe('workThroughQueue', () => {
})

expect(await result).toStrictEqual({
errors: [],
specificationType: 'openapi',
specificationVersion: '3.1.0',
version: '3.1',
errors: [],
filesystem: [
{
dir: './',
Expand Down Expand Up @@ -184,6 +185,7 @@ describe('workThroughQueue', () => {
})

expect(await result).toStrictEqual({
errors: [],
version: '3.1',
specification: {
openapi: '3.1.0',
Expand Down
23 changes: 23 additions & 0 deletions packages/openapi-parser/tests/migration-layer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"openapi": "3.1.0",
"info": {
"title": "Hello World",
"version": "1.0.0"
},
"paths": {
"/foobar": {
"post": {
"requestBody": {
"$ref": "#/components/requestBodies/Foobar"
}
}
}
},
"components": {
"requestBodies": {
"Foobar": {
"content": {}
}
}
}
}
136 changes: 136 additions & 0 deletions packages/openapi-parser/tests/migration-layer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import OriginalSwaggerParser from '@apidevtools/swagger-parser'
import path from 'node:path'
import { describe, expect, it, vi } from 'vitest'

import { dereference } from '../src/utils/dereference'
import { load } from '../src/utils/load'
import { fetchUrls } from '../src/utils/load/plugins/fetchUrls'
import { readFiles } from '../src/utils/load/plugins/readFiles'
import { validate } from '../src/utils/validate'

const myAPI = JSON.stringify({
openapi: '3.1.0',
info: {
title: 'Hello World',
version: '1.0.0',
},
paths: {
'/foobar': {
post: {
requestBody: {
$ref: '#/components/requestBodies/Foobar',
},
},
},
},
components: {
requestBodies: {
Foobar: {
content: {},
},
},
},
})

class SwaggerParser {
static async validate(api: string, callback: (err: any, api: any) => void) {
try {
const { filesystem } = await load(api, {
plugins: [fetchUrls(), readFiles()],
throwOnError: true,
})

validate(filesystem, {
throwOnError: true,
}).then((result) => {
callback(null, result.schema)
})
} catch (error) {
callback(error, null)
}
}

static async dereference(api: string) {
const { filesystem } = await load(api, {
plugins: [fetchUrls(), readFiles()],
throwOnError: true,
})

return dereference(filesystem).then((result) => result.schema)
}
}

// https://github.com/APIDevTools/swagger-parser?tab=readme-ov-file#example
describe('validate', async () => {
it('validates', async () => {
return new Promise((resolve, reject) => {
SwaggerParser.validate(myAPI, (err, api) => {
if (err) {
reject(err)
} else {
expect(api.info.title).toBe('Hello World')
expect(api.info.version).toBe('1.0.0')

resolve(null)
}
})
})
})

it('throws an error for invalid documents', async () => {
return new Promise((resolve, reject) => {
SwaggerParser.validate('invalid', (err) => {
if (err) {
resolve(null)
} else {
reject()
}
})
})
})
})

// https://apitools.dev/swagger-parser/docs/swagger-parser.html#dereferenceapi-options-callback
describe('dereference', () => {
it('dereferences', async () => {
let api = await SwaggerParser.dereference(myAPI)

// The `api` object is a normal JavaScript object,
// so you can easily access any part of the API using simple dot notation
expect(api?.paths?.['/foobar']?.post?.requestBody?.content).toEqual({})
})

it('dereferences URLs', async () => {
global.fetch = async (url: string) =>
({
text: async () => {
if (url === 'http://example.com/specification/openapi.yaml') {
return myAPI
}

throw new Error('Not found')
},
}) as Response

let api = await SwaggerParser.dereference(
'http://example.com/specification/openapi.yaml',
)

// The `api` object is a normal JavaScript object,
// so you can easily access any part of the API using simple dot notation
expect(api?.paths?.['/foobar']?.post?.requestBody?.content).toEqual({})
})

it('dereferences files', async () => {
const EXAMPLE_FILE = path.join(
new URL(import.meta.url).pathname,
'../../tests/migration-layer.json',
)

let api = await SwaggerParser.dereference(EXAMPLE_FILE)

// The `api` object is a normal JavaScript object,
// so you can easily access any part of the API using simple dot notation
expect(api?.paths?.['/foobar']?.post?.requestBody?.content).toEqual({})
})
})