@redocly/openapi-core 1.0.0-beta.65 → 1.0.0-beta.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/__snapshots__/bundle.test.ts.snap +126 -0
- package/__tests__/bundle.test.ts +53 -1
- package/__tests__/fixtures/refs/definitions.yaml +3 -0
- package/__tests__/fixtures/refs/external-request-body.yaml +13 -0
- package/__tests__/fixtures/refs/externalref.yaml +35 -0
- package/__tests__/fixtures/refs/hosted.yaml +35 -0
- package/__tests__/fixtures/refs/rename.yaml +1 -0
- package/__tests__/fixtures/refs/requestBody.yaml +9 -0
- package/__tests__/fixtures/refs/simple.yaml +1 -0
- package/__tests__/fixtures/refs/vendor.schema.yaml +20 -0
- package/lib/bundle.d.ts +4 -0
- package/lib/bundle.js +25 -7
- package/lib/config/all.js +9 -1
- package/lib/config/config.js +1 -1
- package/lib/config/minimal.js +1 -0
- package/lib/config/recommended.js +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -1
- package/lib/lint.js +2 -0
- package/lib/redocly/index.d.ts +3 -14
- package/lib/redocly/index.js +19 -186
- package/lib/redocly/registry-api-types.d.ts +28 -0
- package/lib/redocly/registry-api-types.js +2 -0
- package/lib/redocly/registry-api.d.ts +11 -0
- package/lib/redocly/registry-api.js +94 -0
- package/lib/ref-utils.js +1 -2
- package/lib/rules/common/info-license-url.js +1 -0
- package/lib/rules/common/no-http-verbs-in-paths.d.ts +2 -0
- package/lib/rules/common/no-http-verbs-in-paths.js +33 -0
- package/lib/rules/common/operation-4xx-response.d.ts +2 -0
- package/lib/rules/common/operation-4xx-response.js +17 -0
- package/lib/rules/common/path-excludes-patterns.d.ts +2 -0
- package/lib/rules/common/path-excludes-patterns.js +22 -0
- package/lib/rules/common/path-segment-plural.d.ts +2 -0
- package/lib/rules/common/path-segment-plural.js +32 -0
- package/lib/rules/common/registry-dependencies.js +4 -7
- package/lib/rules/oas2/index.d.ts +6 -0
- package/lib/rules/oas2/index.js +12 -0
- package/lib/rules/oas2/request-mime-type.d.ts +2 -0
- package/lib/rules/oas2/request-mime-type.js +17 -0
- package/lib/rules/oas2/response-mime-type.d.ts +2 -0
- package/lib/rules/oas2/response-mime-type.js +17 -0
- package/lib/rules/oas3/index.js +12 -0
- package/lib/rules/oas3/no-server-trailing-slash.js +1 -1
- package/lib/rules/oas3/request-mime-type.d.ts +2 -0
- package/lib/rules/oas3/request-mime-type.js +31 -0
- package/lib/rules/oas3/response-mime-type.d.ts +2 -0
- package/lib/rules/oas3/response-mime-type.js +31 -0
- package/lib/types/oas3_1.js +6 -0
- package/lib/utils.d.ts +10 -0
- package/lib/utils.js +65 -1
- package/lib/walk.d.ts +2 -0
- package/lib/walk.js +7 -0
- package/package.json +5 -3
- package/src/__tests__/utils.test.ts +19 -1
- package/src/bundle.ts +51 -9
- package/src/config/all.ts +9 -1
- package/src/config/config.ts +2 -2
- package/src/config/minimal.ts +1 -0
- package/src/config/recommended.ts +1 -0
- package/src/index.ts +1 -1
- package/src/lint.ts +2 -0
- package/src/redocly/index.ts +17 -194
- package/src/redocly/registry-api-types.ts +31 -0
- package/src/redocly/registry-api.ts +106 -0
- package/src/ref-utils.ts +1 -3
- package/src/rules/common/__tests__/info-license.test.ts +1 -1
- package/src/rules/common/__tests__/operation-4xx-response.test.ts +108 -0
- package/src/rules/common/info-license-url.ts +1 -0
- package/src/rules/common/no-http-verbs-in-paths.ts +36 -0
- package/src/rules/common/operation-4xx-response.ts +17 -0
- package/src/rules/common/path-excludes-patterns.ts +23 -0
- package/src/rules/common/path-segment-plural.ts +31 -0
- package/src/rules/common/registry-dependencies.ts +6 -8
- package/src/rules/oas2/index.ts +12 -0
- package/src/rules/oas2/request-mime-type.ts +17 -0
- package/src/rules/oas2/response-mime-type.ts +17 -0
- package/src/rules/oas3/__tests__/no-server-trailing-slash.test.ts +19 -0
- package/src/rules/oas3/index.ts +12 -0
- package/src/rules/oas3/no-server-trailing-slash.ts +1 -1
- package/src/rules/oas3/request-mime-type.ts +31 -0
- package/src/rules/oas3/response-mime-type.ts +31 -0
- package/src/rules/utils.ts +1 -1
- package/src/types/oas3_1.ts +7 -0
- package/src/utils.ts +75 -0
- package/src/walk.ts +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/lib/redocly/query.d.ts +0 -4
- package/lib/redocly/query.js +0 -44
- package/src/redocly/query.ts +0 -38
package/src/redocly/index.ts
CHANGED
|
@@ -2,15 +2,17 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { yellow, red, green, gray } from 'colorette';
|
|
5
|
-
import {
|
|
5
|
+
import { RegistryApi } from './registry-api';
|
|
6
6
|
|
|
7
7
|
const TOKEN_FILENAME = '.redocly-config.json';
|
|
8
8
|
|
|
9
9
|
export class RedoclyClient {
|
|
10
10
|
private accessToken: string | undefined;
|
|
11
|
+
registryApi: RegistryApi;
|
|
11
12
|
|
|
12
13
|
constructor() {
|
|
13
14
|
this.loadToken();
|
|
15
|
+
this.registryApi = new RegistryApi(this.accessToken);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
hasToken(): boolean {
|
|
@@ -36,9 +38,8 @@ export class RedoclyClient {
|
|
|
36
38
|
|
|
37
39
|
async verifyToken(accessToken: string, verbose: boolean = false): Promise<boolean> {
|
|
38
40
|
if (!accessToken) return false;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return true;
|
|
41
|
+
|
|
42
|
+
return this.registryApi.setAccessToken(accessToken).authStatus(verbose);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
async getAuthorizationHeader(): Promise<string | undefined> {
|
|
@@ -83,198 +84,20 @@ export class RedoclyClient {
|
|
|
83
84
|
}
|
|
84
85
|
process.stdout.write('Logged out from the Redocly account. ✋\n');
|
|
85
86
|
}
|
|
87
|
+
}
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static async authorize(accessToken: string, options: { queryName?: string; verbose?: boolean }) {
|
|
95
|
-
const { queryName = '', verbose = false } = options;
|
|
96
|
-
try {
|
|
97
|
-
const queryStr = `query ${queryName}{ viewer { id } }`;
|
|
98
|
-
|
|
99
|
-
return await query(queryStr, {}, { Authorization: accessToken });
|
|
100
|
-
} catch (e) {
|
|
101
|
-
if (verbose) console.log(e);
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async updateDependencies(dependencies: string[] | undefined): Promise<void> {
|
|
107
|
-
const definitionId = process.env.DEFINITION;
|
|
108
|
-
const versionId = process.env.DEFINITION;
|
|
109
|
-
const branchId = process.env.BRANCH;
|
|
110
|
-
|
|
111
|
-
if (!definitionId || !versionId || !branchId) return;
|
|
112
|
-
|
|
113
|
-
await this.query(
|
|
114
|
-
`
|
|
115
|
-
mutation UpdateBranchDependenciesFromURLs(
|
|
116
|
-
$urls: [String!]!
|
|
117
|
-
$definitionId: Int!
|
|
118
|
-
$versionId: Int!
|
|
119
|
-
$branchId: Int!
|
|
120
|
-
) {
|
|
121
|
-
updateBranchDependenciesFromURLs(
|
|
122
|
-
definitionId: $definitionId
|
|
123
|
-
versionId: $versionId
|
|
124
|
-
branchId: $branchId
|
|
125
|
-
urls: $urls
|
|
126
|
-
) {
|
|
127
|
-
branchName
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
`,
|
|
131
|
-
{
|
|
132
|
-
urls: dependencies || [],
|
|
133
|
-
definitionId: parseInt(definitionId, 10),
|
|
134
|
-
versionId: parseInt(versionId, 10),
|
|
135
|
-
branchId: parseInt(branchId, 10),
|
|
136
|
-
},
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
updateDefinitionVersion(definitionId: number, versionId: number, updatePatch: object): Promise<void> {
|
|
141
|
-
return this.query(`
|
|
142
|
-
mutation UpdateDefinitionVersion($definitionId: Int!, $versionId: Int!, $updatePatch: DefinitionVersionPatch!) {
|
|
143
|
-
updateDefinitionVersionByDefinitionIdAndId(input: {definitionId: $definitionId, id: $versionId, patch: $updatePatch}) {
|
|
144
|
-
definitionVersion {
|
|
145
|
-
...VersionDetails
|
|
146
|
-
__typename
|
|
147
|
-
}
|
|
148
|
-
__typename
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
fragment VersionDetails on DefinitionVersion {
|
|
153
|
-
id
|
|
154
|
-
nodeId
|
|
155
|
-
uuid
|
|
156
|
-
definitionId
|
|
157
|
-
name
|
|
158
|
-
description
|
|
159
|
-
sourceType
|
|
160
|
-
source
|
|
161
|
-
registryAccess
|
|
162
|
-
__typename
|
|
163
|
-
}
|
|
164
|
-
`,
|
|
165
|
-
{
|
|
166
|
-
definitionId,
|
|
167
|
-
versionId,
|
|
168
|
-
updatePatch,
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
getOrganizationId(organizationId: string) {
|
|
174
|
-
return this.query(`
|
|
175
|
-
query ($organizationId: String!) {
|
|
176
|
-
organizationById(id: $organizationId) {
|
|
177
|
-
id
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
`, {
|
|
181
|
-
organizationId
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
getDefinitionByName(name: string, organizationId: string) {
|
|
186
|
-
return this.query(`
|
|
187
|
-
query ($name: String!, $organizationId: String!) {
|
|
188
|
-
definition: definitionByOrganizationIdAndName(name: $name, organizationId: $organizationId) {
|
|
189
|
-
id
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
`, {
|
|
193
|
-
name,
|
|
194
|
-
organizationId
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
createDefinition(organizationId: string, name: string) {
|
|
199
|
-
return this.query(`
|
|
200
|
-
mutation CreateDefinition($organizationId: String!, $name: String!) {
|
|
201
|
-
def: createDefinition(input: {organizationId: $organizationId, name: $name }) {
|
|
202
|
-
definition {
|
|
203
|
-
id
|
|
204
|
-
nodeId
|
|
205
|
-
name
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
`, {
|
|
210
|
-
organizationId,
|
|
211
|
-
name
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
createDefinitionVersion(definitionId: string, name: string, sourceType: string, source: any) {
|
|
216
|
-
return this.query(`
|
|
217
|
-
mutation CreateVersion($definitionId: Int!, $name: String!, $sourceType: DvSourceType!, $source: JSON) {
|
|
218
|
-
createDefinitionVersion(input: {definitionId: $definitionId, name: $name, sourceType: $sourceType, source: $source }) {
|
|
219
|
-
definitionVersion {
|
|
220
|
-
id
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
`, {
|
|
225
|
-
definitionId,
|
|
226
|
-
name,
|
|
227
|
-
sourceType,
|
|
228
|
-
source
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
getSignedUrl(organizationId: string, filesHash: string, fileName: string) {
|
|
233
|
-
return this.query(`
|
|
234
|
-
query ($organizationId: String!, $filesHash: String!, $fileName: String!) {
|
|
235
|
-
signFileUploadCLI(organizationId: $organizationId, filesHash: $filesHash, fileName: $fileName) {
|
|
236
|
-
signedFileUrl
|
|
237
|
-
uploadedFilePath
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
`, {
|
|
241
|
-
organizationId,
|
|
242
|
-
filesHash,
|
|
243
|
-
fileName
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
getDefinitionVersion(organizationId: string, definitionName: string, versionName: string) {
|
|
248
|
-
return this.query(`
|
|
249
|
-
query ($organizationId: String!, $definitionName: String!, $versionName: String!) {
|
|
250
|
-
version: definitionVersionByOrganizationDefinitionAndName(organizationId: $organizationId, definitionName: $definitionName, versionName: $versionName) {
|
|
251
|
-
id
|
|
252
|
-
definitionId
|
|
253
|
-
defaultBranch {
|
|
254
|
-
name
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
`, {
|
|
259
|
-
organizationId,
|
|
260
|
-
definitionName,
|
|
261
|
-
versionName
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
static isRegistryURL(link: string): boolean {
|
|
266
|
-
const domain = process.env.REDOCLY_DOMAIN || 'redoc.ly';
|
|
267
|
-
if (!link.startsWith(`https://api.${domain}/registry/`)) return false;
|
|
268
|
-
const registryPath = link.replace(`https://api.${domain}/registry/`, '');
|
|
89
|
+
export function isRedoclyRegistryURL(link: string): boolean {
|
|
90
|
+
const domain = process.env.REDOCLY_DOMAIN || 'redoc.ly';
|
|
91
|
+
if (!link.startsWith(`https://api.${domain}/registry/`)) return false;
|
|
92
|
+
const registryPath = link.replace(`https://api.${domain}/registry/`, '');
|
|
269
93
|
|
|
270
|
-
|
|
94
|
+
const pathParts = registryPath.split('/');
|
|
271
95
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
96
|
+
// we can be sure, that there is job UUID present
|
|
97
|
+
// (org, definition, version, bundle, branch, job, "openapi.yaml" 🤦♂️)
|
|
98
|
+
// so skip this link.
|
|
99
|
+
// FIXME
|
|
100
|
+
if (pathParts.length === 7) return false;
|
|
277
101
|
|
|
278
|
-
|
|
279
|
-
}
|
|
102
|
+
return true;
|
|
280
103
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export namespace RegistryApiTypes {
|
|
2
|
+
interface VersionParams {
|
|
3
|
+
organizationId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PrepareFileuploadParams extends VersionParams {
|
|
9
|
+
filesHash: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
isUpsert?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PushApiParams extends VersionParams {
|
|
15
|
+
rootFilePath: string;
|
|
16
|
+
filePaths: string[];
|
|
17
|
+
branch?: string;
|
|
18
|
+
isUpsert?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PrepareFileuploadOKResponse {
|
|
22
|
+
filePath: string;
|
|
23
|
+
signedUploadUrl: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NotFoundProblemResponse {
|
|
27
|
+
status: 404;
|
|
28
|
+
title: 'Not Found';
|
|
29
|
+
code: 'ORGANIZATION_NOT_FOUND' | 'API_VERSION_NOT_FOUND';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fetch, { RequestInit } from 'node-fetch';
|
|
2
|
+
import { RegistryApiTypes } from './registry-api-types';
|
|
3
|
+
const version = require('../../package.json').version;
|
|
4
|
+
|
|
5
|
+
export class RegistryApi {
|
|
6
|
+
private readonly baseUrl = `https://api.${process.env.REDOCLY_DOMAIN || 'redoc.ly'}/registry`;
|
|
7
|
+
|
|
8
|
+
constructor(private accessToken?: string) {}
|
|
9
|
+
|
|
10
|
+
private async request(path = '', options: RequestInit = {}) {
|
|
11
|
+
if (!this.accessToken) {
|
|
12
|
+
throw new Error('Unauthorized');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const headers = Object.assign({}, options.headers || {}, {
|
|
16
|
+
authorization: this.accessToken,
|
|
17
|
+
'x-redocly-cli-version': version,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const response = await fetch(`${this.baseUrl}${path}`, Object.assign({}, options, { headers }));
|
|
21
|
+
|
|
22
|
+
if (response.status === 401) {
|
|
23
|
+
throw new Error('Unauthorized');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (response.status === 404) {
|
|
27
|
+
const body: RegistryApiTypes.NotFoundProblemResponse = await response.json();
|
|
28
|
+
throw new Error(body.code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setAccessToken(accessToken: string) {
|
|
35
|
+
this.accessToken = accessToken;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async authStatus(verbose = false) {
|
|
40
|
+
try {
|
|
41
|
+
const response = await this.request();
|
|
42
|
+
|
|
43
|
+
return response.ok;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (verbose) {
|
|
46
|
+
console.log(error);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async prepareFileUpload({
|
|
53
|
+
organizationId,
|
|
54
|
+
name,
|
|
55
|
+
version,
|
|
56
|
+
filesHash,
|
|
57
|
+
filename,
|
|
58
|
+
isUpsert,
|
|
59
|
+
}: RegistryApiTypes.PrepareFileuploadParams): Promise<RegistryApiTypes.PrepareFileuploadOKResponse> {
|
|
60
|
+
const response = await this.request(
|
|
61
|
+
`/${organizationId}/${name}/${version}/prepare-file-upload`,
|
|
62
|
+
{
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'content-type': 'application/json' },
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
filesHash,
|
|
67
|
+
filename,
|
|
68
|
+
isUpsert,
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error('Could not prepare file upload');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async pushApi({
|
|
81
|
+
organizationId,
|
|
82
|
+
name,
|
|
83
|
+
version,
|
|
84
|
+
rootFilePath,
|
|
85
|
+
filePaths,
|
|
86
|
+
branch,
|
|
87
|
+
isUpsert,
|
|
88
|
+
}: RegistryApiTypes.PushApiParams) {
|
|
89
|
+
const response = await this.request(`/${organizationId}/${name}/${version}`, {
|
|
90
|
+
method: 'PUT',
|
|
91
|
+
headers: { 'content-type': 'application/json' },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
rootFilePath,
|
|
94
|
+
filePaths,
|
|
95
|
+
branch,
|
|
96
|
+
isUpsert,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error('Could not push api');
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/ref-utils.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { sep as platformDependentSeparator } from 'path';
|
|
2
|
-
|
|
3
1
|
import { Source } from './resolve';
|
|
4
2
|
import { OasRef } from './typings/openapi';
|
|
5
3
|
|
|
@@ -61,7 +59,7 @@ export function pointerBaseName(pointer: string) {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
export function refBaseName(ref: string) {
|
|
64
|
-
const parts = ref.split(
|
|
62
|
+
const parts = ref.split(/[\/\\]/); // split by '\' and '/'
|
|
65
63
|
return parts[parts.length - 1].split('.')[0];
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { outdent } from 'outdent';
|
|
2
|
+
import { lintDocument } from '../../../lint';
|
|
3
|
+
import { parseYamlToDocument, replaceSourceWithRef } from '../../../../__tests__/utils';
|
|
4
|
+
import { makeConfig } from '../../__tests__/config';
|
|
5
|
+
import { BaseResolver } from '../../../resolve';
|
|
6
|
+
|
|
7
|
+
describe('Oas3 operation-4xx-response', () => {
|
|
8
|
+
it('should report missing 4xx response', async () => {
|
|
9
|
+
const document = parseYamlToDocument(
|
|
10
|
+
outdent`
|
|
11
|
+
openapi: 3.0.0
|
|
12
|
+
paths:
|
|
13
|
+
'/test':
|
|
14
|
+
put:
|
|
15
|
+
responses:
|
|
16
|
+
200:
|
|
17
|
+
description: ok response
|
|
18
|
+
`,
|
|
19
|
+
'foobar.yaml',
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const results = await lintDocument({
|
|
23
|
+
externalRefResolver: new BaseResolver(),
|
|
24
|
+
document,
|
|
25
|
+
config: makeConfig({ 'operation-4xx-response': 'error' }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
29
|
+
Array [
|
|
30
|
+
Object {
|
|
31
|
+
"location": Array [
|
|
32
|
+
Object {
|
|
33
|
+
"pointer": "#/paths/~1test/put/responses",
|
|
34
|
+
"reportOnKey": true,
|
|
35
|
+
"source": "foobar.yaml",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
"message": "Operation must have at least one \`4xx\` response.",
|
|
39
|
+
"ruleId": "operation-4xx-response",
|
|
40
|
+
"severity": "error",
|
|
41
|
+
"suggest": Array [],
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should not report for present 4xx response', async () => {
|
|
48
|
+
const document = parseYamlToDocument(
|
|
49
|
+
outdent`
|
|
50
|
+
openapi: 3.0.0
|
|
51
|
+
paths:
|
|
52
|
+
'/test/':
|
|
53
|
+
put:
|
|
54
|
+
responses:
|
|
55
|
+
400:
|
|
56
|
+
description: error response
|
|
57
|
+
`,
|
|
58
|
+
'foobar.yaml',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const results = await lintDocument({
|
|
62
|
+
externalRefResolver: new BaseResolver(),
|
|
63
|
+
document,
|
|
64
|
+
config: makeConfig({ 'operation-4xx-response': 'error' }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should report if default is present but missing 4xx response', async () => {
|
|
71
|
+
const document = parseYamlToDocument(
|
|
72
|
+
outdent`
|
|
73
|
+
openapi: 3.0.0
|
|
74
|
+
paths:
|
|
75
|
+
'/test/':
|
|
76
|
+
put:
|
|
77
|
+
responses:
|
|
78
|
+
default:
|
|
79
|
+
description: some default response
|
|
80
|
+
`,
|
|
81
|
+
'foobar.yaml',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const results = await lintDocument({
|
|
85
|
+
externalRefResolver: new BaseResolver(),
|
|
86
|
+
document,
|
|
87
|
+
config: makeConfig({ 'operation-4xx-response': 'error' }),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
91
|
+
Array [
|
|
92
|
+
Object {
|
|
93
|
+
"location": Array [
|
|
94
|
+
Object {
|
|
95
|
+
"pointer": "#/paths/~1test~1/put/responses",
|
|
96
|
+
"reportOnKey": true,
|
|
97
|
+
"source": "foobar.yaml",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
"message": "Operation must have at least one \`4xx\` response.",
|
|
101
|
+
"ruleId": "operation-4xx-response",
|
|
102
|
+
"severity": "error",
|
|
103
|
+
"suggest": Array [],
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
`);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
+
import { Oas2PathItem } from '../../typings/swagger';
|
|
3
|
+
import { Oas3PathItem } from '../../typings/openapi';
|
|
4
|
+
import { UserContext } from '../../walk';
|
|
5
|
+
import { isPathParameter, splitCamelCaseIntoWords } from '../../utils';
|
|
6
|
+
|
|
7
|
+
const httpMethods = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace'];
|
|
8
|
+
|
|
9
|
+
export const NoHttpVerbsInPaths: Oas3Rule | Oas2Rule = ({ splitIntoWords }) => {
|
|
10
|
+
return {
|
|
11
|
+
PathItem(_path: Oas2PathItem | Oas3PathItem, { key, report, location }: UserContext) {
|
|
12
|
+
const pathKey = key.toString();
|
|
13
|
+
if (!pathKey.startsWith('/')) return;
|
|
14
|
+
const pathSegments = pathKey.split('/');
|
|
15
|
+
|
|
16
|
+
for (const pathSegment of pathSegments) {
|
|
17
|
+
if (!pathSegment || isPathParameter(pathSegment)) continue;
|
|
18
|
+
|
|
19
|
+
const isHttpMethodIncluded = (method: string) => {
|
|
20
|
+
return splitIntoWords
|
|
21
|
+
? splitCamelCaseIntoWords(pathSegment).has(method)
|
|
22
|
+
: pathSegment.toLocaleLowerCase().includes(method);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const method of httpMethods) {
|
|
26
|
+
if (isHttpMethodIncluded(method)) {
|
|
27
|
+
report({
|
|
28
|
+
message: `path \`${pathKey}\` should not contain http verb ${method}`,
|
|
29
|
+
location: location.key(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
|
|
4
|
+
export const Operation4xxResponse: Oas3Rule | Oas2Rule = () => {
|
|
5
|
+
return {
|
|
6
|
+
ResponsesMap(responses: Record<string, object>, { report }: UserContext) {
|
|
7
|
+
const codes = Object.keys(responses);
|
|
8
|
+
|
|
9
|
+
if (!codes.some((code) => /4[Xx0-9]{2}/.test(code))) {
|
|
10
|
+
report({
|
|
11
|
+
message: 'Operation must have at least one `4xx` response.',
|
|
12
|
+
location: { reportOnKey: true },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Oas2Rule, Oas3Rule } from '../../visitors';
|
|
2
|
+
import { Oas2PathItem } from '../../typings/swagger';
|
|
3
|
+
import { Oas3PathItem } from '../../typings/openapi';
|
|
4
|
+
import { UserContext } from '../../walk';
|
|
5
|
+
|
|
6
|
+
export const PathExcludesPatterns: Oas3Rule | Oas2Rule = ({ patterns }) => {
|
|
7
|
+
return {
|
|
8
|
+
PathItem(_path: Oas2PathItem | Oas3PathItem, { report, key, location }: UserContext) {
|
|
9
|
+
if (!patterns)
|
|
10
|
+
throw new Error(`Parameter "patterns" is not provided for "path-excludes-patterns" rule`);
|
|
11
|
+
const pathKey = key.toString();
|
|
12
|
+
if (pathKey.startsWith('/')) {
|
|
13
|
+
const matches = patterns.filter((pattern: string) => pathKey.match(pattern));
|
|
14
|
+
for (const match of matches) {
|
|
15
|
+
report({
|
|
16
|
+
message: `path \`${pathKey}\` should not match regex pattern: \`${match}\``,
|
|
17
|
+
location: location.key(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Oas3Rule, Oas2Rule } from '../../visitors';
|
|
2
|
+
import { UserContext } from '../../walk';
|
|
3
|
+
import { isPathParameter, isSingular } from '../../utils';
|
|
4
|
+
|
|
5
|
+
export const PathSegmentPlural: Oas3Rule | Oas2Rule = (opts) => {
|
|
6
|
+
const { ignoreLastPathSegment, exceptions } = opts;
|
|
7
|
+
return {
|
|
8
|
+
PathItem: {
|
|
9
|
+
leave(_path: any, { report, key, location }: UserContext) {
|
|
10
|
+
const pathKey = key.toString();
|
|
11
|
+
if (pathKey.startsWith('/')) {
|
|
12
|
+
const pathSegments = pathKey.split('/');
|
|
13
|
+
pathSegments.shift();
|
|
14
|
+
if (ignoreLastPathSegment && pathSegments.length > 1) {
|
|
15
|
+
pathSegments.pop();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const pathSegment of pathSegments) {
|
|
19
|
+
if (exceptions && exceptions.includes(pathSegment)) continue;
|
|
20
|
+
if (!isPathParameter(pathSegment) && isSingular(pathSegment)) {
|
|
21
|
+
report({
|
|
22
|
+
message: `path segment \`${pathSegment}\` should be plural.`,
|
|
23
|
+
location: location.key(),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UserContext } from '../../walk';
|
|
2
|
+
import { isRedoclyRegistryURL } from '../../redocly';
|
|
2
3
|
|
|
3
4
|
import { Oas3Decorator, Oas2Decorator } from '../../visitors';
|
|
4
5
|
|
|
5
6
|
export const RegistryDependencies: Oas3Decorator | Oas2Decorator = () => {
|
|
6
|
-
let redoclyClient: RedoclyClient;
|
|
7
7
|
let registryDependencies = new Set<string>();
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
10
|
DefinitionRoot: {
|
|
11
|
-
leave() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
redoclyClient.updateDependencies(Array.from(registryDependencies.keys()));
|
|
15
|
-
}
|
|
11
|
+
leave(_: any, ctx: UserContext) {
|
|
12
|
+
const data = ctx.getVisitorData();
|
|
13
|
+
data.links = Array.from(registryDependencies);
|
|
16
14
|
},
|
|
17
15
|
},
|
|
18
16
|
ref(node) {
|
|
19
17
|
if (node.$ref) {
|
|
20
18
|
const link = node.$ref.split('#/')[0];
|
|
21
|
-
if (
|
|
19
|
+
if (isRedoclyRegistryURL(link)) {
|
|
22
20
|
registryDependencies.add(link);
|
|
23
21
|
}
|
|
24
22
|
}
|