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

Commit 2512b07

Browse files
authored
feat: add a migration layer for @apidevtools/swagger-parser (#140)
* 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
1 parent 5706b18 commit 2512b07

File tree

8 files changed

+256
-11
lines changed

8 files changed

+256
-11
lines changed

packages/openapi-parser/src/configuration/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const ERRORS = {
2828
INVALID_REFERENCE: 'Can’t resolve reference: %s',
2929
EXTERNAL_REFERENCE_NOT_FOUND: 'Can’t resolve external reference: %s',
3030
FILE_DOES_NOT_EXIST: 'File does not exist: %s',
31+
NO_CONTENT: 'No content found',
3132
} as const
3233

3334
export type VALIDATOR_ERROR = keyof typeof ERRORS

packages/openapi-parser/src/lib/Validator/Validator.ts

+8
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export class Validator {
8989

9090
// AnyObject is not supported
9191
if (!version) {
92+
if (options?.throwOnError) {
93+
throw new Error(ERRORS.OPENAPI_VERSION_NOT_SUPPORTED)
94+
}
95+
9296
return {
9397
valid: false,
9498
errors: transformErrors(
@@ -105,6 +109,10 @@ export class Validator {
105109
// Error handling
106110
if (validateSchema.errors) {
107111
if (validateSchema.errors.length > 0) {
112+
if (options?.throwOnError) {
113+
throw new Error(validateSchema.errors[0])
114+
}
115+
108116
return {
109117
valid: false,
110118
errors: transformErrors(entrypoint, validateSchema.errors),

packages/openapi-parser/src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type AnyObject = Record<string, any>
66

77
export type LoadResult = {
88
filesystem: Filesystem
9+
errors?: ErrorObject[]
910
}
1011

1112
export type ValidateResult = {

packages/openapi-parser/src/utils/load/load.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,26 @@ describe('load', async () => {
395395
},
396396
})
397397
})
398+
399+
it('returns an error', async () => {
400+
const { errors } = await load('INVALID', {
401+
plugins: [readFiles(), fetchUrls()],
402+
})
403+
404+
expect(errors).toMatchObject([
405+
{
406+
code: 'EXTERNAL_REFERENCE_NOT_FOUND',
407+
message: 'Can’t resolve external reference: INVALID',
408+
},
409+
])
410+
})
411+
412+
it('throws an error', async () => {
413+
expect(async () => {
414+
await load('INVALID', {
415+
plugins: [readFiles(), fetchUrls()],
416+
throwOnError: true,
417+
})
418+
}).rejects.toThrowError('Can’t resolve external reference: INVALID')
419+
})
398420
})

packages/openapi-parser/src/utils/load/load.ts

+61-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { Filesystem, LoadResult } from '../../types'
1+
import { ERRORS } from '../../configuration'
2+
import type {
3+
AnyObject,
4+
ErrorObject,
5+
Filesystem,
6+
LoadResult,
7+
ThrowOnErrorOption,
8+
} from '../../types'
29
import { getEntrypoint } from '../getEntrypoint'
310
import { getListOfReferences } from '../getListOfReferences'
411
import { makeFilesystem } from '../makeFilesystem'
@@ -18,26 +25,64 @@ export async function load(
1825
plugins?: LoadPlugin[]
1926
filename?: string
2027
filesystem?: Filesystem
21-
},
28+
} & ThrowOnErrorOption,
2229
): Promise<LoadResult> {
30+
const errors: ErrorObject[] = []
31+
2332
// Don’t load a reference twice, check the filesystem before fetching something
2433
if (
2534
options?.filesystem &&
2635
options?.filesystem.find((entry) => entry.filename === value)
2736
) {
2837
return {
2938
filesystem: options.filesystem,
39+
errors,
3040
}
3141
}
3242

3343
// Check whether the value is an URL or file path
3444
const plugin = options?.plugins?.find((plugin) => plugin.check(value))
35-
const content = normalize(plugin ? await plugin.get(value) : value)
45+
46+
let content: AnyObject
47+
48+
if (plugin) {
49+
try {
50+
content = normalize(await plugin.get(value))
51+
} catch (error) {
52+
if (options?.throwOnError) {
53+
throw new Error(
54+
ERRORS.EXTERNAL_REFERENCE_NOT_FOUND.replace('%s', value),
55+
)
56+
}
57+
58+
errors.push({
59+
code: 'EXTERNAL_REFERENCE_NOT_FOUND',
60+
message: ERRORS.EXTERNAL_REFERENCE_NOT_FOUND.replace('%s', value),
61+
})
62+
63+
return {
64+
filesystem: [],
65+
errors,
66+
}
67+
}
68+
} else {
69+
content = normalize(value)
70+
}
3671

3772
// No content
3873
if (content === undefined) {
74+
if (options?.throwOnError) {
75+
throw new Error('No content to load')
76+
}
77+
78+
errors.push({
79+
code: 'NO_CONTENT',
80+
message: ERRORS.NO_CONTENT,
81+
})
82+
3983
return {
4084
filesystem: [],
85+
errors,
4186
}
4287
}
4388

@@ -56,6 +101,7 @@ export async function load(
56101
if (listOfReferences.length === 0) {
57102
return {
58103
filesystem,
104+
errors,
59105
}
60106
}
61107

@@ -79,12 +125,17 @@ export async function load(
79125
continue
80126
}
81127

82-
const { filesystem: referencedFiles } = await load(target, {
83-
...options,
84-
// Make the filename the exact same value as the $ref
85-
// TODO: This leads to problems, if there are multiple references with the same file name but in different folders
86-
filename: reference,
87-
})
128+
const { filesystem: referencedFiles, errors: newErrors } = await load(
129+
target,
130+
{
131+
...options,
132+
// Make the filename the exact same value as the $ref
133+
// TODO: This leads to problems, if there are multiple references with the same file name but in different folders
134+
filename: reference,
135+
},
136+
)
137+
138+
errors.push(...newErrors)
88139

89140
filesystem = [
90141
...filesystem,
@@ -99,5 +150,6 @@ export async function load(
99150

100151
return {
101152
filesystem,
153+
errors,
102154
}
103155
}

packages/openapi-parser/src/utils/workThroughQueue.test.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('workThroughQueue', () => {
2525
})
2626

2727
expect(await result).toStrictEqual({
28+
errors: [],
2829
filesystem: [
2930
{
3031
dir: './',
@@ -65,9 +66,9 @@ describe('workThroughQueue', () => {
6566
})
6667

6768
expect(await result).toStrictEqual({
69+
errors: [],
6870
valid: true,
6971
version: '3.1',
70-
errors: [],
7172
filesystem: [
7273
{
7374
dir: './',
@@ -124,10 +125,10 @@ describe('workThroughQueue', () => {
124125
})
125126

126127
expect(await result).toStrictEqual({
128+
errors: [],
127129
specificationType: 'openapi',
128130
specificationVersion: '3.1.0',
129131
version: '3.1',
130-
errors: [],
131132
filesystem: [
132133
{
133134
dir: './',
@@ -184,6 +185,7 @@ describe('workThroughQueue', () => {
184185
})
185186

186187
expect(await result).toStrictEqual({
188+
errors: [],
187189
version: '3.1',
188190
specification: {
189191
openapi: '3.1.0',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "Hello World",
5+
"version": "1.0.0"
6+
},
7+
"paths": {
8+
"/foobar": {
9+
"post": {
10+
"requestBody": {
11+
"$ref": "#/components/requestBodies/Foobar"
12+
}
13+
}
14+
}
15+
},
16+
"components": {
17+
"requestBodies": {
18+
"Foobar": {
19+
"content": {}
20+
}
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import OriginalSwaggerParser from '@apidevtools/swagger-parser'
2+
import path from 'node:path'
3+
import { describe, expect, it, vi } from 'vitest'
4+
5+
import { dereference } from '../src/utils/dereference'
6+
import { load } from '../src/utils/load'
7+
import { fetchUrls } from '../src/utils/load/plugins/fetchUrls'
8+
import { readFiles } from '../src/utils/load/plugins/readFiles'
9+
import { validate } from '../src/utils/validate'
10+
11+
const myAPI = JSON.stringify({
12+
openapi: '3.1.0',
13+
info: {
14+
title: 'Hello World',
15+
version: '1.0.0',
16+
},
17+
paths: {
18+
'/foobar': {
19+
post: {
20+
requestBody: {
21+
$ref: '#/components/requestBodies/Foobar',
22+
},
23+
},
24+
},
25+
},
26+
components: {
27+
requestBodies: {
28+
Foobar: {
29+
content: {},
30+
},
31+
},
32+
},
33+
})
34+
35+
class SwaggerParser {
36+
static async validate(api: string, callback: (err: any, api: any) => void) {
37+
try {
38+
const { filesystem } = await load(api, {
39+
plugins: [fetchUrls(), readFiles()],
40+
throwOnError: true,
41+
})
42+
43+
validate(filesystem, {
44+
throwOnError: true,
45+
}).then((result) => {
46+
callback(null, result.schema)
47+
})
48+
} catch (error) {
49+
callback(error, null)
50+
}
51+
}
52+
53+
static async dereference(api: string) {
54+
const { filesystem } = await load(api, {
55+
plugins: [fetchUrls(), readFiles()],
56+
throwOnError: true,
57+
})
58+
59+
return dereference(filesystem).then((result) => result.schema)
60+
}
61+
}
62+
63+
// https://github.com/APIDevTools/swagger-parser?tab=readme-ov-file#example
64+
describe('validate', async () => {
65+
it('validates', async () => {
66+
return new Promise((resolve, reject) => {
67+
SwaggerParser.validate(myAPI, (err, api) => {
68+
if (err) {
69+
reject(err)
70+
} else {
71+
expect(api.info.title).toBe('Hello World')
72+
expect(api.info.version).toBe('1.0.0')
73+
74+
resolve(null)
75+
}
76+
})
77+
})
78+
})
79+
80+
it('throws an error for invalid documents', async () => {
81+
return new Promise((resolve, reject) => {
82+
SwaggerParser.validate('invalid', (err) => {
83+
if (err) {
84+
resolve(null)
85+
} else {
86+
reject()
87+
}
88+
})
89+
})
90+
})
91+
})
92+
93+
// https://apitools.dev/swagger-parser/docs/swagger-parser.html#dereferenceapi-options-callback
94+
describe('dereference', () => {
95+
it('dereferences', async () => {
96+
let api = await SwaggerParser.dereference(myAPI)
97+
98+
// The `api` object is a normal JavaScript object,
99+
// so you can easily access any part of the API using simple dot notation
100+
expect(api?.paths?.['/foobar']?.post?.requestBody?.content).toEqual({})
101+
})
102+
103+
it('dereferences URLs', async () => {
104+
global.fetch = async (url: string) =>
105+
({
106+
text: async () => {
107+
if (url === 'http://example.com/specification/openapi.yaml') {
108+
return myAPI
109+
}
110+
111+
throw new Error('Not found')
112+
},
113+
}) as Response
114+
115+
let api = await SwaggerParser.dereference(
116+
'http://example.com/specification/openapi.yaml',
117+
)
118+
119+
// The `api` object is a normal JavaScript object,
120+
// so you can easily access any part of the API using simple dot notation
121+
expect(api?.paths?.['/foobar']?.post?.requestBody?.content).toEqual({})
122+
})
123+
124+
it('dereferences files', async () => {
125+
const EXAMPLE_FILE = path.join(
126+
new URL(import.meta.url).pathname,
127+
'../../tests/migration-layer.json',
128+
)
129+
130+
let api = await SwaggerParser.dereference(EXAMPLE_FILE)
131+
132+
// The `api` object is a normal JavaScript object,
133+
// so you can easily access any part of the API using simple dot notation
134+
expect(api?.paths?.['/foobar']?.post?.requestBody?.content).toEqual({})
135+
})
136+
})

0 commit comments

Comments
 (0)