@amritk/lint 0.1.0 → 0.2.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.
Files changed (62) hide show
  1. package/README.md +34 -1
  2. package/dist/rules/openapi/fixers.d.ts +7 -0
  3. package/dist/rules/openapi/fixers.js +189 -0
  4. package/dist/rules/openapi/formats.d.ts +13 -0
  5. package/dist/rules/openapi/formats.js +23 -0
  6. package/dist/rules/openapi/functions/example-validation.d.ts +5 -0
  7. package/dist/rules/openapi/functions/example-validation.js +97 -0
  8. package/dist/rules/openapi/functions/helpers.d.ts +3 -0
  9. package/dist/rules/openapi/functions/helpers.js +5 -0
  10. package/dist/rules/openapi/functions/index.d.ts +24 -0
  11. package/dist/rules/openapi/functions/index.js +67 -0
  12. package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
  13. package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
  14. package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
  15. package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
  16. package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
  17. package/dist/rules/openapi/functions/oas-example-value.js +31 -0
  18. package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
  19. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
  20. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
  21. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
  22. package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
  23. package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -0
  24. package/dist/rules/openapi/functions/oas-op-params.d.ts +3 -0
  25. package/dist/rules/openapi/functions/oas-op-params.js +21 -0
  26. package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
  27. package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
  28. package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
  29. package/dist/rules/openapi/functions/oas-op-success-response.js +11 -0
  30. package/dist/rules/openapi/functions/oas-path-param.d.ts +3 -0
  31. package/dist/rules/openapi/functions/oas-path-param.js +45 -0
  32. package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
  33. package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
  34. package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
  35. package/dist/rules/openapi/functions/oas-schema.js +15 -0
  36. package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
  37. package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
  38. package/dist/rules/openapi/functions/oas-server-variables.d.ts +3 -0
  39. package/dist/rules/openapi/functions/oas-server-variables.js +23 -0
  40. package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
  41. package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
  42. package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
  43. package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
  44. package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
  45. package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
  46. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
  47. package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
  48. package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
  49. package/dist/rules/openapi/functions/oas-unused-component.js +52 -0
  50. package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
  51. package/dist/rules/openapi/functions/ref-siblings.js +13 -0
  52. package/dist/rules/openapi/index.d.ts +25 -0
  53. package/dist/rules/openapi/index.js +127 -0
  54. package/dist/rules/openapi/oas.d.ts +3 -0
  55. package/dist/rules/openapi/oas.js +492 -0
  56. package/dist/rules/openapi/schemas/index.d.ts +8 -0
  57. package/dist/rules/openapi/schemas/index.js +36 -0
  58. package/dist/rules/openapi/schemas/oas20.json +1592 -0
  59. package/dist/rules/openapi/schemas/oas30.json +1651 -0
  60. package/dist/rules/openapi/schemas/oas31.json +1412 -0
  61. package/dist/rules/openapi/schemas/oas32.json +1684 -0
  62. package/package.json +6 -2
package/README.md CHANGED
@@ -19,7 +19,9 @@
19
19
 
20
20
  `@amritk/lint` lints **any** JSON or YAML document against a ruleset you define. A rule matches nodes with a **JSONPath** (`given`) and runs a **function** (`then`) over each match — structural validation against a **JSON Schema**, style checks (`casing`, `pattern`, `alphabetical`, `length`, …), or your own custom function. Every finding carries an exact `line:column` range, because the parser keeps source positions on every node.
21
21
 
22
- It is **format-agnostic**: the engine ships no built-in ruleset and knows nothing about OpenAPI or any other schema — you bring the rules. This is JSON/YAML style-guide linting with JSON Schema and custom rules, and nothing else.
22
+ It is **format-agnostic**: the core engine ships no built-in ruleset and knows nothing about OpenAPI or any other schema — you bring the rules. This is JSON/YAML style-guide linting with JSON Schema and custom rules at its core.
23
+
24
+ For OpenAPI specifically, the `@amritk/lint/rules/openapi` subpath ships a ready-made preset on top of that engine — see [OpenAPI ruleset](#openapi-ruleset) below.
23
25
 
24
26
  The CLI lives in the [`mjst`](../cli) binary as `mjst lint`; this package is the programmatic library behind it.
25
27
 
@@ -181,6 +183,37 @@ The engine internals (`createDocument`, `lint`, `query`, `validateRuleset`, `par
181
183
 
182
184
  ---
183
185
 
186
+ ## OpenAPI ruleset
187
+
188
+ The core package is format-agnostic, but OpenAPI is common enough to ship a ready-made preset. It lives at the **`@amritk/lint/rules/openapi`** subpath — a self-contained layer on top of the engine that adds **no dependencies** beyond what `@amritk/lint` already uses.
189
+
190
+ ```ts
191
+ import { lint } from '@amritk/lint'
192
+ import { createOpenApiRuleset } from '@amritk/lint/rules/openapi'
193
+
194
+ // Defaults to `extends: [oas]` (recommended rules only, like `spectral:oas`).
195
+ const ruleset = createOpenApiRuleset()
196
+ const findings = await lint(spec, { ruleset })
197
+ ```
198
+
199
+ `createOpenApiRuleset(definition?, basePath?)` builds a runnable `Ruleset` with the OpenAPI functions and format detectors layered over the built-ins, and with `extends` resolution that understands the `oas` / `loupe:oas` / `spectral:oas` names (the last two accepted so existing Spectral-style rulesets extend unchanged). Enable every rule with `createOpenApiRuleset({ extends: [['oas', 'all']] })`, or pass your own definition to override severities, add rules, or point `extends` at a file/npm package.
200
+
201
+ | Export | What it does |
202
+ | --- | --- |
203
+ | `createOpenApiRuleset(definition?, basePath?)` | Build a runnable OpenAPI `Ruleset` (functions + formats + `extends` resolution). |
204
+ | `resolveOpenApiRuleset(name, basePath?)` | Resolve an `extends` reference, including the `oas` / `loupe:oas` / `spectral:oas` names. |
205
+ | `oas` | The built-in OpenAPI ruleset definition. |
206
+ | `oasFunctions` / `allFunctions` | The OpenAPI-specific functions; `allFunctions` = built-ins + OpenAPI. |
207
+ | `oasFormats` | OpenAPI version detectors (`oas2`, `oas3`, `oas3.0`, `oas3.1`, `oas3.2`). |
208
+ | `oasFixers` | Auto-fixers for the mechanically-repairable OpenAPI rules (pass to `fixDocument`). |
209
+ | `loadOasSchema(version)` | Lazily load one OpenAPI version's official structural meta-schema (`'2.0'` / `'3.0'` / `'3.1'` / `'3.2'`), vendored as raw `.json` from `spec.openapis.org` (3.0/3.1/3.2 verbatim; 2.0 with its external draft-04 metaschema refs inlined). See [`schemas/README.md`](./src/rules/openapi/schemas/README.md). |
210
+
211
+ The structural rules validate against the **official `spec.openapis.org` meta-schemas, vendored as raw `.json`** ([`schemas/`](./src/rules/openapi/schemas/)). 3.0/3.1/3.2 are byte-for-byte verbatim; only 2.0 differs (its external draft-04 metaschema refs are inlined, since the offline interpreter never fetches remote refs). OpenAPI 3.1/3.2 express Schema Objects as JSON Schema 2020-12 via a local `$dynamicRef`/`$dynamicAnchor`, which `@amritk/runtime-validators` resolves natively — so the whole document envelope is validated against the official schema with no bundling or dialect engine, while Schema Object internals stay permissive.
212
+
213
+ `$ref` resolution stays the caller's job: the preset doesn't pull in a resolver, so for rules that need the dereferenced document (`resolved: true`) pass a `resolve` function to the core `lintWithResult` (for example wrapping [`@amritk/resolve-refs`](../resolve-refs)). The `mjst lint` CLI already wires one up.
214
+
215
+ ---
216
+
184
217
  ## License
185
218
 
186
219
  MIT
@@ -0,0 +1,7 @@
1
+ import type { FixerRegistry } from '../../fix/index.js';
2
+ /**
3
+ * Auto-fixers for the mechanically-repairable OpenAPI rules, keyed by rule
4
+ * code. Pass these to `@amritk/lint`'s `fixDocument` (as its `fixers`), or wrap
5
+ * them with `createFixPlugin` for a lower-level plugin.
6
+ */
7
+ export declare const oasFixers: FixerRegistry;
@@ -0,0 +1,189 @@
1
+ /** Reads the value at `path` in the parsed document, or `undefined` if absent. */
2
+ const getAtPath = (data, path) => {
3
+ let current = data;
4
+ for (const segment of path) {
5
+ if (current == null || typeof current !== 'object')
6
+ return undefined;
7
+ current = current[segment];
8
+ }
9
+ return current;
10
+ };
11
+ const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
12
+ const stripTrailingSlash = (value) => value.replace(/\/+$/, '');
13
+ /**
14
+ * `oas2-host-trailing-slash` / `oas3-server-trailing-slash`: drop the trailing
15
+ * slash from a string value (host or server URL).
16
+ */
17
+ const trailingSlashValue = {
18
+ safe: true,
19
+ fix: ({ diagnostic, data }) => {
20
+ const value = getAtPath(data, diagnostic.path);
21
+ if (typeof value !== 'string')
22
+ return undefined;
23
+ const stripped = stripTrailingSlash(value);
24
+ if (stripped === value || stripped === '')
25
+ return undefined;
26
+ return { op: 'setValue', path: diagnostic.path, value: stripped };
27
+ },
28
+ };
29
+ /** `path-keys-no-trailing-slash`: rename a `paths` key to drop its trailing slash. */
30
+ const pathKeyTrailingSlash = {
31
+ safe: true,
32
+ fix: ({ diagnostic }) => {
33
+ const key = diagnostic.path[diagnostic.path.length - 1];
34
+ if (typeof key !== 'string')
35
+ return undefined;
36
+ const stripped = stripTrailingSlash(key);
37
+ if (stripped === key || stripped === '')
38
+ return undefined;
39
+ return { op: 'renameProperty', path: diagnostic.path, newKey: stripped };
40
+ },
41
+ };
42
+ /** `no-$ref-siblings`: delete the sibling key that sits next to a `$ref`. */
43
+ const refSibling = {
44
+ safe: true,
45
+ fix: ({ diagnostic }) => {
46
+ if (diagnostic.path.length === 0)
47
+ return undefined;
48
+ return { op: 'removeProperty', path: diagnostic.path };
49
+ },
50
+ };
51
+ /** `duplicated-entry-in-enum`: remove the later copies of each repeated enum value. */
52
+ const duplicatedEnum = {
53
+ safe: true,
54
+ fix: ({ diagnostic, data }) => {
55
+ const array = getAtPath(data, diagnostic.path);
56
+ if (!Array.isArray(array))
57
+ return undefined;
58
+ const seen = new Set();
59
+ const duplicates = [];
60
+ array.forEach((item, index) => {
61
+ const key = JSON.stringify(item);
62
+ if (seen.has(key))
63
+ duplicates.push(index);
64
+ else
65
+ seen.add(key);
66
+ });
67
+ if (duplicates.length === 0)
68
+ return undefined;
69
+ return { op: 'removeItems', path: diagnostic.path, indices: duplicates };
70
+ },
71
+ };
72
+ /** `openapi-tags-alphabetical`: reorder the top-level `tags` array by `name`. */
73
+ const tagsAlphabetical = {
74
+ safe: true,
75
+ fix: ({ diagnostic, data }) => {
76
+ // Findings point at the out-of-order item; the array is its parent.
77
+ const arrayPath = diagnostic.path.slice(0, -1);
78
+ const array = getAtPath(data, arrayPath);
79
+ if (!Array.isArray(array))
80
+ return undefined;
81
+ const nameOf = (item) => item != null && typeof item === 'object' ? String(item['name']) : String(item);
82
+ const order = array.map((_, index) => index).sort((a, b) => nameOf(array[a]).localeCompare(nameOf(array[b])));
83
+ if (order.every((value, index) => value === index))
84
+ return undefined;
85
+ return { op: 'reorderArray', path: arrayPath, order };
86
+ },
87
+ };
88
+ /** `path-not-include-query`: drop the `?query` portion from a `paths` key. */
89
+ const pathKeyQueryString = {
90
+ safe: true,
91
+ fix: ({ diagnostic, data }) => {
92
+ const key = diagnostic.path[diagnostic.path.length - 1];
93
+ if (typeof key !== 'string')
94
+ return undefined;
95
+ const stripped = key.replace(/\?.*$/, '');
96
+ if (stripped === key || stripped === '')
97
+ return undefined;
98
+ // Renaming onto an existing path would collide and silently drop a path, so skip.
99
+ const paths = getAtPath(data, diagnostic.path.slice(0, -1));
100
+ if (isObject(paths) && stripped in paths)
101
+ return undefined;
102
+ return { op: 'renameProperty', path: diagnostic.path, newKey: stripped };
103
+ },
104
+ };
105
+ /** `openapi-tags-uniqueness`: remove the later copies of each repeated tag name. */
106
+ const tagsUnique = {
107
+ safe: true,
108
+ fix: ({ diagnostic, data }) => {
109
+ // Findings point at `tags[index].name`, so the array sits two segments up.
110
+ const arrayPath = diagnostic.path.slice(0, -2);
111
+ const array = getAtPath(data, arrayPath);
112
+ if (!Array.isArray(array))
113
+ return undefined;
114
+ const seen = new Set();
115
+ const duplicates = [];
116
+ array.forEach((tag, index) => {
117
+ const name = isObject(tag) ? tag['name'] : undefined;
118
+ if (typeof name !== 'string')
119
+ return;
120
+ if (seen.has(name))
121
+ duplicates.push(index);
122
+ else
123
+ seen.add(name);
124
+ });
125
+ if (duplicates.length === 0)
126
+ return undefined;
127
+ return { op: 'removeItems', path: arrayPath, indices: duplicates };
128
+ },
129
+ };
130
+ /**
131
+ * `oas3-unused-component` / `oas2-unused-definition`: delete the unreferenced
132
+ * component. Marked unsafe because a component can be referenced from another
133
+ * document or resolved dynamically, so removing it may not be semantics-preserving
134
+ * — it only applies under `--fix-unsafe`.
135
+ */
136
+ const unusedComponent = {
137
+ safe: false,
138
+ fix: ({ diagnostic }) => {
139
+ if (diagnostic.path.length === 0)
140
+ return undefined;
141
+ return { op: 'removeProperty', path: diagnostic.path };
142
+ },
143
+ };
144
+ /**
145
+ * `oas3_1-no-nullable`: migrate the 3.0-era `nullable` keyword to its OpenAPI
146
+ * 3.1 (JSON Schema 2020-12) equivalent. `nullable: false` is the schema default,
147
+ * so it is simply dropped; `nullable: true` folds a `"null"` member into the
148
+ * sibling `type` before the keyword is removed. Marked unsafe because it rewrites
149
+ * the schema's `type`, so it only applies under `--fix-unsafe`.
150
+ */
151
+ const noNullable = {
152
+ safe: false,
153
+ fix: ({ diagnostic, data }) => {
154
+ const value = getAtPath(data, diagnostic.path);
155
+ const remove = { op: 'removeProperty', path: diagnostic.path };
156
+ // `nullable: false` (or any non-true value) is the default — drop the keyword.
157
+ if (value !== true)
158
+ return remove;
159
+ const typePath = [...diagnostic.path.slice(0, -1), 'type'];
160
+ const type = getAtPath(data, typePath);
161
+ if (typeof type === 'string' && type !== 'null') {
162
+ return [{ op: 'setValue', path: typePath, value: [type, 'null'] }, remove];
163
+ }
164
+ if (Array.isArray(type) && !type.includes('null')) {
165
+ return [{ op: 'insertItem', path: typePath, value: 'null' }, remove];
166
+ }
167
+ // No type to widen (the schema already permits any value, null included) or
168
+ // `null` is already allowed — removing the keyword is enough.
169
+ return remove;
170
+ },
171
+ };
172
+ /**
173
+ * Auto-fixers for the mechanically-repairable OpenAPI rules, keyed by rule
174
+ * code. Pass these to `@amritk/lint`'s `fixDocument` (as its `fixers`), or wrap
175
+ * them with `createFixPlugin` for a lower-level plugin.
176
+ */
177
+ export const oasFixers = {
178
+ 'oas2-host-trailing-slash': trailingSlashValue,
179
+ 'oas3-server-trailing-slash': trailingSlashValue,
180
+ 'path-keys-no-trailing-slash': pathKeyTrailingSlash,
181
+ 'path-not-include-query': pathKeyQueryString,
182
+ 'no-$ref-siblings': refSibling,
183
+ 'duplicated-entry-in-enum': duplicatedEnum,
184
+ 'openapi-tags-alphabetical': tagsAlphabetical,
185
+ 'openapi-tags-uniqueness': tagsUnique,
186
+ 'oas3-unused-component': unusedComponent,
187
+ 'oas2-unused-definition': unusedComponent,
188
+ 'oas3_1-no-nullable': noNullable,
189
+ };
@@ -0,0 +1,13 @@
1
+ import type { Format } from '../../core/index.js';
2
+ /** Matches OpenAPI/Swagger 2.0 (`swagger: "2.0"`). */
3
+ export declare const oas2: Format;
4
+ /** Matches any OpenAPI 3.x (`openapi: 3.*`). */
5
+ export declare const oas3: Format;
6
+ /** Matches OpenAPI 3.0.x specifically. */
7
+ export declare const oas3_0: Format;
8
+ /** Matches OpenAPI 3.1.x specifically. */
9
+ export declare const oas3_1: Format;
10
+ /** Matches OpenAPI 3.2.x specifically. */
11
+ export declare const oas3_2: Format;
12
+ /** OpenAPI format detectors keyed by Loupe-compatible names. */
13
+ export declare const oasFormats: Record<string, Format>;
@@ -0,0 +1,23 @@
1
+ const isObject = (value) => typeof value === 'object' && value !== null;
2
+ const openapiVersion = (document) => isObject(document) && typeof document['openapi'] === 'string' ? document['openapi'] : undefined;
3
+ /** Matches OpenAPI/Swagger 2.0 (`swagger: "2.0"`). */
4
+ export const oas2 = (document) => isObject(document) && document['swagger'] === '2.0';
5
+ /** Matches any OpenAPI 3.x (`openapi: 3.*`). */
6
+ export const oas3 = (document) => openapiVersion(document)?.startsWith('3.') ?? false;
7
+ /** Matches OpenAPI 3.0.x specifically. */
8
+ export const oas3_0 = (document) => openapiVersion(document)?.startsWith('3.0') ?? false;
9
+ /** Matches OpenAPI 3.1.x specifically. */
10
+ export const oas3_1 = (document) => openapiVersion(document)?.startsWith('3.1') ?? false;
11
+ /** Matches OpenAPI 3.2.x specifically. */
12
+ export const oas3_2 = (document) => openapiVersion(document)?.startsWith('3.2') ?? false;
13
+ /** OpenAPI format detectors keyed by Loupe-compatible names. */
14
+ export const oasFormats = {
15
+ oas2,
16
+ oas3,
17
+ 'oas3.0': oas3_0,
18
+ oas3_0,
19
+ 'oas3.1': oas3_1,
20
+ oas3_1,
21
+ 'oas3.2': oas3_2,
22
+ oas3_2,
23
+ };
@@ -0,0 +1,5 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /** Validates a schema object's inline `example` against the schema itself. */
3
+ export declare const oasSchemaExample: RulesetFunction;
4
+ /** Validates media type / parameter `example` and `examples` against the schema. */
5
+ export declare const oasMediaExample: RulesetFunction;
@@ -0,0 +1,97 @@
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
+ const buildValidator = (schema) => {
11
+ if (!isObject(schema))
12
+ return undefined;
13
+ try {
14
+ return validate(schema);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ };
20
+ const buildValidatorOrNull = (schema) => buildValidator(schema) ?? null;
21
+ // Keyed by the Schema Object node; `oasSchemaExample` validates a schema's own
22
+ // `example` against the schema minus its `example`/`examples` keywords.
23
+ const schemaExampleResults = new WeakMap();
24
+ // Keyed by the Media Type Object node; `oasMediaExample` validates the media's
25
+ // `example`/`examples` against the media's `schema`. The node (not the schema) is
26
+ // the cache key because two media objects can share a `schema` but carry different
27
+ // examples.
28
+ const mediaExampleResults = new WeakMap();
29
+ const withPath = (findings, path) => {
30
+ if (findings.length === 0)
31
+ return [];
32
+ return findings.map((finding) => ({ message: finding.message, path: [...path, ...finding.suffix] }));
33
+ };
34
+ /** Validates a schema object's inline `example` against the schema itself. */
35
+ export const oasSchemaExample = (schema, _options, context) => {
36
+ if (!isObject(schema) || schema['example'] === undefined)
37
+ return [];
38
+ let findings = schemaExampleResults.get(schema);
39
+ if (findings === undefined) {
40
+ findings = [];
41
+ const { example, examples, ...rest } = schema;
42
+ void example;
43
+ void examples;
44
+ const check = buildValidatorOrNull(rest);
45
+ if (check) {
46
+ const result = check(schema['example']);
47
+ if (result !== true) {
48
+ for (const error of result.errors)
49
+ findings.push({ message: `"example" ${error.message}`.trim(), suffix: ['example'] });
50
+ }
51
+ }
52
+ schemaExampleResults.set(schema, findings);
53
+ }
54
+ return withPath(findings, context.path);
55
+ };
56
+ /** Validates media type / parameter `example` and `examples` against the schema. */
57
+ export const oasMediaExample = (media, _options, context) => {
58
+ if (!isObject(media) || !isObject(media['schema']))
59
+ return [];
60
+ // Skip building a validator when there is no example to check (most media
61
+ // objects in a large spec have a schema but no example).
62
+ const hasExample = media['example'] !== undefined || isObject(media['examples']);
63
+ if (!hasExample)
64
+ return [];
65
+ let findings = mediaExampleResults.get(media);
66
+ if (findings === undefined) {
67
+ findings = [];
68
+ const check = buildValidatorOrNull(media['schema']);
69
+ if (check) {
70
+ if (media['example'] !== undefined) {
71
+ const result = check(media['example']);
72
+ if (result !== true) {
73
+ for (const error of result.errors)
74
+ findings.push({ message: `"example" ${error.message}`.trim(), suffix: ['example'] });
75
+ }
76
+ }
77
+ const examples = isObject(media['examples']) ? media['examples'] : undefined;
78
+ if (examples) {
79
+ for (const [name, example] of Object.entries(examples)) {
80
+ if (isObject(example) && example['value'] !== undefined) {
81
+ const result = check(example['value']);
82
+ if (result !== true) {
83
+ for (const error of result.errors) {
84
+ findings.push({
85
+ message: `Example "${name}" ${error.message}`.trim(),
86
+ suffix: ['examples', name, 'value'],
87
+ });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ mediaExampleResults.set(media, findings);
95
+ }
96
+ return withPath(findings, context.path);
97
+ };
@@ -0,0 +1,3 @@
1
+ /** True for a non-null, non-array object (an OpenAPI "object" value). */
2
+ export declare const isObject: (value: unknown) => value is Record<string, unknown>;
3
+ export declare const HTTP_METHODS: Set<string>;
@@ -0,0 +1,5 @@
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']);
@@ -0,0 +1,24 @@
1
+ import type { FunctionRegistry } from '../../../core/index.js';
2
+ export { oasMediaExample, oasSchemaExample } from './example-validation.js';
3
+ export { oasAdditionalOperations } from './oas-additional-operations.js';
4
+ export { oasDiscriminator } from './oas-discriminator.js';
5
+ export { oasExampleValue } from './oas-example-value.js';
6
+ export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
7
+ export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
8
+ export { oasOpIdUnique } from './oas-op-id-unique.js';
9
+ export { oasOpParams } from './oas-op-params.js';
10
+ export { oasOpSecurityDefined } from './oas-op-security-defined.js';
11
+ export { oasOpSuccessResponse } from './oas-op-success-response.js';
12
+ export { oasPathParam } from './oas-path-param.js';
13
+ export { type IOasSchemaOptions, oasSchema } from './oas-schema.js';
14
+ export { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
15
+ export { oasServerNameUnique } from './oas-server-name-unique.js';
16
+ export { oasServerVariables } from './oas-server-variables.js';
17
+ export { oasTagDefined } from './oas-tag-defined.js';
18
+ export { oasTagKind } from './oas-tag-kind.js';
19
+ export { oasTagParentDefined } from './oas-tag-parent-defined.js';
20
+ export { oasTagsUnique } from './oas-tags-unique.js';
21
+ export { oasUnusedComponent } from './oas-unused-component.js';
22
+ export { refSiblings } from './ref-siblings.js';
23
+ /** The OpenAPI-specific custom functions, keyed by name for ruleset `then` references. */
24
+ export declare const oasFunctions: FunctionRegistry;
@@ -0,0 +1,67 @@
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 { oasExampleValue } from './oas-example-value.js';
5
+ import { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
6
+ import { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
7
+ import { oasOpIdUnique } from './oas-op-id-unique.js';
8
+ import { oasOpParams } from './oas-op-params.js';
9
+ import { oasOpSecurityDefined } from './oas-op-security-defined.js';
10
+ import { oasOpSuccessResponse } from './oas-op-success-response.js';
11
+ import { oasPathParam } from './oas-path-param.js';
12
+ import { oasSchema } from './oas-schema.js';
13
+ import { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
14
+ import { oasServerNameUnique } from './oas-server-name-unique.js';
15
+ import { oasServerVariables } from './oas-server-variables.js';
16
+ import { oasTagDefined } from './oas-tag-defined.js';
17
+ import { oasTagKind } from './oas-tag-kind.js';
18
+ import { oasTagParentDefined } from './oas-tag-parent-defined.js';
19
+ import { oasTagsUnique } from './oas-tags-unique.js';
20
+ import { oasUnusedComponent } from './oas-unused-component.js';
21
+ import { refSiblings } from './ref-siblings.js';
22
+ export { oasMediaExample, oasSchemaExample } from './example-validation.js';
23
+ export { oasAdditionalOperations } from './oas-additional-operations.js';
24
+ export { oasDiscriminator } from './oas-discriminator.js';
25
+ export { oasExampleValue } from './oas-example-value.js';
26
+ export { oasMutuallyExclusive } from './oas-mutually-exclusive.js';
27
+ export { oasOpFormDataConsumeCheck } from './oas-op-form-data-consume-check.js';
28
+ export { oasOpIdUnique } from './oas-op-id-unique.js';
29
+ export { oasOpParams } from './oas-op-params.js';
30
+ export { oasOpSecurityDefined } from './oas-op-security-defined.js';
31
+ export { oasOpSuccessResponse } from './oas-op-success-response.js';
32
+ export { oasPathParam } from './oas-path-param.js';
33
+ export { oasSchema } from './oas-schema.js';
34
+ export { oasSchemaExampleDeprecated } from './oas-schema-example-deprecated.js';
35
+ export { oasServerNameUnique } from './oas-server-name-unique.js';
36
+ export { oasServerVariables } from './oas-server-variables.js';
37
+ export { oasTagDefined } from './oas-tag-defined.js';
38
+ export { oasTagKind } from './oas-tag-kind.js';
39
+ export { oasTagParentDefined } from './oas-tag-parent-defined.js';
40
+ export { oasTagsUnique } from './oas-tags-unique.js';
41
+ export { oasUnusedComponent } from './oas-unused-component.js';
42
+ export { refSiblings } from './ref-siblings.js';
43
+ /** The OpenAPI-specific custom functions, keyed by name for ruleset `then` references. */
44
+ export const oasFunctions = {
45
+ refSiblings,
46
+ oasOpSuccessResponse,
47
+ oasTagDefined,
48
+ oasOpIdUnique,
49
+ oasPathParam,
50
+ oasOpParams,
51
+ oasTagsUnique,
52
+ oasOpSecurityDefined: oasOpSecurityDefined,
53
+ oasOpFormDataConsumeCheck,
54
+ oasDiscriminator,
55
+ oasServerVariables,
56
+ oasSchemaExample,
57
+ oasMediaExample,
58
+ oasUnusedComponent,
59
+ oasMutuallyExclusive: oasMutuallyExclusive,
60
+ oasSchema: oasSchema,
61
+ oasAdditionalOperations,
62
+ oasServerNameUnique,
63
+ oasTagParentDefined,
64
+ oasSchemaExampleDeprecated,
65
+ oasTagKind,
66
+ oasExampleValue,
67
+ };
@@ -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 { HTTP_METHODS, isObject } from './helpers.js';
2
+ // HTTP methods that have a dedicated fixed field on the Path Item Object. In
3
+ // OpenAPI 3.2 `query` joined the original eight, and these MUST NOT be redefined
4
+ // inside the new `additionalOperations` map (which is for non-standard methods).
5
+ const FIXED_PATH_ITEM_METHODS = new Set([...HTTP_METHODS, 'query']);
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,3 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /** Validates a v2 discriminator references a required property. */
3
+ export declare const oasDiscriminator: RulesetFunction;
@@ -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,3 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /** Flags forbidden field combinations on an OpenAPI 3.2 Example Object. */
3
+ export declare const oasExampleValue: RulesetFunction;
@@ -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,3 @@
1
+ import type { RulesetFunction } from '../../../core/index.js';
2
+ /** Validates OpenAPI v2 formData operations declare a compatible `consumes`. */
3
+ export declare const oasOpFormDataConsumeCheck: RulesetFunction;