@amritk/lint 0.1.0 → 0.3.0
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 +52 -1
- package/dist/core/glob.d.ts +1 -1
- package/dist/core/glob.js +89 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/jsonpath.d.ts +17 -1
- package/dist/core/jsonpath.js +218 -23
- package/dist/core/lint.d.ts +15 -8
- package/dist/core/lint.js +12 -3
- package/dist/core/plugin.d.ts +6 -0
- package/dist/core/plugin.js +6 -0
- package/dist/core/pointers.js +15 -15
- package/dist/core/ruleset.js +0 -0
- package/dist/core/runner.d.ts +6 -1
- package/dist/core/runner.js +127 -43
- package/dist/core/types.d.ts +17 -2
- package/dist/core/validate-ruleset.js +16 -0
- package/dist/fix/apply.d.ts +8 -2
- package/dist/fix/apply.js +68 -18
- package/dist/functions/alphabetical.js +40 -13
- package/dist/functions/casing.js +27 -5
- package/dist/functions/enumeration.d.ts +5 -3
- package/dist/functions/enumeration.js +18 -1
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.js +3 -0
- package/dist/functions/length.d.ts +13 -3
- package/dist/functions/length.js +11 -3
- package/dist/functions/or.d.ts +11 -0
- package/dist/functions/or.js +25 -0
- package/dist/functions/pattern.d.ts +5 -3
- package/dist/functions/pattern.js +42 -9
- package/dist/functions/schema.d.ts +13 -0
- package/dist/functions/schema.js +95 -2
- package/dist/functions/typed-enum.js +7 -1
- package/dist/functions/unreferenced-reusable-object.d.ts +7 -1
- package/dist/functions/unreferenced-reusable-object.js +18 -3
- package/dist/functions/xor.js +8 -1
- package/dist/index.js +9 -1
- package/dist/parsers/edit-model.d.ts +15 -0
- package/dist/parsers/edit-model.js +210 -41
- package/dist/parsers/types.d.ts +14 -2
- package/dist/parsers/yaml.d.ts +10 -0
- package/dist/parsers/yaml.js +174 -26
- package/dist/rules/openapi/fixers.d.ts +7 -0
- package/dist/rules/openapi/fixers.js +248 -0
- package/dist/rules/openapi/formats.d.ts +13 -0
- package/dist/rules/openapi/formats.js +30 -0
- package/dist/rules/openapi/functions/example-validation.d.ts +18 -0
- package/dist/rules/openapi/functions/example-validation.js +160 -0
- package/dist/rules/openapi/functions/helpers.d.ts +4 -0
- package/dist/rules/openapi/functions/helpers.js +10 -0
- package/dist/rules/openapi/functions/index.d.ts +26 -0
- package/dist/rules/openapi/functions/index.js +73 -0
- package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
- package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
- package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
- package/dist/rules/openapi/functions/oas-example-external-value.d.ts +11 -0
- package/dist/rules/openapi/functions/oas-example-external-value.js +23 -0
- package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-example-value.js +31 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
- package/dist/rules/openapi/functions/oas-no-nullable.d.ts +13 -0
- package/dist/rules/openapi/functions/oas-no-nullable.js +22 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-id-unique.js +29 -0
- package/dist/rules/openapi/functions/oas-op-params.d.ts +9 -0
- package/dist/rules/openapi/functions/oas-op-params.js +46 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
- package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
- package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-op-success-response.js +16 -0
- package/dist/rules/openapi/functions/oas-path-param.d.ts +12 -0
- package/dist/rules/openapi/functions/oas-path-param.js +106 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
- package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
- package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
- package/dist/rules/openapi/functions/oas-schema.js +15 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
- package/dist/rules/openapi/functions/oas-server-variables.d.ts +8 -0
- package/dist/rules/openapi/functions/oas-server-variables.js +52 -0
- package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
- package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
- package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
- package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
- package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
- package/dist/rules/openapi/functions/oas-unused-component.js +65 -0
- package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
- package/dist/rules/openapi/functions/ref-siblings.js +13 -0
- package/dist/rules/openapi/index.d.ts +25 -0
- package/dist/rules/openapi/index.js +127 -0
- package/dist/rules/openapi/oas.d.ts +3 -0
- package/dist/rules/openapi/oas.js +549 -0
- package/dist/rules/openapi/schemas/index.d.ts +8 -0
- package/dist/rules/openapi/schemas/index.js +36 -0
- package/dist/rules/openapi/schemas/oas20.json +1592 -0
- package/dist/rules/openapi/schemas/oas30.json +1651 -0
- package/dist/rules/openapi/schemas/oas31.json +1412 -0
- package/dist/rules/openapi/schemas/oas32.json +1684 -0
- package/package.json +17 -5
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { validate } from '@amritk/runtime-validators';
|
|
2
|
+
import { isObject } from './helpers.js';
|
|
3
|
+
// The example schemas come from the *linted document*, so they are only known at
|
|
4
|
+
// runtime. `@amritk/runtime-validators` interprets a schema directly — no
|
|
5
|
+
// `ajv.compile` (whose codegen dominated lint time on large specs, recompiling
|
|
6
|
+
// `$ref`-duplicated schemas tens of thousands of times) and no `new Function`,
|
|
7
|
+
// so it is also CSP/edge-runtime safe. Returns undefined for a non-object or a
|
|
8
|
+
// schema the validator can't build (mirrors the old skip-on-compile-failure
|
|
9
|
+
// behavior).
|
|
10
|
+
//
|
|
11
|
+
// Formats are asserted (`formats: 'all'`) to match Spectral, whose example rules
|
|
12
|
+
// run ajv with `ajv-formats` enabled — otherwise a format-violating example (a
|
|
13
|
+
// bad `email`/`date`/`uuid`) would slip through. This mirrors the core `schema`
|
|
14
|
+
// built-in's option exactly; `@amritk/runtime-validators` treats the OAS-specific
|
|
15
|
+
// numeric/binary formats (`int32`/`int64`/`float`/`byte`/`binary`) as non-failing.
|
|
16
|
+
const buildValidator = (schema) => {
|
|
17
|
+
if (!isObject(schema))
|
|
18
|
+
return undefined;
|
|
19
|
+
let validator;
|
|
20
|
+
try {
|
|
21
|
+
validator = validate(schema, { formats: 'all' });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// A schema can carry a `$ref` the runtime validator cannot resolve (an external
|
|
27
|
+
// or cyclic ref left behind after `$ref` inlining), which throws at *run* time
|
|
28
|
+
// rather than build time. Treat that as "cannot validate — skip" (returning a
|
|
29
|
+
// valid result) so an unresolvable example schema never crashes the whole lint.
|
|
30
|
+
return (input) => {
|
|
31
|
+
try {
|
|
32
|
+
return validator(input);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const buildValidatorOrNull = (schema) => buildValidator(schema) ?? null;
|
|
40
|
+
// Keyed by the Schema Object node; `oasSchemaExample` validates a schema's own
|
|
41
|
+
// `example`/`default` against the schema minus its `example`/`examples` keywords.
|
|
42
|
+
const schemaExampleResults = new WeakMap();
|
|
43
|
+
// Keyed by the Media Type / Response Object node; `oasMediaExample` validates the
|
|
44
|
+
// object's example(s) against its `schema`. The node (not the schema) is the cache
|
|
45
|
+
// key because two media objects can share a `schema` but carry different examples.
|
|
46
|
+
const mediaExampleResults = new WeakMap();
|
|
47
|
+
const withPath = (findings, path) => {
|
|
48
|
+
if (findings.length === 0)
|
|
49
|
+
return [];
|
|
50
|
+
return findings.map((finding) => ({ message: finding.message, path: [...path, ...finding.suffix] }));
|
|
51
|
+
};
|
|
52
|
+
// A schema node reached via `$..[?(...)]` might be a `properties`/`patternProperties`
|
|
53
|
+
// *map* whose keys happen to look like schema keywords (e.g. a property literally
|
|
54
|
+
// named `type`). Those maps are not Schema Objects, so we never validate them.
|
|
55
|
+
const isPropertiesMap = (path) => {
|
|
56
|
+
const tail = path[path.length - 1];
|
|
57
|
+
return tail === 'properties' || tail === 'patternProperties';
|
|
58
|
+
};
|
|
59
|
+
/** Validates a schema object's inline `example` and `default` against the schema itself. */
|
|
60
|
+
export const oasSchemaExample = (schema, _options, context) => {
|
|
61
|
+
if (!isObject(schema))
|
|
62
|
+
return [];
|
|
63
|
+
if (schema['example'] === undefined && schema['default'] === undefined)
|
|
64
|
+
return [];
|
|
65
|
+
if (isPropertiesMap(context.path))
|
|
66
|
+
return [];
|
|
67
|
+
let findings = schemaExampleResults.get(schema);
|
|
68
|
+
if (findings === undefined) {
|
|
69
|
+
findings = [];
|
|
70
|
+
// `example`/`examples` are annotations, not constraints, so drop them before
|
|
71
|
+
// building the validator; `default` stays (it is not a validation keyword).
|
|
72
|
+
const { example, examples, ...rest } = schema;
|
|
73
|
+
void example;
|
|
74
|
+
void examples;
|
|
75
|
+
const check = buildValidatorOrNull(rest);
|
|
76
|
+
if (check) {
|
|
77
|
+
for (const field of ['example', 'default']) {
|
|
78
|
+
if (schema[field] === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
const result = check(schema[field]);
|
|
81
|
+
if (result !== true) {
|
|
82
|
+
for (const error of result.errors)
|
|
83
|
+
findings.push({ message: `"${field}" ${error.message}`.trim(), suffix: [field] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
schemaExampleResults.set(schema, findings);
|
|
88
|
+
}
|
|
89
|
+
return withPath(findings, context.path);
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Validates a Media Type / Response / Parameter object's examples against its
|
|
93
|
+
* `schema`. Version-split because OpenAPI 2.0 and 3.x model examples differently:
|
|
94
|
+
* - OAS3: a singular `example` value plus an `examples` map of Example Objects,
|
|
95
|
+
* each of which carries the value under `value`.
|
|
96
|
+
* - OAS2: `examples` is a MIME-type → value map (`{ 'application/json': value }`),
|
|
97
|
+
* with no Example Objects and no singular `example` on the media object — so the
|
|
98
|
+
* 3.x logic validated nothing at all for a real 2.0 document.
|
|
99
|
+
*/
|
|
100
|
+
export const oasMediaExample = (media, options, context) => {
|
|
101
|
+
if (!isObject(media) || !isObject(media['schema']))
|
|
102
|
+
return [];
|
|
103
|
+
const oasVersion = options?.oasVersion ?? 3;
|
|
104
|
+
// Skip building a validator when there is nothing to check — most media objects
|
|
105
|
+
// in a large spec have a schema but no example. (OAS2 only has the map form.)
|
|
106
|
+
const hasExample = (oasVersion !== 2 && media['example'] !== undefined) || isObject(media['examples']);
|
|
107
|
+
if (!hasExample)
|
|
108
|
+
return [];
|
|
109
|
+
let findings = mediaExampleResults.get(media);
|
|
110
|
+
if (findings === undefined) {
|
|
111
|
+
findings = [];
|
|
112
|
+
const check = buildValidatorOrNull(media['schema']);
|
|
113
|
+
if (check) {
|
|
114
|
+
if (oasVersion === 2)
|
|
115
|
+
collectOas2(media, check, findings);
|
|
116
|
+
else
|
|
117
|
+
collectOas3(media, check, findings);
|
|
118
|
+
}
|
|
119
|
+
mediaExampleResults.set(media, findings);
|
|
120
|
+
}
|
|
121
|
+
return withPath(findings, context.path);
|
|
122
|
+
};
|
|
123
|
+
/** OAS2: validate each `examples[mimeType]` value against the sibling `schema`. */
|
|
124
|
+
const collectOas2 = (media, check, findings) => {
|
|
125
|
+
const examples = isObject(media['examples']) ? media['examples'] : undefined;
|
|
126
|
+
if (!examples)
|
|
127
|
+
return;
|
|
128
|
+
for (const [mimeType, value] of Object.entries(examples)) {
|
|
129
|
+
const result = check(value);
|
|
130
|
+
if (result !== true) {
|
|
131
|
+
for (const error of result.errors) {
|
|
132
|
+
findings.push({ message: `Example "${mimeType}" ${error.message}`.trim(), suffix: ['examples', mimeType] });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/** OAS3: validate the singular `example` and each `examples[name].value` against the `schema`. */
|
|
138
|
+
const collectOas3 = (media, check, findings) => {
|
|
139
|
+
if (media['example'] !== undefined) {
|
|
140
|
+
const result = check(media['example']);
|
|
141
|
+
if (result !== true) {
|
|
142
|
+
for (const error of result.errors)
|
|
143
|
+
findings.push({ message: `"example" ${error.message}`.trim(), suffix: ['example'] });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const examples = isObject(media['examples']) ? media['examples'] : undefined;
|
|
147
|
+
if (!examples)
|
|
148
|
+
return;
|
|
149
|
+
for (const [name, example] of Object.entries(examples)) {
|
|
150
|
+
// An external-value example is fetched elsewhere, so there is nothing inline to check.
|
|
151
|
+
if (isObject(example) && example['value'] !== undefined) {
|
|
152
|
+
const result = check(example['value']);
|
|
153
|
+
if (result !== true) {
|
|
154
|
+
for (const error of result.errors) {
|
|
155
|
+
findings.push({ message: `Example "${name}" ${error.message}`.trim(), suffix: ['examples', name, 'value'] });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** True for a non-null, non-array object (an OpenAPI "object" value). */
|
|
2
|
+
export const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
// The eight standard HTTP methods that have a dedicated fixed field on the Path
|
|
4
|
+
// Item Object. Shared by the rules that iterate a path item's operations.
|
|
5
|
+
export const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
|
|
6
|
+
// OpenAPI 3.2 promoted `query` to a fixed Path Item Object operation field, so the
|
|
7
|
+
// rules that walk a path item's operations must consider it too. Including it is
|
|
8
|
+
// harmless on older versions, which never carry a `query` operation, so the same
|
|
9
|
+
// set works across every OpenAPI version.
|
|
10
|
+
export const OPERATION_METHODS = new Set([...HTTP_METHODS, 'query']);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FunctionRegistry } from '../../../core/index.js';
|
|
2
|
+
export { type IOasExampleOptions, oasMediaExample, oasSchemaExample } from './example-validation.js';
|
|
3
|
+
export { oasAdditionalOperations } from './oas-additional-operations.js';
|
|
4
|
+
export { oasDiscriminator } from './oas-discriminator.js';
|
|
5
|
+
export { oasExampleExternalValue } from './oas-example-external-value.js';
|
|
6
|
+
export { oasExampleValue } from './oas-example-value.js';
|
|
7
|
+
export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
|
|
8
|
+
export { oasNoNullable } from './oas-no-nullable.js';
|
|
9
|
+
export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
|
|
10
|
+
export { oasOpIdUnique } from './oas-op-id-unique.js';
|
|
11
|
+
export { oasOpParams } from './oas-op-params.js';
|
|
12
|
+
export { oasOpSecurityDefined } from './oas-op-security-defined.js';
|
|
13
|
+
export { oasOpSuccessResponse } from './oas-op-success-response.js';
|
|
14
|
+
export { oasPathParam } from './oas-path-param.js';
|
|
15
|
+
export { type IOasSchemaOptions, oasSchema } from './oas-schema.js';
|
|
16
|
+
export { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
|
|
17
|
+
export { oasServerNameUnique } from './oas-server-name-unique.js';
|
|
18
|
+
export { oasServerVariables } from './oas-server-variables.js';
|
|
19
|
+
export { oasTagDefined } from './oas-tag-defined.js';
|
|
20
|
+
export { oasTagKind } from './oas-tag-kind.js';
|
|
21
|
+
export { oasTagParentDefined } from './oas-tag-parent-defined.js';
|
|
22
|
+
export { oasTagsUnique } from './oas-tags-unique.js';
|
|
23
|
+
export { oasUnusedComponent } from './oas-unused-component.js';
|
|
24
|
+
export { refSiblings } from './ref-siblings.js';
|
|
25
|
+
/** The OpenAPI-specific custom functions, keyed by name for ruleset `then` references. */
|
|
26
|
+
export declare const oasFunctions: FunctionRegistry;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { oasMediaExample, oasSchemaExample } from './example-validation.js';
|
|
2
|
+
import { oasAdditionalOperations } from './oas-additional-operations.js';
|
|
3
|
+
import { oasDiscriminator } from './oas-discriminator.js';
|
|
4
|
+
import { oasExampleExternalValue } from './oas-example-external-value.js';
|
|
5
|
+
import { oasExampleValue } from './oas-example-value.js';
|
|
6
|
+
import { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
|
|
7
|
+
import { oasNoNullable } from './oas-no-nullable.js';
|
|
8
|
+
import { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
|
|
9
|
+
import { oasOpIdUnique } from './oas-op-id-unique.js';
|
|
10
|
+
import { oasOpParams } from './oas-op-params.js';
|
|
11
|
+
import { oasOpSecurityDefined } from './oas-op-security-defined.js';
|
|
12
|
+
import { oasOpSuccessResponse } from './oas-op-success-response.js';
|
|
13
|
+
import { oasPathParam } from './oas-path-param.js';
|
|
14
|
+
import { oasSchema } from './oas-schema.js';
|
|
15
|
+
import { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
|
|
16
|
+
import { oasServerNameUnique } from './oas-server-name-unique.js';
|
|
17
|
+
import { oasServerVariables } from './oas-server-variables.js';
|
|
18
|
+
import { oasTagDefined } from './oas-tag-defined.js';
|
|
19
|
+
import { oasTagKind } from './oas-tag-kind.js';
|
|
20
|
+
import { oasTagParentDefined } from './oas-tag-parent-defined.js';
|
|
21
|
+
import { oasTagsUnique } from './oas-tags-unique.js';
|
|
22
|
+
import { oasUnusedComponent } from './oas-unused-component.js';
|
|
23
|
+
import { refSiblings } from './ref-siblings.js';
|
|
24
|
+
export { oasMediaExample, oasSchemaExample } from './example-validation.js';
|
|
25
|
+
export { oasAdditionalOperations } from './oas-additional-operations.js';
|
|
26
|
+
export { oasDiscriminator } from './oas-discriminator.js';
|
|
27
|
+
export { oasExampleExternalValue } from './oas-example-external-value.js';
|
|
28
|
+
export { oasExampleValue } from './oas-example-value.js';
|
|
29
|
+
export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
|
|
30
|
+
export { oasNoNullable } from './oas-no-nullable.js';
|
|
31
|
+
export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
|
|
32
|
+
export { oasOpIdUnique } from './oas-op-id-unique.js';
|
|
33
|
+
export { oasOpParams } from './oas-op-params.js';
|
|
34
|
+
export { oasOpSecurityDefined } from './oas-op-security-defined.js';
|
|
35
|
+
export { oasOpSuccessResponse } from './oas-op-success-response.js';
|
|
36
|
+
export { oasPathParam } from './oas-path-param.js';
|
|
37
|
+
export { oasSchema } from './oas-schema.js';
|
|
38
|
+
export { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
|
|
39
|
+
export { oasServerNameUnique } from './oas-server-name-unique.js';
|
|
40
|
+
export { oasServerVariables } from './oas-server-variables.js';
|
|
41
|
+
export { oasTagDefined } from './oas-tag-defined.js';
|
|
42
|
+
export { oasTagKind } from './oas-tag-kind.js';
|
|
43
|
+
export { oasTagParentDefined } from './oas-tag-parent-defined.js';
|
|
44
|
+
export { oasTagsUnique } from './oas-tags-unique.js';
|
|
45
|
+
export { oasUnusedComponent } from './oas-unused-component.js';
|
|
46
|
+
export { refSiblings } from './ref-siblings.js';
|
|
47
|
+
/** The OpenAPI-specific custom functions, keyed by name for ruleset `then` references. */
|
|
48
|
+
export const oasFunctions = {
|
|
49
|
+
refSiblings,
|
|
50
|
+
oasOpSuccessResponse,
|
|
51
|
+
oasTagDefined,
|
|
52
|
+
oasOpIdUnique,
|
|
53
|
+
oasPathParam,
|
|
54
|
+
oasOpParams,
|
|
55
|
+
oasTagsUnique,
|
|
56
|
+
oasOpSecurityDefined: oasOpSecurityDefined,
|
|
57
|
+
oasOpFormDataConsumeCheck,
|
|
58
|
+
oasDiscriminator,
|
|
59
|
+
oasServerVariables,
|
|
60
|
+
oasSchemaExample,
|
|
61
|
+
oasMediaExample: oasMediaExample,
|
|
62
|
+
oasUnusedComponent,
|
|
63
|
+
oasMutuallyExclusive: oasMutuallyExclusive,
|
|
64
|
+
oasNoNullable,
|
|
65
|
+
oasSchema: oasSchema,
|
|
66
|
+
oasAdditionalOperations,
|
|
67
|
+
oasServerNameUnique,
|
|
68
|
+
oasTagParentDefined,
|
|
69
|
+
oasSchemaExampleDeprecated,
|
|
70
|
+
oasTagKind,
|
|
71
|
+
oasExampleValue,
|
|
72
|
+
oasExampleExternalValue,
|
|
73
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags standard HTTP methods inside an OpenAPI 3.2 `additionalOperations` map.
|
|
4
|
+
* The spec reserves that map for methods without a dedicated fixed field, so a
|
|
5
|
+
* standard method key (sent uppercase, e.g. `POST`) belongs in the lowercase
|
|
6
|
+
* fixed field (`post`) instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare const oasAdditionalOperations: RulesetFunction;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isObject, OPERATION_METHODS } from './helpers.js';
|
|
2
|
+
// Methods that have a dedicated fixed field on the Path Item Object (the eight
|
|
3
|
+
// standard methods plus 3.2's `query`). These MUST NOT be redefined inside the
|
|
4
|
+
// new `additionalOperations` map, which is reserved for non-standard methods.
|
|
5
|
+
const FIXED_PATH_ITEM_METHODS = OPERATION_METHODS;
|
|
6
|
+
/**
|
|
7
|
+
* Flags standard HTTP methods inside an OpenAPI 3.2 `additionalOperations` map.
|
|
8
|
+
* The spec reserves that map for methods without a dedicated fixed field, so a
|
|
9
|
+
* standard method key (sent uppercase, e.g. `POST`) belongs in the lowercase
|
|
10
|
+
* fixed field (`post`) instead.
|
|
11
|
+
*/
|
|
12
|
+
export const oasAdditionalOperations = (input, _options, context) => {
|
|
13
|
+
if (!isObject(input))
|
|
14
|
+
return [];
|
|
15
|
+
const results = [];
|
|
16
|
+
for (const method of Object.keys(input)) {
|
|
17
|
+
if (FIXED_PATH_ITEM_METHODS.has(method.toLowerCase())) {
|
|
18
|
+
results.push({
|
|
19
|
+
message: `"additionalOperations" must not redefine the standard method "${method}"; use the "${method.toLowerCase()}" field instead`,
|
|
20
|
+
path: [...context.path, method],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return results;
|
|
25
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Validates a v2 discriminator references a required property. */
|
|
3
|
+
export const oasDiscriminator = (schema, _options, context) => {
|
|
4
|
+
if (!isObject(schema) || typeof schema['discriminator'] !== 'string')
|
|
5
|
+
return [];
|
|
6
|
+
const property = schema['discriminator'];
|
|
7
|
+
const required = Array.isArray(schema['required']) ? schema['required'] : [];
|
|
8
|
+
const properties = isObject(schema['properties']) ? schema['properties'] : {};
|
|
9
|
+
const results = [];
|
|
10
|
+
if (!(property in properties)) {
|
|
11
|
+
results.push({
|
|
12
|
+
message: `Discriminator "${property}" must be defined in properties`,
|
|
13
|
+
path: [...context.path, 'discriminator'],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (!required.includes(property)) {
|
|
17
|
+
results.push({
|
|
18
|
+
message: `Discriminator "${property}" must be a required property`,
|
|
19
|
+
path: [...context.path, 'discriminator'],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Enforces that an Example Object carries exactly one inline value source. Across
|
|
4
|
+
* all of OpenAPI 3.x an Example must use either `value` or `externalValue` (not
|
|
5
|
+
* both, not neither). OpenAPI 3.2 added `dataValue`/`serializedValue` as further
|
|
6
|
+
* ways to supply the example, so their presence also satisfies the "has an
|
|
7
|
+
* example" requirement — otherwise a valid 3.2 `dataValue`-only example would be
|
|
8
|
+
* wrongly flagged. The mutual exclusivity of the new fields is policed separately
|
|
9
|
+
* by `oas3_2-example-value`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const oasExampleExternalValue: RulesetFunction;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Enforces that an Example Object carries exactly one inline value source. Across
|
|
4
|
+
* all of OpenAPI 3.x an Example must use either `value` or `externalValue` (not
|
|
5
|
+
* both, not neither). OpenAPI 3.2 added `dataValue`/`serializedValue` as further
|
|
6
|
+
* ways to supply the example, so their presence also satisfies the "has an
|
|
7
|
+
* example" requirement — otherwise a valid 3.2 `dataValue`-only example would be
|
|
8
|
+
* wrongly flagged. The mutual exclusivity of the new fields is policed separately
|
|
9
|
+
* by `oas3_2-example-value`.
|
|
10
|
+
*/
|
|
11
|
+
export const oasExampleExternalValue = (example) => {
|
|
12
|
+
if (!isObject(example))
|
|
13
|
+
return [];
|
|
14
|
+
// A 3.2 dataValue/serializedValue already provides the example, so value /
|
|
15
|
+
// externalValue are optional in that case.
|
|
16
|
+
if (example['dataValue'] !== undefined || example['serializedValue'] !== undefined)
|
|
17
|
+
return [];
|
|
18
|
+
const present = ['value', 'externalValue'].filter((property) => property in example);
|
|
19
|
+
if (present.length !== 1) {
|
|
20
|
+
return [{ message: 'Example object must have exactly one of "value" or "externalValue"' }];
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
// OpenAPI 3.2 Example Object exclusivity (MUST-level, per the spec's field
|
|
3
|
+
// table). The `value`/`externalValue` pair is intentionally omitted here — the
|
|
4
|
+
// 3.x-wide `oas3-examples-value-or-externalValue` rule already covers it, so we
|
|
5
|
+
// only police the constraints introduced with the new `dataValue` /
|
|
6
|
+
// `serializedValue` fields to avoid double-reporting.
|
|
7
|
+
const EXAMPLE_EXCLUSIONS = [
|
|
8
|
+
// dataValue MUST NOT accompany value.
|
|
9
|
+
{ field: 'dataValue', conflicts: ['value'] },
|
|
10
|
+
// serializedValue MUST NOT accompany value or externalValue.
|
|
11
|
+
{ field: 'serializedValue', conflicts: ['value', 'externalValue'] },
|
|
12
|
+
];
|
|
13
|
+
/** Flags forbidden field combinations on an OpenAPI 3.2 Example Object. */
|
|
14
|
+
export const oasExampleValue = (example, _options, context) => {
|
|
15
|
+
if (!isObject(example))
|
|
16
|
+
return [];
|
|
17
|
+
const results = [];
|
|
18
|
+
for (const { field, conflicts } of EXAMPLE_EXCLUSIONS) {
|
|
19
|
+
if (example[field] === undefined)
|
|
20
|
+
continue;
|
|
21
|
+
for (const other of conflicts) {
|
|
22
|
+
if (example[other] !== undefined) {
|
|
23
|
+
results.push({
|
|
24
|
+
message: `"${field}" must not be used together with "${other}"`,
|
|
25
|
+
path: [...context.path, other],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags objects that carry more than one of a set of mutually exclusive
|
|
4
|
+
* properties. OpenAPI 3.1's License Object, for example, defines `identifier`
|
|
5
|
+
* (SPDX) as "mutually exclusive of the url field" — neither, or exactly one, is
|
|
6
|
+
* allowed, but not both.
|
|
7
|
+
*/
|
|
8
|
+
export declare const oasMutuallyExclusive: RulesetFunction<Record<string, unknown>, {
|
|
9
|
+
properties: string[];
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags objects that carry more than one of a set of mutually exclusive
|
|
4
|
+
* properties. OpenAPI 3.1's License Object, for example, defines `identifier`
|
|
5
|
+
* (SPDX) as "mutually exclusive of the url field" — neither, or exactly one, is
|
|
6
|
+
* allowed, but not both.
|
|
7
|
+
*/
|
|
8
|
+
export const oasMutuallyExclusive = (input, options, context) => {
|
|
9
|
+
if (!isObject(input))
|
|
10
|
+
return [];
|
|
11
|
+
const present = (options?.properties ?? []).filter((property) => input[property] !== undefined);
|
|
12
|
+
if (present.length <= 1)
|
|
13
|
+
return [];
|
|
14
|
+
// The first present property is the "anchor"; flag every later one as the conflict.
|
|
15
|
+
const [anchor, ...conflicts] = present;
|
|
16
|
+
return conflicts.map((property) => ({
|
|
17
|
+
message: `"${property}" must not be used together with "${anchor}" (mutually exclusive)`,
|
|
18
|
+
path: [...context.path, property],
|
|
19
|
+
}));
|
|
20
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags a Schema Object's `nullable` keyword, removed in OpenAPI 3.1+ (JSON
|
|
4
|
+
* Schema 2020-12 uses a `"null"` type instead). Targets the *parent* of a
|
|
5
|
+
* `nullable` key (`$..nullable^`) rather than `$..nullable` directly, which fixes
|
|
6
|
+
* two problems with the naive `then: falsy` approach:
|
|
7
|
+
* - a property literally named `nullable` (`properties: { nullable: {...} }`)
|
|
8
|
+
* is no longer flagged, because there the `nullable` value is a Schema Object,
|
|
9
|
+
* not the boolean keyword, and
|
|
10
|
+
* - `nullable: false` is flagged too (a boolean of either value counts as the
|
|
11
|
+
* keyword being present), so the migration fixer can drop it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const oasNoNullable: RulesetFunction;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Flags a Schema Object's `nullable` keyword, removed in OpenAPI 3.1+ (JSON
|
|
4
|
+
* Schema 2020-12 uses a `"null"` type instead). Targets the *parent* of a
|
|
5
|
+
* `nullable` key (`$..nullable^`) rather than `$..nullable` directly, which fixes
|
|
6
|
+
* two problems with the naive `then: falsy` approach:
|
|
7
|
+
* - a property literally named `nullable` (`properties: { nullable: {...} }`)
|
|
8
|
+
* is no longer flagged, because there the `nullable` value is a Schema Object,
|
|
9
|
+
* not the boolean keyword, and
|
|
10
|
+
* - `nullable: false` is flagged too (a boolean of either value counts as the
|
|
11
|
+
* keyword being present), so the migration fixer can drop it.
|
|
12
|
+
*/
|
|
13
|
+
export const oasNoNullable = (parent, _options, context) => {
|
|
14
|
+
if (!isObject(parent) || typeof parent['nullable'] !== 'boolean')
|
|
15
|
+
return [];
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
message: 'nullable is not available in OpenAPI 3.1 or later; use a "null" type instead.',
|
|
19
|
+
path: [...context.path, 'nullable'],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/** Validates OpenAPI v2 formData operations declare a compatible `consumes`. */
|
|
3
|
+
export const oasOpFormDataConsumeCheck = (operation, _options, context) => {
|
|
4
|
+
if (!isObject(operation))
|
|
5
|
+
return [];
|
|
6
|
+
const params = Array.isArray(operation['parameters']) ? operation['parameters'] : [];
|
|
7
|
+
const hasFormData = params.some((param) => isObject(param) && param['in'] === 'formData');
|
|
8
|
+
if (!hasFormData)
|
|
9
|
+
return [];
|
|
10
|
+
const consumes = Array.isArray(operation['consumes']) ? operation['consumes'] : [];
|
|
11
|
+
const ok = consumes.some((type) => type === 'application/x-www-form-urlencoded' || type === 'multipart/form-data');
|
|
12
|
+
if (!ok) {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
message: 'Operations with formData parameters must consume application/x-www-form-urlencoded or multipart/form-data',
|
|
16
|
+
path: [...context.path, 'consumes'],
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
return [];
|
|
21
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isObject, OPERATION_METHODS } from './helpers.js';
|
|
2
|
+
/** Ensures `operationId` values are unique across the document. */
|
|
3
|
+
export const oasOpIdUnique = (paths, _options, context) => {
|
|
4
|
+
if (!isObject(paths))
|
|
5
|
+
return [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const [path, item] of Object.entries(paths)) {
|
|
9
|
+
if (!isObject(item))
|
|
10
|
+
continue;
|
|
11
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
12
|
+
// Only real operations carry an `operationId`; an `x-`extension object with
|
|
13
|
+
// an `operationId`-looking key would otherwise create a phantom duplicate.
|
|
14
|
+
if (!OPERATION_METHODS.has(method) || !isObject(operation))
|
|
15
|
+
continue;
|
|
16
|
+
const id = operation['operationId'];
|
|
17
|
+
if (typeof id !== 'string')
|
|
18
|
+
continue;
|
|
19
|
+
if (seen.has(id)) {
|
|
20
|
+
results.push({
|
|
21
|
+
message: `operationId "${id}" must be unique`,
|
|
22
|
+
path: [...context.path, path, method, 'operationId'],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
seen.add(id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RulesetFunction } from '../../../core/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates an operation's `parameters` array, mirroring Spectral's `oasOpParams`:
|
|
4
|
+
* flags duplicate `name`+`in` combinations, and — for OpenAPI 2.0, where the body
|
|
5
|
+
* is a parameter — flags more than one `in: body` parameter (even with different
|
|
6
|
+
* names) as well as mixing `in: body` with `in: formData`. `$ref` entries are
|
|
7
|
+
* skipped because their real `name`/`in` is only known after resolution.
|
|
8
|
+
*/
|
|
9
|
+
export declare const oasOpParams: RulesetFunction;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isObject } from './helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates an operation's `parameters` array, mirroring Spectral's `oasOpParams`:
|
|
4
|
+
* flags duplicate `name`+`in` combinations, and — for OpenAPI 2.0, where the body
|
|
5
|
+
* is a parameter — flags more than one `in: body` parameter (even with different
|
|
6
|
+
* names) as well as mixing `in: body` with `in: formData`. `$ref` entries are
|
|
7
|
+
* skipped because their real `name`/`in` is only known after resolution.
|
|
8
|
+
*/
|
|
9
|
+
export const oasOpParams = (params, _options, context) => {
|
|
10
|
+
if (!Array.isArray(params))
|
|
11
|
+
return [];
|
|
12
|
+
const results = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const bodyIndices = [];
|
|
15
|
+
const formDataIndices = [];
|
|
16
|
+
params.forEach((param, index) => {
|
|
17
|
+
if (!isObject(param) || '$ref' in param)
|
|
18
|
+
return;
|
|
19
|
+
if (param['name'] !== undefined && param['in'] !== undefined) {
|
|
20
|
+
const key = `${String(param['in'])}:${String(param['name'])}`;
|
|
21
|
+
if (seen.has(key)) {
|
|
22
|
+
results.push({
|
|
23
|
+
message: `Duplicate parameter "${String(param['name'])}" in "${String(param['in'])}"`,
|
|
24
|
+
path: [...context.path, index],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
seen.add(key);
|
|
28
|
+
}
|
|
29
|
+
if (param['in'] === 'body')
|
|
30
|
+
bodyIndices.push(index);
|
|
31
|
+
else if (param['in'] === 'formData')
|
|
32
|
+
formDataIndices.push(index);
|
|
33
|
+
});
|
|
34
|
+
// OAS2 allows at most one body parameter, and body and formData are mutually
|
|
35
|
+
// exclusive within a single operation.
|
|
36
|
+
if (bodyIndices.length > 0 && formDataIndices.length > 0) {
|
|
37
|
+
results.push({ message: 'Operation must not have both "in:body" and "in:formData" parameters' });
|
|
38
|
+
}
|
|
39
|
+
for (let i = 1; i < bodyIndices.length; i++) {
|
|
40
|
+
results.push({
|
|
41
|
+
message: 'Operation must not have more than a single instance of the "in:body" parameter',
|
|
42
|
+
path: [...context.path, bodyIndices[i]],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
};
|