@amritk/lint 0.0.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 (136) hide show
  1. package/README.md +34 -1
  2. package/dist/core/document.d.ts +19 -0
  3. package/dist/core/document.js +11 -0
  4. package/dist/core/formats.d.ts +9 -0
  5. package/dist/core/formats.js +14 -0
  6. package/dist/core/glob.d.ts +4 -0
  7. package/dist/core/glob.js +48 -0
  8. package/dist/core/index.d.ts +11 -0
  9. package/dist/core/index.js +11 -0
  10. package/dist/core/jsonpath.d.ts +59 -0
  11. package/dist/core/jsonpath.js +415 -0
  12. package/dist/core/lint.d.ts +57 -0
  13. package/dist/core/lint.js +82 -0
  14. package/dist/core/plugin.d.ts +59 -0
  15. package/dist/core/plugin.js +25 -0
  16. package/dist/core/pointers.d.ts +35 -0
  17. package/dist/core/pointers.js +169 -0
  18. package/dist/core/ruleset.d.ts +52 -0
  19. package/dist/core/ruleset.js +170 -0
  20. package/dist/core/runner.d.ts +21 -0
  21. package/dist/core/runner.js +222 -0
  22. package/dist/core/types.d.ts +170 -0
  23. package/dist/core/types.js +1 -0
  24. package/dist/core/validate-ruleset.d.ts +14 -0
  25. package/dist/core/validate-ruleset.js +105 -0
  26. package/dist/fix/apply.d.ts +21 -0
  27. package/dist/fix/apply.js +51 -0
  28. package/dist/fix/index.d.ts +3 -0
  29. package/dist/fix/index.js +2 -0
  30. package/dist/fix/plugin.d.ts +18 -0
  31. package/dist/fix/plugin.js +21 -0
  32. package/dist/fix/types.d.ts +39 -0
  33. package/dist/fix/types.js +0 -0
  34. package/dist/functions/alphabetical.d.ts +8 -0
  35. package/dist/functions/alphabetical.js +28 -0
  36. package/dist/functions/casing.d.ts +14 -0
  37. package/dist/functions/casing.js +30 -0
  38. package/dist/functions/defined.d.ts +3 -0
  39. package/dist/functions/defined.js +6 -0
  40. package/dist/functions/enumeration.d.ts +5 -0
  41. package/dist/functions/enumeration.js +10 -0
  42. package/dist/functions/falsy.d.ts +3 -0
  43. package/dist/functions/falsy.js +6 -0
  44. package/dist/functions/index.d.ts +16 -0
  45. package/dist/functions/index.js +42 -0
  46. package/dist/functions/length.d.ts +6 -0
  47. package/dist/functions/length.js +27 -0
  48. package/dist/functions/pattern.d.ts +6 -0
  49. package/dist/functions/pattern.js +20 -0
  50. package/dist/functions/schema.d.ts +8 -0
  51. package/dist/functions/schema.js +36 -0
  52. package/dist/functions/truthy.d.ts +3 -0
  53. package/dist/functions/truthy.js +6 -0
  54. package/dist/functions/typed-enum.d.ts +3 -0
  55. package/dist/functions/typed-enum.js +34 -0
  56. package/dist/functions/undefined.d.ts +6 -0
  57. package/dist/functions/undefined.js +9 -0
  58. package/dist/functions/unreferenced-reusable-object.d.ts +8 -0
  59. package/dist/functions/unreferenced-reusable-object.js +37 -0
  60. package/dist/functions/xor.d.ts +7 -0
  61. package/dist/functions/xor.js +11 -0
  62. package/dist/index.d.ts +89 -0
  63. package/dist/index.js +168 -0
  64. package/dist/parsers/edit-model.d.ts +69 -0
  65. package/dist/parsers/edit-model.js +326 -0
  66. package/dist/parsers/index.d.ts +18 -0
  67. package/dist/parsers/index.js +21 -0
  68. package/dist/parsers/json.d.ts +3 -0
  69. package/dist/parsers/json.js +38 -0
  70. package/dist/parsers/lines.d.ts +13 -0
  71. package/dist/parsers/lines.js +28 -0
  72. package/dist/parsers/types.d.ts +50 -0
  73. package/dist/parsers/types.js +8 -0
  74. package/dist/parsers/yaml.d.ts +6 -0
  75. package/dist/parsers/yaml.js +65 -0
  76. package/dist/rules/openapi/fixers.d.ts +7 -0
  77. package/dist/rules/openapi/fixers.js +189 -0
  78. package/dist/rules/openapi/formats.d.ts +13 -0
  79. package/dist/rules/openapi/formats.js +23 -0
  80. package/dist/rules/openapi/functions/example-validation.d.ts +5 -0
  81. package/dist/rules/openapi/functions/example-validation.js +97 -0
  82. package/dist/rules/openapi/functions/helpers.d.ts +3 -0
  83. package/dist/rules/openapi/functions/helpers.js +5 -0
  84. package/dist/rules/openapi/functions/index.d.ts +24 -0
  85. package/dist/rules/openapi/functions/index.js +67 -0
  86. package/dist/rules/openapi/functions/oas-additional-operations.d.ts +8 -0
  87. package/dist/rules/openapi/functions/oas-additional-operations.js +25 -0
  88. package/dist/rules/openapi/functions/oas-discriminator.d.ts +3 -0
  89. package/dist/rules/openapi/functions/oas-discriminator.js +23 -0
  90. package/dist/rules/openapi/functions/oas-example-value.d.ts +3 -0
  91. package/dist/rules/openapi/functions/oas-example-value.js +31 -0
  92. package/dist/rules/openapi/functions/oas-mutually-exclusive.d.ts +10 -0
  93. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +20 -0
  94. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.d.ts +3 -0
  95. package/dist/rules/openapi/functions/oas-op-form-data-consume-check.js +21 -0
  96. package/dist/rules/openapi/functions/oas-op-id-unique.d.ts +3 -0
  97. package/dist/rules/openapi/functions/oas-op-id-unique.js +27 -0
  98. package/dist/rules/openapi/functions/oas-op-params.d.ts +3 -0
  99. package/dist/rules/openapi/functions/oas-op-params.js +21 -0
  100. package/dist/rules/openapi/functions/oas-op-security-defined.d.ts +5 -0
  101. package/dist/rules/openapi/functions/oas-op-security-defined.js +44 -0
  102. package/dist/rules/openapi/functions/oas-op-success-response.d.ts +3 -0
  103. package/dist/rules/openapi/functions/oas-op-success-response.js +11 -0
  104. package/dist/rules/openapi/functions/oas-path-param.d.ts +3 -0
  105. package/dist/rules/openapi/functions/oas-path-param.js +45 -0
  106. package/dist/rules/openapi/functions/oas-schema-example-deprecated.d.ts +10 -0
  107. package/dist/rules/openapi/functions/oas-schema-example-deprecated.js +42 -0
  108. package/dist/rules/openapi/functions/oas-schema.d.ts +15 -0
  109. package/dist/rules/openapi/functions/oas-schema.js +15 -0
  110. package/dist/rules/openapi/functions/oas-server-name-unique.d.ts +3 -0
  111. package/dist/rules/openapi/functions/oas-server-name-unique.js +20 -0
  112. package/dist/rules/openapi/functions/oas-server-variables.d.ts +3 -0
  113. package/dist/rules/openapi/functions/oas-server-variables.js +23 -0
  114. package/dist/rules/openapi/functions/oas-tag-defined.d.ts +3 -0
  115. package/dist/rules/openapi/functions/oas-tag-defined.js +21 -0
  116. package/dist/rules/openapi/functions/oas-tag-kind.d.ts +3 -0
  117. package/dist/rules/openapi/functions/oas-tag-kind.js +17 -0
  118. package/dist/rules/openapi/functions/oas-tag-parent-defined.d.ts +7 -0
  119. package/dist/rules/openapi/functions/oas-tag-parent-defined.js +45 -0
  120. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +3 -0
  121. package/dist/rules/openapi/functions/oas-tags-unique.js +17 -0
  122. package/dist/rules/openapi/functions/oas-unused-component.d.ts +7 -0
  123. package/dist/rules/openapi/functions/oas-unused-component.js +52 -0
  124. package/dist/rules/openapi/functions/ref-siblings.d.ts +3 -0
  125. package/dist/rules/openapi/functions/ref-siblings.js +13 -0
  126. package/dist/rules/openapi/index.d.ts +25 -0
  127. package/dist/rules/openapi/index.js +127 -0
  128. package/dist/rules/openapi/oas.d.ts +3 -0
  129. package/dist/rules/openapi/oas.js +492 -0
  130. package/dist/rules/openapi/schemas/index.d.ts +8 -0
  131. package/dist/rules/openapi/schemas/index.js +36 -0
  132. package/dist/rules/openapi/schemas/oas20.json +1592 -0
  133. package/dist/rules/openapi/schemas/oas30.json +1651 -0
  134. package/dist/rules/openapi/schemas/oas31.json +1412 -0
  135. package/dist/rules/openapi/schemas/oas32.json +1684 -0
  136. package/package.json +8 -5
@@ -0,0 +1,13 @@
1
+ import { isObject } from './helpers.js';
2
+ /** Flags objects that mix `$ref` with sibling keys (which are ignored per spec). */
3
+ export const refSiblings = (input, _options, context) => {
4
+ if (!isObject(input) || !('$ref' in input))
5
+ return [];
6
+ const results = [];
7
+ for (const key of Object.keys(input)) {
8
+ if (key !== '$ref') {
9
+ results.push({ message: `$ref must not be placed next to "${key}"`, path: [...context.path, key] });
10
+ }
11
+ }
12
+ return results;
13
+ };
@@ -0,0 +1,25 @@
1
+ import { type FunctionRegistry, type ResolvedExtend, type Ruleset, type RulesetDefinition } from '../../core/index.js';
2
+ export { oasFixers } from './fixers.js';
3
+ export { oas2, oas3, oas3_0, oas3_1, oas3_2, oasFormats } from './formats.js';
4
+ export { oasFunctions } from './functions/index.js';
5
+ export { oas } from './oas.js';
6
+ export { loadOasSchema, type OasVersion } from './schemas/index.js';
7
+ /** The built-in `@amritk/lint` functions plus the OpenAPI-specific ones, keyed by name. */
8
+ export declare const allFunctions: FunctionRegistry;
9
+ /**
10
+ * Resolves an `extends` reference to a ruleset definition. Extends the generic
11
+ * file/package resolution with the OpenAPI preset names:
12
+ * - `oas` / `loupe:oas` / `spectral:oas` → the built-in {@link oas} ruleset,
13
+ * - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
14
+ * - npm package specifiers (resolved from `basePath`), including subpaths.
15
+ */
16
+ export declare const resolveOpenApiRuleset: (name: string, basePath?: string) => ResolvedExtend;
17
+ /**
18
+ * Builds a runnable {@link Ruleset} for OpenAPI, layering the built-in and
19
+ * OpenAPI functions (plus any custom ones the definition declares), the OpenAPI
20
+ * `formats`, and `extends` resolution that understands the `oas` / `loupe:oas` /
21
+ * `spectral:oas` names. With no definition it defaults to `extends: [oas]`
22
+ * (recommended rules only). Feed the result to `@amritk/lint`'s core
23
+ * `lintWithResult` (with a `$ref` resolver for `resolved: true` rules).
24
+ */
25
+ export declare const createOpenApiRuleset: (definition?: RulesetDefinition, basePath?: string) => Ruleset;
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, isAbsolute, resolve as resolvePath } from 'node:path';
4
+ import { createRuleset as createCoreRuleset, } from '../../core/index.js';
5
+ import { builtinFunctions } from '../../functions/index.js';
6
+ import { parseWithPointers } from '../../parsers/index.js';
7
+ import { oasFormats } from './formats.js';
8
+ import { oasFunctions } from './functions/index.js';
9
+ import { oas } from './oas.js';
10
+ export { oasFixers } from './fixers.js';
11
+ export { oas2, oas3, oas3_0, oas3_1, oas3_2, oasFormats } from './formats.js';
12
+ export { oasFunctions } from './functions/index.js';
13
+ export { oas } from './oas.js';
14
+ export { loadOasSchema } from './schemas/index.js';
15
+ /** The built-in `@amritk/lint` functions plus the OpenAPI-specific ones, keyed by name. */
16
+ export const allFunctions = { ...builtinFunctions, ...oasFunctions };
17
+ const require = createRequire(import.meta.url);
18
+ /** The names that resolve to the built-in OpenAPI ruleset (incl. the legacy Spectral alias). */
19
+ const OAS_RULESET_NAMES = new Set(['oas', 'loupe:oas', 'spectral:oas']);
20
+ /** Loads a ruleset definition from a file path by extension (YAML/JSON parsed, JS/CJS/MJS required). */
21
+ const loadRulesetFile = (file) => {
22
+ if (/\.(ya?ml|json)$/i.test(file)) {
23
+ return parseWithPointers(readFileSync(file, 'utf8')).data;
24
+ }
25
+ const module = require(file);
26
+ return (module.default ?? module);
27
+ };
28
+ /**
29
+ * Resolves an `extends` reference to a ruleset definition. Extends the generic
30
+ * file/package resolution with the OpenAPI preset names:
31
+ * - `oas` / `loupe:oas` / `spectral:oas` → the built-in {@link oas} ruleset,
32
+ * - local file paths (relative to `basePath`, or absolute): `.yaml` / `.yml` / `.json` / `.js`,
33
+ * - npm package specifiers (resolved from `basePath`), including subpaths.
34
+ */
35
+ export const resolveOpenApiRuleset = (name, basePath = process.cwd()) => {
36
+ if (OAS_RULESET_NAMES.has(name))
37
+ return { definition: oas, basePath };
38
+ if (name.startsWith('.') || isAbsolute(name)) {
39
+ const file = resolvePath(basePath, name);
40
+ return { definition: loadRulesetFile(file), basePath: dirname(file) };
41
+ }
42
+ let file;
43
+ try {
44
+ file = require.resolve(name, { paths: [basePath] });
45
+ }
46
+ catch {
47
+ throw new Error(`Cannot resolve extended ruleset "${name}" from ${basePath}`);
48
+ }
49
+ return { definition: loadRulesetFile(file), basePath: dirname(file) };
50
+ };
51
+ /** Loads a single custom function module (`<dir>/<name>.{js,cjs,mjs}` or a bare path). */
52
+ const loadFunctionByName = (basePath, dir, name) => {
53
+ const baseFile = resolvePath(basePath, dir, name);
54
+ for (const candidate of [baseFile, `${baseFile}.js`, `${baseFile}.cjs`, `${baseFile}.mjs`]) {
55
+ try {
56
+ const resolvedFile = require.resolve(candidate);
57
+ const module = require(resolvedFile);
58
+ const fn = module.default ?? module;
59
+ if (typeof fn !== 'function')
60
+ throw new Error(`"${name}" did not export a function`);
61
+ return fn;
62
+ }
63
+ catch (error) {
64
+ if (error.code !== 'MODULE_NOT_FOUND')
65
+ throw error;
66
+ }
67
+ }
68
+ throw new Error(`Cannot resolve custom function "${name}" from ${resolvePath(basePath, dir)}`);
69
+ };
70
+ /**
71
+ * Walks a ruleset definition (and its string `extends`) collecting custom
72
+ * functions declared via `functions` / `functionsDir`, each loaded relative to
73
+ * the directory of the ruleset that declared it. The built-in `oas` names carry
74
+ * no custom functions, so they are skipped.
75
+ */
76
+ const collectCustomFunctions = (definition, basePath, into, seen) => {
77
+ if (seen.has(definition))
78
+ return;
79
+ seen.add(definition);
80
+ if (definition.extends) {
81
+ const entries = Array.isArray(definition.extends) ? definition.extends : [definition.extends];
82
+ for (const entry of entries) {
83
+ const target = Array.isArray(entry) ? entry[0] : entry;
84
+ if (typeof target === 'string') {
85
+ if (OAS_RULESET_NAMES.has(target))
86
+ continue;
87
+ const resolved = resolveOpenApiRuleset(target, basePath);
88
+ collectCustomFunctions(resolved.definition, resolved.basePath, into, seen);
89
+ }
90
+ else {
91
+ collectCustomFunctions(target, basePath, into, seen);
92
+ }
93
+ }
94
+ }
95
+ if (Array.isArray(definition.functions)) {
96
+ const dir = definition.functionsDir ?? 'functions';
97
+ for (const name of definition.functions)
98
+ into[name] = loadFunctionByName(basePath, dir, name);
99
+ }
100
+ };
101
+ /**
102
+ * Builds a runnable {@link Ruleset} for OpenAPI, layering the built-in and
103
+ * OpenAPI functions (plus any custom ones the definition declares), the OpenAPI
104
+ * `formats`, and `extends` resolution that understands the `oas` / `loupe:oas` /
105
+ * `spectral:oas` names. With no definition it defaults to `extends: [oas]`
106
+ * (recommended rules only). Feed the result to `@amritk/lint`'s core
107
+ * `lintWithResult` (with a `$ref` resolver for `resolved: true` rules).
108
+ */
109
+ export const createOpenApiRuleset = (definition, basePath) => {
110
+ // With no explicit ruleset, behave like `extends: [oas]` so only `recommended`
111
+ // rules run by default. A user-supplied ruleset is used as-is (its own rules
112
+ // run regardless of `recommended`).
113
+ const resolved = definition ?? { extends: [oas] };
114
+ // Custom functions referenced by name (YAML/JSON rulesets) are loaded relative
115
+ // to the declaring ruleset's directory and layered over the built-ins.
116
+ let functions = allFunctions;
117
+ const custom = {};
118
+ collectCustomFunctions(resolved, basePath ?? process.cwd(), custom, new Set());
119
+ if (Object.keys(custom).length > 0)
120
+ functions = { ...allFunctions, ...custom };
121
+ return createCoreRuleset(resolved, {
122
+ functions,
123
+ formats: oasFormats,
124
+ resolve: resolveOpenApiRuleset,
125
+ ...(basePath !== undefined ? { basePath } : {}),
126
+ });
127
+ };
@@ -0,0 +1,3 @@
1
+ import type { RulesetDefinition } from '../../core/index.js';
2
+ /** Loupe's built-in OpenAPI ruleset (`loupe:oas`). */
3
+ export declare const oas: RulesetDefinition;
@@ -0,0 +1,492 @@
1
+ const OPERATIONS = '$.paths[*][get,put,post,delete,options,head,patch,trace]';
2
+ const WEBHOOK_OPERATIONS = '$.webhooks[*][get,put,post,delete,options,head,patch,trace]';
3
+ const OPERATION_ID_URL_SAFE = "^[A-Za-z0-9-._~:/?#\\[\\]@!\\$&'()*+,;=]*$";
4
+ /** Rules shared across OpenAPI v2 and v3. */
5
+ const sharedRules = {
6
+ 'contact-properties': {
7
+ description: 'Contact object must have name, url, and email.',
8
+ given: '$.info.contact',
9
+ severity: 'warn',
10
+ recommended: false,
11
+ then: [
12
+ { field: 'name', function: 'truthy' },
13
+ { field: 'url', function: 'truthy' },
14
+ { field: 'email', function: 'truthy' },
15
+ ],
16
+ },
17
+ 'duplicated-entry-in-enum': {
18
+ description: 'Enum values must not have duplicate entries.',
19
+ given: '$..enum',
20
+ severity: 'warn',
21
+ then: { function: 'schema', functionOptions: { schema: { type: 'array', uniqueItems: true } } },
22
+ },
23
+ 'info-contact': {
24
+ description: 'Info object must have a contact object.',
25
+ given: '$.info',
26
+ then: { field: 'contact', function: 'truthy' },
27
+ },
28
+ 'info-description': {
29
+ description: 'Info object must have a description.',
30
+ given: '$.info',
31
+ then: { field: 'description', function: 'truthy' },
32
+ },
33
+ 'info-license': {
34
+ description: 'Info object should have a license.',
35
+ given: '$.info',
36
+ severity: 'warn',
37
+ recommended: false,
38
+ then: { field: 'license', function: 'truthy' },
39
+ },
40
+ 'license-url': {
41
+ description: 'License object should have a url.',
42
+ given: '$.info.license',
43
+ severity: 'warn',
44
+ recommended: false,
45
+ then: { field: 'url', function: 'truthy' },
46
+ },
47
+ 'no-$ref-siblings': {
48
+ description: 'Sibling values alongside $ref are ignored.',
49
+ // OpenAPI 3.1 (JSON Schema 2020-12) permits siblings next to `$ref`, so this
50
+ // rule is gated to 2.0 / 3.0 — matching `spectral:oas`.
51
+ formats: ['oas2', 'oas3_0'],
52
+ given: '$..$ref^',
53
+ severity: 'error',
54
+ resolved: false,
55
+ then: { function: 'refSiblings' },
56
+ },
57
+ 'no-eval-in-markdown': {
58
+ description: 'Markdown descriptions must not contain "eval(".',
59
+ given: '$..description',
60
+ then: { function: 'pattern', functionOptions: { notMatch: 'eval\\(' } },
61
+ },
62
+ 'no-script-tags-in-markdown': {
63
+ description: 'Markdown descriptions must not contain <script> tags.',
64
+ given: '$..description',
65
+ then: { function: 'pattern', functionOptions: { notMatch: '<script' } },
66
+ },
67
+ 'openapi-tags': {
68
+ description: 'Top-level tags should be present and non-empty.',
69
+ given: '$.tags',
70
+ severity: 'warn',
71
+ recommended: false,
72
+ then: { function: 'length', functionOptions: { min: 1 } },
73
+ },
74
+ 'openapi-tags-alphabetical': {
75
+ description: 'Top-level tags should be in alphabetical order.',
76
+ given: '$.tags',
77
+ severity: 'warn',
78
+ recommended: false,
79
+ then: { function: 'alphabetical', functionOptions: { keyedBy: 'name' } },
80
+ },
81
+ 'openapi-tags-uniqueness': {
82
+ description: 'Top-level tag names must be unique.',
83
+ given: '$.tags',
84
+ severity: 'error',
85
+ then: { function: 'oasTagsUnique' },
86
+ },
87
+ 'operation-description': {
88
+ description: 'Operation must have a description.',
89
+ given: OPERATIONS,
90
+ then: { field: 'description', function: 'truthy' },
91
+ },
92
+ 'operation-operationId': {
93
+ description: 'Operation must have an operationId.',
94
+ given: OPERATIONS,
95
+ then: { field: 'operationId', function: 'truthy' },
96
+ },
97
+ 'operation-operationId-unique': {
98
+ description: 'Every operationId must be unique.',
99
+ given: '$.paths',
100
+ severity: 'error',
101
+ then: { function: 'oasOpIdUnique' },
102
+ },
103
+ 'operation-operationId-valid-in-url': {
104
+ description: 'operationId must use URL-safe characters.',
105
+ given: OPERATIONS,
106
+ then: { field: 'operationId', function: 'pattern', functionOptions: { match: OPERATION_ID_URL_SAFE } },
107
+ },
108
+ 'operation-parameters': {
109
+ description: 'Operation parameters must be unique and non-repeating.',
110
+ given: '$.paths[*][*].parameters',
111
+ then: { function: 'oasOpParams' },
112
+ },
113
+ 'operation-singular-tag': {
114
+ description: 'Operation should have a single tag.',
115
+ given: OPERATIONS,
116
+ severity: 'warn',
117
+ recommended: false,
118
+ then: { field: 'tags', function: 'length', functionOptions: { max: 1 } },
119
+ },
120
+ 'operation-success-response': {
121
+ description: 'Operation must have at least one 2xx or 3xx response.',
122
+ given: OPERATIONS,
123
+ then: { field: 'responses', function: 'oasOpSuccessResponse' },
124
+ },
125
+ 'operation-tags': {
126
+ description: 'Operation must have non-empty tags.',
127
+ given: OPERATIONS,
128
+ then: { field: 'tags', function: 'truthy' },
129
+ },
130
+ 'operation-tag-defined': {
131
+ description: 'Operation tags must be defined in the global tags list.',
132
+ given: OPERATIONS,
133
+ then: { function: 'oasTagDefined' },
134
+ },
135
+ 'path-declarations-must-exist': {
136
+ description: 'Path parameter declarations must not be empty ({}).',
137
+ given: '$.paths',
138
+ then: { field: '@key', function: 'pattern', functionOptions: { notMatch: '{}' } },
139
+ },
140
+ 'path-keys-no-trailing-slash': {
141
+ description: 'Path keys should not end with a slash.',
142
+ given: '$.paths',
143
+ then: { field: '@key', function: 'pattern', functionOptions: { notMatch: '.+\\/$' } },
144
+ },
145
+ 'path-not-include-query': {
146
+ description: 'Path keys should not include query strings.',
147
+ given: '$.paths',
148
+ then: { field: '@key', function: 'pattern', functionOptions: { notMatch: '\\?' } },
149
+ },
150
+ 'path-params': {
151
+ description: 'Path parameters must be defined and not duplicated.',
152
+ given: '$.paths',
153
+ severity: 'error',
154
+ then: { function: 'oasPathParam' },
155
+ },
156
+ 'tag-description': {
157
+ description: 'Tags should have a description.',
158
+ given: '$.tags[*]',
159
+ severity: 'warn',
160
+ recommended: false,
161
+ then: { field: 'description', function: 'truthy' },
162
+ },
163
+ 'typed-enum': {
164
+ description: 'Enum values must respect the specified type.',
165
+ given: '$..enum^',
166
+ then: { function: 'typedEnum' },
167
+ },
168
+ 'array-items': {
169
+ description: 'Schemas of type array must define items.',
170
+ given: "$..[?(@ && @.type === 'array')]",
171
+ severity: 'error',
172
+ then: { field: 'items', function: 'defined' },
173
+ },
174
+ };
175
+ /** OpenAPI v2.0-only rules. */
176
+ const oas2Rules = {
177
+ 'oas2-anyOf': {
178
+ description: 'anyOf is not available in OpenAPI v2.0.',
179
+ formats: ['oas2'],
180
+ given: '$..anyOf',
181
+ then: { function: 'falsy' },
182
+ },
183
+ 'oas2-oneOf': {
184
+ description: 'oneOf is not available in OpenAPI v2.0.',
185
+ formats: ['oas2'],
186
+ given: '$..oneOf',
187
+ then: { function: 'falsy' },
188
+ },
189
+ 'oas2-api-host': {
190
+ description: 'OpenAPI host must be present and non-empty.',
191
+ formats: ['oas2'],
192
+ given: '$',
193
+ then: { field: 'host', function: 'truthy' },
194
+ },
195
+ 'oas2-api-schemes': {
196
+ description: 'OpenAPI schemes must be present and non-empty.',
197
+ formats: ['oas2'],
198
+ given: '$.schemes',
199
+ then: { function: 'length', functionOptions: { min: 1 } },
200
+ },
201
+ 'oas2-discriminator': {
202
+ description: 'Discriminator must reference a required property.',
203
+ formats: ['oas2'],
204
+ given: '$.definitions[*]',
205
+ severity: 'error',
206
+ then: { function: 'oasDiscriminator' },
207
+ },
208
+ 'oas2-host-not-example': {
209
+ description: 'Host should not point to example.com.',
210
+ formats: ['oas2'],
211
+ given: '$',
212
+ severity: 'warn',
213
+ recommended: false,
214
+ then: { field: 'host', function: 'pattern', functionOptions: { notMatch: 'example\\.com' } },
215
+ },
216
+ 'oas2-host-trailing-slash': {
217
+ description: 'Host should not have a trailing slash.',
218
+ formats: ['oas2'],
219
+ given: '$',
220
+ then: { field: 'host', function: 'pattern', functionOptions: { notMatch: '/$' } },
221
+ },
222
+ 'oas2-operation-formData-consume-check': {
223
+ description: 'formData operations must consume form media types.',
224
+ formats: ['oas2'],
225
+ given: OPERATIONS,
226
+ then: { function: 'oasOpFormDataConsumeCheck' },
227
+ },
228
+ 'oas2-operation-security-defined': {
229
+ description: 'Operation security must reference defined securityDefinitions.',
230
+ formats: ['oas2'],
231
+ given: '$',
232
+ then: { function: 'oasOpSecurityDefined', functionOptions: { schemesPath: ['securityDefinitions'] } },
233
+ },
234
+ 'oas2-parameter-description': {
235
+ description: 'Parameters should have a description.',
236
+ formats: ['oas2'],
237
+ given: '$..parameters[*]',
238
+ severity: 'warn',
239
+ recommended: false,
240
+ then: { field: 'description', function: 'truthy' },
241
+ },
242
+ 'oas2-unused-definition': {
243
+ description: 'Definitions should be referenced.',
244
+ formats: ['oas2'],
245
+ given: '$.definitions',
246
+ severity: 'warn',
247
+ resolved: false,
248
+ then: { function: 'unreferencedReusableObject', functionOptions: { reusableObjectsLocation: '#/definitions' } },
249
+ },
250
+ 'oas2-valid-schema-example': {
251
+ description: 'Schema examples must be valid against their schema.',
252
+ formats: ['oas2'],
253
+ given: '$..example^',
254
+ severity: 'error',
255
+ then: { function: 'oasSchemaExample' },
256
+ },
257
+ 'oas2-valid-media-example': {
258
+ description: 'Media type examples must be valid against their schema.',
259
+ formats: ['oas2'],
260
+ given: ['$..responses[*]', '$..parameters[*]'],
261
+ severity: 'error',
262
+ then: { function: 'oasMediaExample' },
263
+ },
264
+ 'oas2-schema': {
265
+ description: 'Validate structure of OpenAPI v2 specification.',
266
+ formats: ['oas2'],
267
+ severity: 'error',
268
+ recommended: true,
269
+ resolved: false,
270
+ given: '$',
271
+ then: { function: 'oasSchema', functionOptions: { version: '2.0' } },
272
+ },
273
+ };
274
+ /** OpenAPI v3.x rules. */
275
+ const oas3Rules = {
276
+ 'oas3-api-servers': {
277
+ description: 'OpenAPI 3 documents must have a non-empty servers array.',
278
+ formats: ['oas3'],
279
+ given: '$.servers',
280
+ then: { function: 'length', functionOptions: { min: 1 } },
281
+ },
282
+ 'oas3-examples-value-or-externalValue': {
283
+ description: 'Example objects must use either value or externalValue, not both.',
284
+ formats: ['oas3'],
285
+ given: ['$.components.examples[*]', '$..content[*].examples[*]', '$..parameters[*].examples[*]'],
286
+ then: { function: 'xor', functionOptions: { properties: ['value', 'externalValue'] } },
287
+ },
288
+ 'oas3-operation-security-defined': {
289
+ description: 'Operation security must reference defined securitySchemes.',
290
+ formats: ['oas3'],
291
+ given: '$',
292
+ then: { function: 'oasOpSecurityDefined', functionOptions: { schemesPath: ['components', 'securitySchemes'] } },
293
+ },
294
+ 'oas3-parameter-description': {
295
+ description: 'Parameters should have a description.',
296
+ formats: ['oas3'],
297
+ given: '$..parameters[*]',
298
+ severity: 'warn',
299
+ recommended: false,
300
+ then: { field: 'description', function: 'truthy' },
301
+ },
302
+ 'oas3-server-not-example.com': {
303
+ description: 'Server URLs should not point to example.com.',
304
+ formats: ['oas3'],
305
+ given: '$.servers[*].url',
306
+ severity: 'warn',
307
+ recommended: false,
308
+ then: { function: 'pattern', functionOptions: { notMatch: 'example\\.com' } },
309
+ },
310
+ 'oas3-server-trailing-slash': {
311
+ description: 'Server URLs should not have trailing slashes.',
312
+ formats: ['oas3'],
313
+ given: '$.servers[*].url',
314
+ then: { function: 'pattern', functionOptions: { notMatch: './$' } },
315
+ },
316
+ 'oas3-server-variables': {
317
+ description: 'Server variables must be defined and used.',
318
+ formats: ['oas3'],
319
+ given: '$.servers[*]',
320
+ severity: 'error',
321
+ then: { function: 'oasServerVariables' },
322
+ },
323
+ 'oas3-callbacks-in-callbacks': {
324
+ description: 'Callbacks must not be defined within other callbacks.',
325
+ formats: ['oas3'],
326
+ given: '$..callbacks..callbacks',
327
+ then: { function: 'falsy' },
328
+ },
329
+ 'oas3-unused-component': {
330
+ description: 'Reusable components should be referenced.',
331
+ formats: ['oas3'],
332
+ given: '$.components',
333
+ severity: 'warn',
334
+ resolved: false,
335
+ then: { function: 'oasUnusedComponent' },
336
+ },
337
+ 'oas3-valid-schema-example': {
338
+ description: 'Schema examples must be valid against their schema.',
339
+ formats: ['oas3'],
340
+ given: '$..example^',
341
+ severity: 'error',
342
+ then: { function: 'oasSchemaExample' },
343
+ },
344
+ 'oas3-valid-media-example': {
345
+ description: 'Media type examples must be valid against their schema.',
346
+ formats: ['oas3'],
347
+ given: '$..content[*]',
348
+ severity: 'error',
349
+ then: { function: 'oasMediaExample' },
350
+ },
351
+ 'oas3-schema': {
352
+ description: 'Validate structure of OpenAPI v3.0.x specification.',
353
+ formats: ['oas3_0'],
354
+ severity: 'error',
355
+ recommended: true,
356
+ resolved: false,
357
+ given: '$',
358
+ then: { function: 'oasSchema', functionOptions: { version: '3.0' } },
359
+ },
360
+ };
361
+ /** Rules for features introduced in OpenAPI 3.1 (and still in 3.2). */
362
+ const oas31Rules = {
363
+ 'oas3_1-servers-in-webhook': {
364
+ description: 'Webhooks must not define servers.',
365
+ formats: ['oas3_1'],
366
+ given: WEBHOOK_OPERATIONS,
367
+ then: { field: 'servers', function: 'falsy' },
368
+ },
369
+ 'oas3_1-callbacks-in-webhook': {
370
+ description: 'Webhooks must not define callbacks.',
371
+ formats: ['oas3_1'],
372
+ given: WEBHOOK_OPERATIONS,
373
+ then: { field: 'callbacks', function: 'falsy' },
374
+ },
375
+ 'oas3_1-no-nullable': {
376
+ // `nullable` was removed in OpenAPI 3.1 (JSON Schema 2020-12 uses a `null`
377
+ // type instead) and stays gone in 3.2 — mirroring the oas2-anyOf/oneOf
378
+ // "feature not available in this version" rules.
379
+ description: 'nullable is not available in OpenAPI 3.1 or later; use a "null" type instead.',
380
+ formats: ['oas3_1', 'oas3_2'],
381
+ given: '$..nullable',
382
+ then: { function: 'falsy' },
383
+ },
384
+ 'oas3_1-license-identifier': {
385
+ // The License Object's `identifier` (SPDX) field was added in 3.1 and is
386
+ // "mutually exclusive of the url field".
387
+ description: 'License object identifier and url are mutually exclusive.',
388
+ formats: ['oas3_1', 'oas3_2'],
389
+ given: '$.info.license',
390
+ severity: 'error',
391
+ then: { function: 'oasMutuallyExclusive', functionOptions: { properties: ['identifier', 'url'] } },
392
+ },
393
+ 'oas3_1-schema': {
394
+ // The official, self-contained OpenAPI 3.1 meta-schema (spec.openapis.org).
395
+ // 3.1 realigned Schema Objects with JSON Schema 2020-12, so the official
396
+ // schema validates the whole document *envelope* while leaving Schema Object
397
+ // internals permissive (a local `$dynamicRef` "#meta" that the runtime
398
+ // validator resolves natively). The 3.0-only `oas3-schema` rule does not
399
+ // apply to 3.1 (different `openapi` version and structure).
400
+ description: 'Validate structure of OpenAPI v3.1 specification.',
401
+ formats: ['oas3_1'],
402
+ severity: 'error',
403
+ recommended: true,
404
+ resolved: false,
405
+ given: '$',
406
+ then: { function: 'oasSchema', functionOptions: { version: '3.1' } },
407
+ },
408
+ 'oas3_1-schema-example-deprecated': {
409
+ // JSON Schema 2020-12 deprecates a Schema Object's singular `example` in
410
+ // favor of the `examples` array. Off by default (recommended: false) since
411
+ // singular examples remain widespread and valid-but-deprecated.
412
+ description: 'Schema "example" is deprecated in OpenAPI 3.1; use "examples" instead.',
413
+ formats: ['oas3_1', 'oas3_2'],
414
+ given: '$..example^',
415
+ severity: 'warn',
416
+ recommended: false,
417
+ then: { function: 'oasSchemaExampleDeprecated' },
418
+ },
419
+ };
420
+ /** Rules for features introduced in OpenAPI 3.2. */
421
+ const oas32Rules = {
422
+ 'oas3_2-schema': {
423
+ // The official, self-contained OpenAPI 3.2 meta-schema (spec.openapis.org).
424
+ // Like `oas3_1-schema`, it validates the document envelope and leaves Schema
425
+ // Object internals to JSON Schema 2020-12 via a local `$dynamicRef`.
426
+ description: 'Validate structure of OpenAPI v3.2 specification.',
427
+ formats: ['oas3_2'],
428
+ severity: 'error',
429
+ recommended: true,
430
+ resolved: false,
431
+ given: '$',
432
+ then: { function: 'oasSchema', functionOptions: { version: '3.2' } },
433
+ },
434
+ 'oas3_2-additional-operations-standard-method': {
435
+ // `additionalOperations` is for HTTP methods without a dedicated fixed
436
+ // field; the spec forbids redefining a standard method there.
437
+ description: 'additionalOperations must not redefine a standard HTTP method that has its own fixed field.',
438
+ formats: ['oas3_2'],
439
+ given: '$..additionalOperations',
440
+ severity: 'error',
441
+ then: { function: 'oasAdditionalOperations' },
442
+ },
443
+ 'oas3_2-server-name-unique': {
444
+ description: 'Server names should be unique across the servers array.',
445
+ formats: ['oas3_2'],
446
+ given: '$.servers',
447
+ severity: 'warn',
448
+ then: { function: 'oasServerNameUnique' },
449
+ },
450
+ 'oas3_2-tag-parent-defined': {
451
+ // Tag hierarchies (3.2): a `parent` must name a defined tag, with no cycles.
452
+ // Mirrors `operation-tag-defined` for the new nesting feature.
453
+ description: 'Tag parent must reference a tag defined in the global tags list, without cycles.',
454
+ formats: ['oas3_2'],
455
+ given: '$.tags',
456
+ severity: 'warn',
457
+ then: { function: 'oasTagParentDefined' },
458
+ },
459
+ 'oas3_2-tag-kind': {
460
+ // `kind` is free-form with a community registry of conventional values, so
461
+ // this is off by default (recommended: false) to avoid flagging valid
462
+ // custom kinds. Only a present-but-unregistered value is flagged.
463
+ description: 'Tag kind should use a registered value (nav, badge, audience).',
464
+ formats: ['oas3_2'],
465
+ given: '$.tags[*]',
466
+ severity: 'warn',
467
+ recommended: false,
468
+ then: { function: 'oasTagKind' },
469
+ },
470
+ 'oas3_2-example-value': {
471
+ // 3.2 added `dataValue`/`serializedValue` to the Example Object with
472
+ // MUST-level exclusivity: dataValue excludes value; serializedValue excludes
473
+ // value and externalValue. (The value/externalValue pair is left to the
474
+ // 3.x-wide oas3-examples-value-or-externalValue rule.)
475
+ description: 'Example object dataValue/serializedValue must not be combined with value or externalValue.',
476
+ formats: ['oas3_2'],
477
+ given: ['$.components.examples[*]', '$..content[*].examples[*]', '$..parameters[*].examples[*]'],
478
+ severity: 'error',
479
+ then: { function: 'oasExampleValue' },
480
+ },
481
+ };
482
+ /** Loupe's built-in OpenAPI ruleset (`loupe:oas`). */
483
+ export const oas = {
484
+ formats: ['oas2', 'oas3'],
485
+ rules: {
486
+ ...sharedRules,
487
+ ...oas2Rules,
488
+ ...oas3Rules,
489
+ ...oas31Rules,
490
+ ...oas32Rules,
491
+ },
492
+ };
@@ -0,0 +1,8 @@
1
+ /** The OpenAPI versions with a bundled structural meta-schema. */
2
+ export type OasVersion = '2.0' | '3.0' | '3.1' | '3.2';
3
+ /**
4
+ * Lazily loads (and memoizes) the official structural meta-schema for one
5
+ * OpenAPI version. The returned object is stable across calls, so downstream
6
+ * validator caches (keyed by schema identity) stay warm.
7
+ */
8
+ export declare const loadOasSchema: (version: OasVersion) => object;