@redocly/openapi-core 1.0.0-beta.109 → 1.0.0-beta.110
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/README.md +2 -2
- package/lib/config/config-resolvers.js +21 -3
- package/lib/config/config.d.ts +1 -0
- package/lib/config/config.js +1 -0
- package/lib/config/load.d.ts +8 -2
- package/lib/config/load.js +4 -2
- package/lib/config/types.d.ts +10 -0
- package/lib/config/utils.js +2 -2
- package/lib/rules/ajv.d.ts +1 -1
- package/lib/rules/ajv.js +5 -5
- package/lib/rules/common/assertions/asserts.d.ts +3 -5
- package/lib/rules/common/assertions/asserts.js +137 -97
- package/lib/rules/common/assertions/index.js +2 -6
- package/lib/rules/common/assertions/utils.d.ts +12 -6
- package/lib/rules/common/assertions/utils.js +33 -20
- package/lib/rules/utils.js +1 -1
- package/lib/types/redocly-yaml.js +16 -1
- package/package.json +3 -5
- package/src/__tests__/lint.test.ts +88 -0
- package/src/config/__tests__/config-resolvers.test.ts +37 -1
- package/src/config/__tests__/config.test.ts +5 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml +16 -0
- package/src/config/__tests__/fixtures/resolve-config/plugin.js +11 -0
- package/src/config/__tests__/load.test.ts +1 -1
- package/src/config/__tests__/resolve-plugins.test.ts +3 -3
- package/src/config/config-resolvers.ts +28 -5
- package/src/config/config.ts +2 -0
- package/src/config/load.ts +10 -4
- package/src/config/types.ts +13 -0
- package/src/config/utils.ts +1 -0
- package/src/rules/ajv.ts +4 -4
- package/src/rules/common/assertions/__tests__/asserts.test.ts +491 -428
- package/src/rules/common/assertions/asserts.ts +155 -97
- package/src/rules/common/assertions/index.ts +2 -11
- package/src/rules/common/assertions/utils.ts +66 -36
- package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +51 -2
- package/src/rules/utils.ts +2 -1
- package/src/types/redocly-yaml.ts +16 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.regexFromString = exports.isOrdered = exports.getIntersectionLength = exports.buildSubjectVisitor = exports.buildVisitorObject = void 0;
|
|
4
|
+
const logger_1 = require("../../../logger");
|
|
4
5
|
const ref_utils_1 = require("../../../ref-utils");
|
|
5
6
|
const asserts_1 = require("./asserts");
|
|
6
7
|
function buildVisitorObject(subject, context, subjectVisitor) {
|
|
@@ -44,14 +45,15 @@ function buildVisitorObject(subject, context, subjectVisitor) {
|
|
|
44
45
|
return visitor;
|
|
45
46
|
}
|
|
46
47
|
exports.buildVisitorObject = buildVisitorObject;
|
|
47
|
-
function buildSubjectVisitor(
|
|
48
|
+
function buildSubjectVisitor(assertId, assertion, asserts) {
|
|
48
49
|
return (node, { report, location, rawLocation, key, type, resolve, rawNode }) => {
|
|
49
50
|
var _a;
|
|
51
|
+
let properties = assertion.property;
|
|
50
52
|
// We need to check context's last node if it has the same type as subject node;
|
|
51
53
|
// if yes - that means we didn't create context's last node visitor,
|
|
52
54
|
// so we need to handle 'matchParentKeys' and 'excludeParentKeys' conditions here;
|
|
53
|
-
if (context) {
|
|
54
|
-
const lastContextNode = context[context.length - 1];
|
|
55
|
+
if (assertion.context) {
|
|
56
|
+
const lastContextNode = assertion.context[assertion.context.length - 1];
|
|
55
57
|
if (lastContextNode.type === type.name) {
|
|
56
58
|
const matchParentKeys = lastContextNode.matchParentKeys;
|
|
57
59
|
const excludeParentKeys = lastContextNode.excludeParentKeys;
|
|
@@ -66,35 +68,55 @@ function buildSubjectVisitor(properties, asserts, context) {
|
|
|
66
68
|
if (properties) {
|
|
67
69
|
properties = Array.isArray(properties) ? properties : [properties];
|
|
68
70
|
}
|
|
71
|
+
const defaultMessage = `${logger_1.colorize.blue(assertId)} failed because the ${logger_1.colorize.blue(assertion.subject)}${logger_1.colorize.blue(properties ? ` ${properties.join(', ')}` : '')} didn't meet the assertions: {{problems}}`;
|
|
72
|
+
const assertResults = [];
|
|
69
73
|
for (const assert of asserts) {
|
|
70
74
|
const currentLocation = assert.name === 'ref' ? rawLocation : location;
|
|
71
75
|
if (properties) {
|
|
72
76
|
for (const property of properties) {
|
|
73
77
|
// we can have resolvable scalar so need to resolve value here.
|
|
74
78
|
const value = ref_utils_1.isRef(node[property]) ? (_a = resolve(node[property])) === null || _a === void 0 ? void 0 : _a.node : node[property];
|
|
75
|
-
runAssertion({
|
|
79
|
+
assertResults.push(runAssertion({
|
|
76
80
|
values: value,
|
|
77
81
|
rawValues: rawNode[property],
|
|
78
82
|
assert,
|
|
79
83
|
location: currentLocation.child(property),
|
|
80
|
-
|
|
81
|
-
});
|
|
84
|
+
}));
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
else {
|
|
85
88
|
const value = assert.name === 'ref' ? rawNode : Object.keys(node);
|
|
86
|
-
runAssertion({
|
|
89
|
+
assertResults.push(runAssertion({
|
|
87
90
|
values: Object.keys(node),
|
|
88
91
|
rawValues: value,
|
|
89
92
|
assert,
|
|
90
93
|
location: currentLocation,
|
|
91
|
-
|
|
92
|
-
});
|
|
94
|
+
}));
|
|
93
95
|
}
|
|
94
96
|
}
|
|
97
|
+
const problems = assertResults.flat();
|
|
98
|
+
if (problems.length) {
|
|
99
|
+
const message = assertion.message || defaultMessage;
|
|
100
|
+
report({
|
|
101
|
+
message: message.replace('{{problems}}', getProblemsMessage(problems)),
|
|
102
|
+
location: getProblemsLocation(problems) || location,
|
|
103
|
+
forceSeverity: assertion.severity || 'error',
|
|
104
|
+
suggest: assertion.suggest || [],
|
|
105
|
+
ruleId: assertId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
95
108
|
};
|
|
96
109
|
}
|
|
97
110
|
exports.buildSubjectVisitor = buildSubjectVisitor;
|
|
111
|
+
function getProblemsLocation(problems) {
|
|
112
|
+
return problems.length ? problems[0].location : undefined;
|
|
113
|
+
}
|
|
114
|
+
function getProblemsMessage(problems) {
|
|
115
|
+
var _a;
|
|
116
|
+
return problems.length === 1
|
|
117
|
+
? (_a = problems[0].message) !== null && _a !== void 0 ? _a : ''
|
|
118
|
+
: problems.map((problem) => { var _a; return `\n- ${(_a = problem.message) !== null && _a !== void 0 ? _a : ''}`; }).join('');
|
|
119
|
+
}
|
|
98
120
|
function getIntersectionLength(keys, properties) {
|
|
99
121
|
const props = new Set(properties);
|
|
100
122
|
let count = 0;
|
|
@@ -127,17 +149,8 @@ function isOrdered(value, options) {
|
|
|
127
149
|
return true;
|
|
128
150
|
}
|
|
129
151
|
exports.isOrdered = isOrdered;
|
|
130
|
-
function runAssertion({ values, rawValues, assert, location
|
|
131
|
-
|
|
132
|
-
if (!lintResult.isValid) {
|
|
133
|
-
report({
|
|
134
|
-
message: assert.message || `The ${assert.assertId} doesn't meet required conditions`,
|
|
135
|
-
location: lintResult.location || location,
|
|
136
|
-
forceSeverity: assert.severity,
|
|
137
|
-
suggest: assert.suggest,
|
|
138
|
-
ruleId: assert.assertId,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
152
|
+
function runAssertion({ values, rawValues, assert, location }) {
|
|
153
|
+
return asserts_1.asserts[assert.name](values, assert.conditions, location, rawValues);
|
|
141
154
|
}
|
|
142
155
|
function regexFromString(input) {
|
|
143
156
|
const matches = input.match(/^\/(.*)\/(.*)|(.*)/);
|
package/lib/rules/utils.js
CHANGED
|
@@ -93,7 +93,7 @@ function validateExample(example, schema, dataLoc, { resolve, location, report }
|
|
|
93
93
|
for (const error of errors) {
|
|
94
94
|
report({
|
|
95
95
|
message: `Example value must conform to the schema: ${error.message}.`,
|
|
96
|
-
location: Object.assign(Object.assign({}, new ref_utils_1.Location(dataLoc.source, error.instancePath)), { reportOnKey: error.keyword === 'additionalProperties' }),
|
|
96
|
+
location: Object.assign(Object.assign({}, new ref_utils_1.Location(dataLoc.source, error.instancePath)), { reportOnKey: error.keyword === 'unevaluatedProperties' || error.keyword === 'additionalProperties' }),
|
|
97
97
|
from: location,
|
|
98
98
|
suggest: error.suggest,
|
|
99
99
|
});
|
|
@@ -149,6 +149,11 @@ const ConfigRoot = {
|
|
|
149
149
|
http: 'ConfigHTTP',
|
|
150
150
|
doNotResolveExamples: { type: 'boolean' },
|
|
151
151
|
},
|
|
152
|
+
}, files: {
|
|
153
|
+
type: 'array',
|
|
154
|
+
items: {
|
|
155
|
+
type: 'string',
|
|
156
|
+
},
|
|
152
157
|
} }),
|
|
153
158
|
};
|
|
154
159
|
const ConfigApis = {
|
|
@@ -161,7 +166,12 @@ const ConfigApisProperties = {
|
|
|
161
166
|
items: {
|
|
162
167
|
type: 'string',
|
|
163
168
|
},
|
|
164
|
-
}, lint: 'ConfigStyleguide', styleguide: 'ConfigStyleguide' }, ConfigStyleguide.properties), { 'features.openapi': 'ConfigReferenceDocs', 'features.mockServer': 'ConfigMockServer'
|
|
169
|
+
}, lint: 'ConfigStyleguide', styleguide: 'ConfigStyleguide' }, ConfigStyleguide.properties), { 'features.openapi': 'ConfigReferenceDocs', 'features.mockServer': 'ConfigMockServer', files: {
|
|
170
|
+
type: 'array',
|
|
171
|
+
items: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
},
|
|
174
|
+
} }),
|
|
165
175
|
required: ['root'],
|
|
166
176
|
};
|
|
167
177
|
const ConfigHTTP = {
|
|
@@ -249,6 +259,11 @@ const Assert = {
|
|
|
249
259
|
maxLength: { type: 'integer' },
|
|
250
260
|
ref: (value) => typeof value === 'string' ? { type: 'string' } : { type: 'boolean' },
|
|
251
261
|
},
|
|
262
|
+
additionalProperties: (_value, key) => {
|
|
263
|
+
if (/^\w+\/\w+$/.test(key))
|
|
264
|
+
return { type: 'object' };
|
|
265
|
+
return;
|
|
266
|
+
},
|
|
252
267
|
required: ['subject'],
|
|
253
268
|
};
|
|
254
269
|
const Context = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/openapi-core",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.110",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"engines": {
|
|
@@ -29,12 +29,10 @@
|
|
|
29
29
|
"oas"
|
|
30
30
|
],
|
|
31
31
|
"contributors": [
|
|
32
|
-
"
|
|
33
|
-
"Roman Hotsiy <roman@redoc.ly> (https://redoc.ly/)",
|
|
34
|
-
"Andriy Leliv <andriy@redoc.ly> (https://redoc.ly/)"
|
|
32
|
+
"Roman Hotsiy <roman@redoc.ly> (https://redoc.ly/)"
|
|
35
33
|
],
|
|
36
34
|
"dependencies": {
|
|
37
|
-
"@redocly/ajv": "^8.
|
|
35
|
+
"@redocly/ajv": "^8.11.0",
|
|
38
36
|
"@types/node": "^14.11.8",
|
|
39
37
|
"colorette": "^1.2.0",
|
|
40
38
|
"js-levenshtein": "^1.1.6",
|
|
@@ -60,6 +60,22 @@ describe('lint', () => {
|
|
|
60
60
|
no-invalid-media-type-examples: error
|
|
61
61
|
path-http-verbs-order: error
|
|
62
62
|
boolean-parameter-prefixes: off
|
|
63
|
+
assert/operation-summary-length:
|
|
64
|
+
subject: Operation
|
|
65
|
+
property: summary
|
|
66
|
+
message: Operation summary should start with an active verb
|
|
67
|
+
local/checkWordsCount:
|
|
68
|
+
min: 3
|
|
69
|
+
features.openapi:
|
|
70
|
+
showConsole: true
|
|
71
|
+
layout:
|
|
72
|
+
scope: section
|
|
73
|
+
routingStrategy: browser
|
|
74
|
+
theme:
|
|
75
|
+
rightPanel:
|
|
76
|
+
backgroundColor: '#263238'
|
|
77
|
+
links:
|
|
78
|
+
color: '#6CC496'
|
|
63
79
|
features.openapi:
|
|
64
80
|
showConsole: true
|
|
65
81
|
layout:
|
|
@@ -77,6 +93,78 @@ describe('lint', () => {
|
|
|
77
93
|
|
|
78
94
|
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
|
|
79
95
|
Array [
|
|
96
|
+
Object {
|
|
97
|
+
"from": undefined,
|
|
98
|
+
"location": Array [
|
|
99
|
+
Object {
|
|
100
|
+
"pointer": "#/atures.openapi",
|
|
101
|
+
"reportOnKey": true,
|
|
102
|
+
"source": "",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
"message": "Property \`atures.openapi\` is not expected here.",
|
|
106
|
+
"ruleId": "configuration spec",
|
|
107
|
+
"severity": "error",
|
|
108
|
+
"suggest": Array [
|
|
109
|
+
"features.openapi",
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
Object {
|
|
113
|
+
"from": undefined,
|
|
114
|
+
"location": Array [
|
|
115
|
+
Object {
|
|
116
|
+
"pointer": "#/showConsole",
|
|
117
|
+
"reportOnKey": true,
|
|
118
|
+
"source": "",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
"message": "Property \`showConsole\` is not expected here.",
|
|
122
|
+
"ruleId": "configuration spec",
|
|
123
|
+
"severity": "error",
|
|
124
|
+
"suggest": Array [],
|
|
125
|
+
},
|
|
126
|
+
Object {
|
|
127
|
+
"from": undefined,
|
|
128
|
+
"location": Array [
|
|
129
|
+
Object {
|
|
130
|
+
"pointer": "#/layout",
|
|
131
|
+
"reportOnKey": true,
|
|
132
|
+
"source": "",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
"message": "Property \`layout\` is not expected here.",
|
|
136
|
+
"ruleId": "configuration spec",
|
|
137
|
+
"severity": "error",
|
|
138
|
+
"suggest": Array [],
|
|
139
|
+
},
|
|
140
|
+
Object {
|
|
141
|
+
"from": undefined,
|
|
142
|
+
"location": Array [
|
|
143
|
+
Object {
|
|
144
|
+
"pointer": "#/routingStrategy",
|
|
145
|
+
"reportOnKey": true,
|
|
146
|
+
"source": "",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
"message": "Property \`routingStrategy\` is not expected here.",
|
|
150
|
+
"ruleId": "configuration spec",
|
|
151
|
+
"severity": "error",
|
|
152
|
+
"suggest": Array [],
|
|
153
|
+
},
|
|
154
|
+
Object {
|
|
155
|
+
"from": undefined,
|
|
156
|
+
"location": Array [
|
|
157
|
+
Object {
|
|
158
|
+
"pointer": "#/theme",
|
|
159
|
+
"reportOnKey": true,
|
|
160
|
+
"source": "",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
"message": "Property \`theme\` is not expected here.",
|
|
164
|
+
"ruleId": "configuration spec",
|
|
165
|
+
"severity": "error",
|
|
166
|
+
"suggest": Array [],
|
|
167
|
+
},
|
|
80
168
|
Object {
|
|
81
169
|
"from": undefined,
|
|
82
170
|
"location": Array [
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { colorize } from '../../logger';
|
|
2
|
+
import { asserts } from '../../rules/common/assertions/asserts';
|
|
1
3
|
import { resolveStyleguideConfig, resolveApis, resolveConfig } from '../config-resolvers';
|
|
2
4
|
const path = require('path');
|
|
3
5
|
|
|
@@ -130,6 +132,40 @@ describe('resolveStyleguideConfig', () => {
|
|
|
130
132
|
expect(styleguide).toMatchSnapshot();
|
|
131
133
|
});
|
|
132
134
|
|
|
135
|
+
it('should resolve custom assertion from plugin', async () => {
|
|
136
|
+
const styleguideConfig = {
|
|
137
|
+
extends: ['local-config-with-custom-function.yaml'],
|
|
138
|
+
};
|
|
139
|
+
const { plugins } = await resolveStyleguideConfig({
|
|
140
|
+
styleguideConfig,
|
|
141
|
+
configPath,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(plugins).toBeDefined();
|
|
145
|
+
expect(plugins?.length).toBe(2);
|
|
146
|
+
expect(asserts['test-plugin/checkWordsCount']).toBeDefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw error when custom assertion load not exist plugin', async () => {
|
|
150
|
+
const styleguideConfig = {
|
|
151
|
+
extends: ['local-config-with-wrong-custom-function.yaml'],
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
await resolveStyleguideConfig({
|
|
155
|
+
styleguideConfig,
|
|
156
|
+
configPath,
|
|
157
|
+
});
|
|
158
|
+
} catch (e) {
|
|
159
|
+
expect(e.message.toString()).toContain(
|
|
160
|
+
`Plugin ${colorize.red(
|
|
161
|
+
'test-plugin'
|
|
162
|
+
)} doesn't export assertions function with name ${colorize.red('checkWordsCount2')}.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
expect(asserts['test-plugin/checkWordsCount']).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
133
169
|
it('should correctly merge assertions from nested config', async () => {
|
|
134
170
|
const styleguideConfig = {
|
|
135
171
|
extends: ['local-config-with-file.yaml'],
|
|
@@ -165,7 +201,7 @@ describe('resolveStyleguideConfig', () => {
|
|
|
165
201
|
const styleguideConfig = {
|
|
166
202
|
// This points to ./fixtures/resolve-remote-configs/remote-config.yaml
|
|
167
203
|
extends: [
|
|
168
|
-
'https://raw.githubusercontent.com/Redocly/redocly-cli/
|
|
204
|
+
'https://raw.githubusercontent.com/Redocly/redocly-cli/main/packages/core/src/config/__tests__/fixtures/resolve-remote-configs/remote-config.yaml',
|
|
169
205
|
],
|
|
170
206
|
};
|
|
171
207
|
|
|
@@ -48,6 +48,7 @@ const testConfig: Config = {
|
|
|
48
48
|
'features.mockServer': {},
|
|
49
49
|
resolve: { http: { headers: [] } },
|
|
50
50
|
organization: 'redocly-test',
|
|
51
|
+
files: [],
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
describe('getMergedConfig', () => {
|
|
@@ -67,6 +68,7 @@ describe('getMergedConfig', () => {
|
|
|
67
68
|
"configFile": "redocly.yaml",
|
|
68
69
|
"features.mockServer": Object {},
|
|
69
70
|
"features.openapi": Object {},
|
|
71
|
+
"files": Array [],
|
|
70
72
|
"organization": "redocly-test",
|
|
71
73
|
"rawConfig": Object {
|
|
72
74
|
"apis": Object {
|
|
@@ -81,6 +83,7 @@ describe('getMergedConfig', () => {
|
|
|
81
83
|
},
|
|
82
84
|
"features.mockServer": Object {},
|
|
83
85
|
"features.openapi": Object {},
|
|
86
|
+
"files": Array [],
|
|
84
87
|
"organization": "redocly-test",
|
|
85
88
|
"styleguide": Object {
|
|
86
89
|
"extendPaths": Array [],
|
|
@@ -163,6 +166,7 @@ describe('getMergedConfig', () => {
|
|
|
163
166
|
"configFile": "redocly.yaml",
|
|
164
167
|
"features.mockServer": Object {},
|
|
165
168
|
"features.openapi": Object {},
|
|
169
|
+
"files": Array [],
|
|
166
170
|
"organization": "redocly-test",
|
|
167
171
|
"rawConfig": Object {
|
|
168
172
|
"apis": Object {
|
|
@@ -177,6 +181,7 @@ describe('getMergedConfig', () => {
|
|
|
177
181
|
},
|
|
178
182
|
"features.mockServer": Object {},
|
|
179
183
|
"features.openapi": Object {},
|
|
184
|
+
"files": Array [],
|
|
180
185
|
"organization": "redocly-test",
|
|
181
186
|
"styleguide": Object {
|
|
182
187
|
"extendPaths": Array [],
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
lint:
|
|
2
|
+
rules:
|
|
3
|
+
no-invalid-media-type-examples: warn
|
|
4
|
+
operation-4xx-response: off
|
|
5
|
+
assert/tag-description:
|
|
6
|
+
subject: Tag
|
|
7
|
+
property: description
|
|
8
|
+
message: Tag description must have at least 3 words.
|
|
9
|
+
severity: error
|
|
10
|
+
test-plugin/checkWordsCount:
|
|
11
|
+
min: 3
|
|
12
|
+
plugins:
|
|
13
|
+
- plugin.js
|
|
14
|
+
extends:
|
|
15
|
+
- recommended
|
|
16
|
+
- test-plugin/all
|
package/src/config/__tests__/fixtures/resolve-config/local-config-with-wrong-custom-function.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
lint:
|
|
2
|
+
rules:
|
|
3
|
+
no-invalid-media-type-examples: warn
|
|
4
|
+
operation-4xx-response: off
|
|
5
|
+
assert/tag-description:
|
|
6
|
+
subject: Tag
|
|
7
|
+
property: description
|
|
8
|
+
message: Tag description must have at least 3 words.
|
|
9
|
+
severity: error
|
|
10
|
+
test-plugin/checkWordsCount2:
|
|
11
|
+
min: 3
|
|
12
|
+
plugins:
|
|
13
|
+
- plugin.js
|
|
14
|
+
extends:
|
|
15
|
+
- recommended
|
|
16
|
+
- test-plugin/all
|
|
@@ -51,6 +51,16 @@ const decorators = {
|
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
+
const assertions = {
|
|
55
|
+
checkWordsCount: (value, opts, location) => {
|
|
56
|
+
const words = value.split(' ');
|
|
57
|
+
if (words.length >= opts.min) {
|
|
58
|
+
return { isValid: true };
|
|
59
|
+
}
|
|
60
|
+
return { isValid: false, location };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
54
64
|
const configs = {
|
|
55
65
|
all: {
|
|
56
66
|
rules: {
|
|
@@ -66,4 +76,5 @@ module.exports = {
|
|
|
66
76
|
rules,
|
|
67
77
|
decorators,
|
|
68
78
|
configs,
|
|
79
|
+
assertions,
|
|
69
80
|
};
|
|
@@ -49,7 +49,7 @@ describe('loadConfig', () => {
|
|
|
49
49
|
|
|
50
50
|
it('should call callback if such passed', async () => {
|
|
51
51
|
const mockFn = jest.fn();
|
|
52
|
-
await loadConfig(
|
|
52
|
+
await loadConfig({ processRawConfig: mockFn });
|
|
53
53
|
expect(mockFn).toHaveBeenCalled();
|
|
54
54
|
});
|
|
55
55
|
});
|
|
@@ -5,21 +5,21 @@ describe('resolving a plugin', () => {
|
|
|
5
5
|
const configPath = path.join(__dirname, 'fixtures/plugin-config.yaml');
|
|
6
6
|
|
|
7
7
|
it('should prefix rule names with the plugin id', async () => {
|
|
8
|
-
const config = await loadConfig(configPath);
|
|
8
|
+
const config = await loadConfig({ configPath });
|
|
9
9
|
const plugin = config.styleguide.plugins[0];
|
|
10
10
|
|
|
11
11
|
expect(plugin.rules?.oas3).toHaveProperty('test-plugin/openid-connect-url-well-known');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('should prefix preprocessor names with the plugin id', async () => {
|
|
15
|
-
const config = await loadConfig(configPath);
|
|
15
|
+
const config = await loadConfig({ configPath });
|
|
16
16
|
const plugin = config.styleguide.plugins[0];
|
|
17
17
|
|
|
18
18
|
expect(plugin.preprocessors?.oas2).toHaveProperty('test-plugin/description-preprocessor');
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
it('should prefix decorator names with the plugin id', async () => {
|
|
22
|
-
const config = await loadConfig(configPath);
|
|
22
|
+
const config = await loadConfig({ configPath });
|
|
23
23
|
const plugin = config.styleguide.plugins[0];
|
|
24
24
|
|
|
25
25
|
expect(plugin.decorators?.oas3).toHaveProperty('test-plugin/inject-x-stats');
|
|
@@ -24,6 +24,7 @@ import { isBrowser } from '../env';
|
|
|
24
24
|
import { isNotString, isString, isDefined, parseYaml } from '../utils';
|
|
25
25
|
import { Config } from './config';
|
|
26
26
|
import { colorize, logger } from '../logger';
|
|
27
|
+
import { asserts, buildAssertCustomFunction } from '../rules/common/assertions/asserts';
|
|
27
28
|
|
|
28
29
|
export async function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config> {
|
|
29
30
|
if (rawConfig.styleguide?.extends?.some(isNotString)) {
|
|
@@ -175,6 +176,10 @@ export function resolvePlugins(
|
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
if (pluginModule.assertions) {
|
|
180
|
+
plugin.assertions = pluginModule.assertions;
|
|
181
|
+
}
|
|
182
|
+
|
|
178
183
|
return plugin;
|
|
179
184
|
})
|
|
180
185
|
.filter(isDefined);
|
|
@@ -299,8 +304,7 @@ export async function resolveStyleguideConfig(
|
|
|
299
304
|
return {
|
|
300
305
|
...resolvedStyleguideConfig,
|
|
301
306
|
rules:
|
|
302
|
-
resolvedStyleguideConfig.rules &&
|
|
303
|
-
groupStyleguideAssertionRules(resolvedStyleguideConfig.rules),
|
|
307
|
+
resolvedStyleguideConfig.rules && groupStyleguideAssertionRules(resolvedStyleguideConfig),
|
|
304
308
|
};
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -392,9 +396,10 @@ function getMergedRawStyleguideConfig(
|
|
|
392
396
|
return resultLint;
|
|
393
397
|
}
|
|
394
398
|
|
|
395
|
-
function groupStyleguideAssertionRules(
|
|
396
|
-
rules
|
|
397
|
-
|
|
399
|
+
function groupStyleguideAssertionRules({
|
|
400
|
+
rules,
|
|
401
|
+
plugins,
|
|
402
|
+
}: ResolvedStyleguideConfig): Record<string, RuleConfig> | undefined {
|
|
398
403
|
if (!rules) {
|
|
399
404
|
return rules;
|
|
400
405
|
}
|
|
@@ -407,6 +412,24 @@ function groupStyleguideAssertionRules(
|
|
|
407
412
|
for (const [ruleKey, rule] of Object.entries(rules)) {
|
|
408
413
|
if (ruleKey.startsWith('assert/') && typeof rule === 'object' && rule !== null) {
|
|
409
414
|
const assertion = rule;
|
|
415
|
+
if (plugins) {
|
|
416
|
+
for (const field of Object.keys(assertion)) {
|
|
417
|
+
const [pluginId, fn] = field.split('/');
|
|
418
|
+
if (!pluginId || !fn) continue;
|
|
419
|
+
const plugin = plugins.find((plugin) => plugin.id === pluginId);
|
|
420
|
+
if (!plugin) {
|
|
421
|
+
throw Error(colorize.red(`Plugin ${colorize.blue(pluginId)} isn't found.`));
|
|
422
|
+
}
|
|
423
|
+
if (!plugin.assertions || !plugin.assertions[fn]) {
|
|
424
|
+
throw Error(
|
|
425
|
+
`Plugin ${colorize.red(
|
|
426
|
+
pluginId
|
|
427
|
+
)} doesn't export assertions function with name ${colorize.red(fn)}.`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
asserts[field] = buildAssertCustomFunction(plugin.assertions[fn]);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
410
433
|
assertions.push({
|
|
411
434
|
...assertion,
|
|
412
435
|
assertionId: ruleKey.replace('assert/', ''),
|
package/src/config/config.ts
CHANGED
|
@@ -306,6 +306,7 @@ export class Config {
|
|
|
306
306
|
'features.openapi': Record<string, any>;
|
|
307
307
|
'features.mockServer'?: Record<string, any>;
|
|
308
308
|
organization?: string;
|
|
309
|
+
files: string[];
|
|
309
310
|
constructor(public rawConfig: ResolvedConfig, public configFile?: string) {
|
|
310
311
|
this.apis = rawConfig.apis || {};
|
|
311
312
|
this.styleguide = new StyleguideConfig(rawConfig.styleguide || {}, configFile);
|
|
@@ -314,5 +315,6 @@ export class Config {
|
|
|
314
315
|
this.resolve = getResolveConfig(rawConfig?.resolve);
|
|
315
316
|
this.region = rawConfig.region;
|
|
316
317
|
this.organization = rawConfig.organization;
|
|
318
|
+
this.files = rawConfig.files || [];
|
|
317
319
|
}
|
|
318
320
|
}
|
package/src/config/load.ts
CHANGED
|
@@ -62,11 +62,17 @@ async function addConfigMetadata({
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export async function loadConfig(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
options: {
|
|
66
|
+
configPath?: string;
|
|
67
|
+
customExtends?: string[];
|
|
68
|
+
processRawConfig?: (rawConfig: RawConfig) => void | Promise<void>;
|
|
69
|
+
files?: string[];
|
|
70
|
+
region?: Region;
|
|
71
|
+
} = {}
|
|
68
72
|
): Promise<Config> {
|
|
69
|
-
const
|
|
73
|
+
const { configPath = findConfig(), customExtends, processRawConfig, files, region } = options;
|
|
74
|
+
const config = await getConfig(configPath);
|
|
75
|
+
const rawConfig = { ...config, files: files ?? config.files, region: region ?? config.region };
|
|
70
76
|
if (typeof processRawConfig === 'function') {
|
|
71
77
|
await processRawConfig(rawConfig);
|
|
72
78
|
}
|
package/src/config/types.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
OasVersion,
|
|
11
11
|
} from '../oas-types';
|
|
12
12
|
import type { NodeType } from '../types';
|
|
13
|
+
import { Location } from '../ref-utils';
|
|
13
14
|
|
|
14
15
|
export type RuleSeverity = ProblemSeverity | 'off';
|
|
15
16
|
|
|
@@ -83,6 +84,15 @@ export type CustomRulesConfig = {
|
|
|
83
84
|
oas2?: Oas2RuleSet;
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
export type AssertResult = { message?: string; location?: Location };
|
|
88
|
+
export type CustomFunction = (
|
|
89
|
+
value: any,
|
|
90
|
+
options: unknown,
|
|
91
|
+
baseLocation: Location
|
|
92
|
+
) => AssertResult[];
|
|
93
|
+
|
|
94
|
+
export type AssertionsConfig = Record<string, CustomFunction>;
|
|
95
|
+
|
|
86
96
|
export type Plugin = {
|
|
87
97
|
id: string;
|
|
88
98
|
configs?: Record<string, PluginStyleguideConfig>;
|
|
@@ -90,6 +100,7 @@ export type Plugin = {
|
|
|
90
100
|
preprocessors?: PreprocessorsConfig;
|
|
91
101
|
decorators?: DecoratorsConfig;
|
|
92
102
|
typeExtension?: TypeExtensionsConfig;
|
|
103
|
+
assertions?: AssertionsConfig;
|
|
93
104
|
};
|
|
94
105
|
|
|
95
106
|
export type PluginStyleguideConfig = Omit<StyleguideRawConfig, 'plugins' | 'extends'>;
|
|
@@ -145,6 +156,7 @@ export type DeprecatedInApi = {
|
|
|
145
156
|
|
|
146
157
|
export type ResolvedApi = Omit<Api, 'styleguide'> & {
|
|
147
158
|
styleguide: ResolvedStyleguideConfig;
|
|
159
|
+
files?: string[];
|
|
148
160
|
};
|
|
149
161
|
|
|
150
162
|
export type RawConfig = {
|
|
@@ -153,6 +165,7 @@ export type RawConfig = {
|
|
|
153
165
|
resolve?: RawResolveConfig;
|
|
154
166
|
region?: Region;
|
|
155
167
|
organization?: string;
|
|
168
|
+
files?: string[];
|
|
156
169
|
} & FeaturesConfig;
|
|
157
170
|
|
|
158
171
|
export type FlatApi = Omit<Api, 'styleguide'> &
|
package/src/config/utils.ts
CHANGED
|
@@ -233,6 +233,7 @@ export function getMergedConfig(config: Config, apiName?: string): Config {
|
|
|
233
233
|
...config['features.mockServer'],
|
|
234
234
|
...config.apis[apiName]?.['features.mockServer'],
|
|
235
235
|
},
|
|
236
|
+
files: [...config.files, ...(config.apis?.[apiName]?.files ?? [])],
|
|
236
237
|
// TODO: merge everything else here
|
|
237
238
|
},
|
|
238
239
|
config.configFile
|
package/src/rules/ajv.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Ajv, { ValidateFunction, ErrorObject } from '@redocly/ajv';
|
|
1
|
+
import Ajv, { ValidateFunction, ErrorObject } from '@redocly/ajv/dist/2020';
|
|
2
2
|
import { Location, escapePointer } from '../ref-utils';
|
|
3
3
|
import { ResolveFn } from '../walk';
|
|
4
4
|
|
|
@@ -20,7 +20,7 @@ function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) {
|
|
|
20
20
|
discriminator: true,
|
|
21
21
|
allowUnionTypes: true,
|
|
22
22
|
validateFormats: false, // TODO: fix it
|
|
23
|
-
|
|
23
|
+
defaultUnevaluatedProperties: allowAdditionalProperties,
|
|
24
24
|
loadSchemaSync(base: string, $ref: string) {
|
|
25
25
|
const resolvedRef = resolve({ $ref }, base.split('#')[0]);
|
|
26
26
|
if (!resolvedRef || !resolvedRef.location) return false;
|
|
@@ -87,8 +87,8 @@ export function validateJsonSchema(
|
|
|
87
87
|
if (propName) {
|
|
88
88
|
message = `\`${propName}\` property ${message}`;
|
|
89
89
|
}
|
|
90
|
-
if (error.keyword === 'additionalProperties') {
|
|
91
|
-
const property = error.params.additionalProperty;
|
|
90
|
+
if (error.keyword === 'additionalProperties' || error.keyword === 'unevaluatedProperties') {
|
|
91
|
+
const property = error.params.additionalProperty || error.params.unevaluatedProperty;
|
|
92
92
|
message = `${message} \`${property}\``;
|
|
93
93
|
error.instancePath += '/' + escapePointer(property);
|
|
94
94
|
}
|