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

Commit

Permalink
feat: add a migration layer for @apidevtools/swagger-parser (#140)
Browse files Browse the repository at this point in the history
* chore: add a simple migration layer (wip)

* feat: add dereference to the migration layer

* feat: load files, add throwOnError to load

* chore: clean up

* fix: tests
  • Loading branch information
hanspagel authored Aug 27, 2024
1 parent 5706b18 commit 2512b07
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 11 deletions.
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({})
})
})

0 comments on commit 2512b07

Please sign in to comment.