@amritk/lint 0.4.8 → 0.5.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 (107) hide show
  1. package/AI.md +73 -4
  2. package/README.md +65 -14
  3. package/dist/core/filter-expression.js +17 -4
  4. package/dist/core/filter.js +82 -35
  5. package/dist/core/glob.js +25 -31
  6. package/dist/core/jsonpath.d.ts +9 -1
  7. package/dist/core/jsonpath.js +74 -16
  8. package/dist/core/lint.d.ts +8 -1
  9. package/dist/core/lint.js +4 -18
  10. package/dist/core/order.d.ts +30 -0
  11. package/dist/core/order.js +36 -0
  12. package/dist/core/pointers.d.ts +3 -2
  13. package/dist/core/pointers.js +27 -18
  14. package/dist/core/ruleset.js +62 -20
  15. package/dist/core/runner.js +16 -23
  16. package/dist/core/severity.d.ts +11 -0
  17. package/dist/core/severity.js +13 -0
  18. package/dist/fix/apply.js +6 -1
  19. package/dist/fix/plugin.js +2 -1
  20. package/dist/functions/or.js +1 -1
  21. package/dist/functions/ref-index.d.ts +15 -0
  22. package/dist/functions/ref-index.js +33 -0
  23. package/dist/functions/schema.d.ts +15 -0
  24. package/dist/functions/schema.js +2 -0
  25. package/dist/functions/typed-enum.js +2 -1
  26. package/dist/functions/unreferenced-reusable-object.js +3 -20
  27. package/dist/functions/xor.js +1 -1
  28. package/dist/index.d.ts +16 -16
  29. package/dist/index.js +24 -93
  30. package/dist/rules/asyncapi/asyncapi.d.ts +3 -0
  31. package/dist/rules/asyncapi/asyncapi.js +563 -0
  32. package/dist/rules/asyncapi/formats.d.ts +23 -0
  33. package/dist/rules/asyncapi/formats.js +49 -0
  34. package/dist/rules/asyncapi/functions/aas-server-variables.d.ts +8 -0
  35. package/dist/rules/asyncapi/functions/aas-server-variables.js +5 -0
  36. package/dist/rules/asyncapi/functions/aas-tags-unique.d.ts +2 -0
  37. package/dist/rules/asyncapi/functions/aas-tags-unique.js +5 -0
  38. package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.d.ts +7 -0
  39. package/dist/rules/asyncapi/functions/asyncapi-channel-parameters.js +46 -0
  40. package/dist/rules/asyncapi/functions/asyncapi-channel-servers.d.ts +27 -0
  41. package/dist/rules/asyncapi/functions/asyncapi-channel-servers.js +61 -0
  42. package/dist/rules/asyncapi/functions/asyncapi-document-schema.d.ts +17 -0
  43. package/dist/rules/asyncapi/functions/asyncapi-document-schema.js +12 -0
  44. package/dist/rules/asyncapi/functions/asyncapi-headers-object.d.ts +23 -0
  45. package/dist/rules/asyncapi/functions/asyncapi-headers-object.js +25 -0
  46. package/dist/rules/asyncapi/functions/asyncapi-message-examples.d.ts +8 -0
  47. package/dist/rules/asyncapi/functions/asyncapi-message-examples.js +42 -0
  48. package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.d.ts +3 -0
  49. package/dist/rules/asyncapi/functions/asyncapi-message-id-unique.js +30 -0
  50. package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.d.ts +3 -0
  51. package/dist/rules/asyncapi/functions/asyncapi-operation-id-unique.js +30 -0
  52. package/dist/rules/asyncapi/functions/asyncapi-payload.d.ts +14 -0
  53. package/dist/rules/asyncapi/functions/asyncapi-payload.js +32 -0
  54. package/dist/rules/asyncapi/functions/asyncapi-schema-validation.d.ts +11 -0
  55. package/dist/rules/asyncapi/functions/asyncapi-schema-validation.js +17 -0
  56. package/dist/rules/asyncapi/functions/asyncapi-security.d.ts +22 -0
  57. package/dist/rules/asyncapi/functions/asyncapi-security.js +78 -0
  58. package/dist/rules/asyncapi/functions/helpers.d.ts +51 -0
  59. package/dist/rules/asyncapi/functions/helpers.js +91 -0
  60. package/dist/rules/asyncapi/functions/index.d.ts +15 -0
  61. package/dist/rules/asyncapi/functions/index.js +53 -0
  62. package/dist/rules/asyncapi/functions/pointer.d.ts +11 -0
  63. package/dist/rules/asyncapi/functions/pointer.js +11 -0
  64. package/dist/rules/asyncapi/functions/schema-format.d.ts +4 -0
  65. package/dist/rules/asyncapi/functions/schema-format.js +7 -0
  66. package/dist/rules/asyncapi/index.d.ts +33 -0
  67. package/dist/rules/asyncapi/index.js +60 -0
  68. package/dist/rules/asyncapi/schemas/aas20.d.ts +2 -0
  69. package/dist/rules/asyncapi/schemas/aas20.js +4 -0
  70. package/dist/rules/asyncapi/schemas/aas21.d.ts +2 -0
  71. package/dist/rules/asyncapi/schemas/aas21.js +4 -0
  72. package/dist/rules/asyncapi/schemas/aas22.d.ts +2 -0
  73. package/dist/rules/asyncapi/schemas/aas22.js +4 -0
  74. package/dist/rules/asyncapi/schemas/aas23.d.ts +2 -0
  75. package/dist/rules/asyncapi/schemas/aas23.js +4 -0
  76. package/dist/rules/asyncapi/schemas/aas24.d.ts +2 -0
  77. package/dist/rules/asyncapi/schemas/aas24.js +4 -0
  78. package/dist/rules/asyncapi/schemas/aas25.d.ts +2 -0
  79. package/dist/rules/asyncapi/schemas/aas25.js +4 -0
  80. package/dist/rules/asyncapi/schemas/aas26.d.ts +2 -0
  81. package/dist/rules/asyncapi/schemas/aas26.js +4 -0
  82. package/dist/rules/asyncapi/schemas/aas30.d.ts +2 -0
  83. package/dist/rules/asyncapi/schemas/aas30.js +4 -0
  84. package/dist/rules/asyncapi/schemas/index.d.ts +20 -0
  85. package/dist/rules/asyncapi/schemas/index.js +45 -0
  86. package/dist/rules/openapi/fixers.d.ts +6 -2
  87. package/dist/rules/openapi/fixers.js +2 -0
  88. package/dist/rules/openapi/functions/oas-discriminator.js +1 -1
  89. package/dist/rules/openapi/functions/oas-mutually-exclusive.js +1 -1
  90. package/dist/rules/openapi/functions/oas-op-security-defined.js +1 -1
  91. package/dist/rules/openapi/functions/oas-server-variables.d.ts +6 -6
  92. package/dist/rules/openapi/functions/oas-server-variables.js +2 -45
  93. package/dist/rules/openapi/functions/oas-tags-unique.d.ts +2 -3
  94. package/dist/rules/openapi/functions/oas-tags-unique.js +2 -16
  95. package/dist/rules/openapi/functions/oas-unused-component.js +3 -25
  96. package/dist/rules/openapi/index.d.ts +9 -2
  97. package/dist/rules/openapi/index.js +15 -70
  98. package/dist/rules/openapi/schemas/index.js +5 -1
  99. package/dist/rules/shared/helpers.d.ts +2 -0
  100. package/dist/rules/shared/helpers.js +4 -0
  101. package/dist/rules/shared/server-variables.d.ts +27 -0
  102. package/dist/rules/shared/server-variables.js +55 -0
  103. package/dist/rules/shared/tags-unique.d.ts +7 -0
  104. package/dist/rules/shared/tags-unique.js +19 -0
  105. package/dist/ruleset-files.d.ts +47 -0
  106. package/dist/ruleset-files.js +92 -0
  107. package/package.json +8 -4
@@ -0,0 +1,563 @@
1
+ import { ASYNCAPI_SCHEMA_FORMAT_FILTER } from "./functions/schema-format.js";
2
+ import { LATEST_ASYNCAPI_VERSION } from "./schemas/index.js";
3
+ const V2_OPERATIONS = "$.channels[*][publish,subscribe]";
4
+ const V2_COMPONENT_OPERATIONS = "$.components.channels[*][publish,subscribe]";
5
+ const V2_MESSAGES = [
6
+ `${V2_OPERATIONS}.message`,
7
+ `${V2_OPERATIONS}.message.oneOf[*]`,
8
+ `${V2_COMPONENT_OPERATIONS}.message`,
9
+ `${V2_COMPONENT_OPERATIONS}.message.oneOf[*]`,
10
+ "$.components.messages[*]"
11
+ ];
12
+ const V2_MESSAGE_TRAITS = [
13
+ `${V2_OPERATIONS}.message.traits[*]`,
14
+ `${V2_OPERATIONS}.message.oneOf[*].traits[*]`,
15
+ `${V2_COMPONENT_OPERATIONS}.message.traits[*]`,
16
+ `${V2_COMPONENT_OPERATIONS}.message.oneOf[*].traits[*]`,
17
+ "$.components.messages[*].traits[*]",
18
+ "$.components.messageTraits[*]"
19
+ ];
20
+ const V2_ALL_MESSAGES = [...V2_MESSAGES, ...V2_MESSAGE_TRAITS];
21
+ const defaultFormatOnly = (given) => given.endsWith(".message") ? `${given.slice(0, -".message".length)}[?(@property === 'message' && (${ASYNCAPI_SCHEMA_FORMAT_FILTER}))]` : `${given.slice(0, -"[*]".length)}[?(${ASYNCAPI_SCHEMA_FORMAT_FILTER})]`;
22
+ const V2_DEFAULT_FORMAT_PAYLOADS = V2_ALL_MESSAGES.map((given) => `${defaultFormatOnly(given)}.payload`);
23
+ const payloadSiblings = (keyword) => V2_DEFAULT_FORMAT_PAYLOADS.map((given) => `${given}.${keyword}^`);
24
+ const sharedRules = {
25
+ "asyncapi-channel-parameters": {
26
+ description: "Channel parameters must be defined, and none may be redundant.",
27
+ // Unresolved: this rule reads what the author wrote — names, addresses,
28
+ // variables — and never needs to see through a `$ref`. Its given already
29
+ // covers the reusable location, so nothing is missed by not dereferencing.
30
+ resolved: false,
31
+ given: ["$.channels[*]", "$.components.channels[*]"],
32
+ severity: "error",
33
+ then: { function: "asyncApiChannelParameters" }
34
+ },
35
+ "asyncapi-info-contact": {
36
+ description: "Info object must have a contact object.",
37
+ given: "$.info",
38
+ then: { field: "contact", function: "truthy" }
39
+ },
40
+ "asyncapi-info-contact-properties": {
41
+ description: "Contact object must have name, url, and email.",
42
+ given: "$.info.contact",
43
+ severity: "warn",
44
+ then: [
45
+ { field: "name", function: "truthy" },
46
+ { field: "url", function: "truthy" },
47
+ { field: "email", function: "truthy" }
48
+ ]
49
+ },
50
+ "asyncapi-info-description": {
51
+ description: "Info object must have a description.",
52
+ given: "$.info",
53
+ then: { field: "description", function: "truthy" }
54
+ },
55
+ "asyncapi-info-license": {
56
+ description: "Info object must have a license object.",
57
+ given: "$.info",
58
+ then: { field: "license", function: "truthy" }
59
+ },
60
+ "asyncapi-info-license-url": {
61
+ description: "License object should have a url.",
62
+ given: "$.info.license",
63
+ severity: "warn",
64
+ recommended: false,
65
+ then: { field: "url", function: "truthy" }
66
+ },
67
+ "asyncapi-latest-version": {
68
+ // Informational: an older document is valid, just not current. The version
69
+ // named here is the newest one this package bundles a meta-schema for, so the
70
+ // advice never points past what the linter can actually check.
71
+ description: "AsyncAPI document should use the latest specification version.",
72
+ message: `The latest version is not used. You should update to the "${LATEST_ASYNCAPI_VERSION}" version.`,
73
+ given: "$.asyncapi",
74
+ severity: "info",
75
+ then: { function: "schema", functionOptions: { schema: { const: LATEST_ASYNCAPI_VERSION } } }
76
+ },
77
+ "asyncapi-parameter-description": {
78
+ description: "Parameter objects should have a description.",
79
+ given: ["$.components.parameters[*]", "$.channels[*].parameters[*]"],
80
+ severity: "warn",
81
+ recommended: false,
82
+ then: { field: "description", function: "truthy" }
83
+ },
84
+ "asyncapi-servers": {
85
+ description: "AsyncAPI object must have a non-empty servers object.",
86
+ given: "$",
87
+ severity: "warn",
88
+ then: {
89
+ field: "servers",
90
+ function: "schema",
91
+ functionOptions: { schema: { type: "object", minProperties: 1 }, allErrors: true }
92
+ }
93
+ },
94
+ "asyncapi-unused-components-schema": {
95
+ description: "Potentially unused components schema has been detected.",
96
+ // Unresolved: once `$ref`s are inlined there is nothing left pointing at
97
+ // `#/components/schemas`, so every entry would look unused.
98
+ resolved: false,
99
+ given: "$.components.schemas",
100
+ severity: "warn",
101
+ then: {
102
+ function: "unreferencedReusableObject",
103
+ functionOptions: { reusableObjectsLocation: "#/components/schemas" }
104
+ }
105
+ },
106
+ "asyncapi-unused-components-server": {
107
+ description: "Potentially unused components server has been detected.",
108
+ resolved: false,
109
+ given: "$.components.servers",
110
+ severity: "warn",
111
+ then: {
112
+ function: "unreferencedReusableObject",
113
+ functionOptions: { reusableObjectsLocation: "#/components/servers" }
114
+ }
115
+ }
116
+ };
117
+ const v2Rules = {
118
+ "asyncapi-channel-no-empty-parameter": {
119
+ description: "Channel path must not have an empty parameter substitution pattern.",
120
+ formats: ["aas2"],
121
+ given: "$.channels",
122
+ severity: "warn",
123
+ then: { field: "@key", function: "pattern", functionOptions: { notMatch: "{}" } }
124
+ },
125
+ "asyncapi-channel-no-query-nor-fragment": {
126
+ description: 'Channel path must not include a query ("?") or fragment ("#") delimiter.',
127
+ formats: ["aas2"],
128
+ given: "$.channels",
129
+ severity: "warn",
130
+ then: { field: "@key", function: "pattern", functionOptions: { notMatch: "[\\?#]" } }
131
+ },
132
+ "asyncapi-channel-no-trailing-slash": {
133
+ description: "Channel path must not end with a slash.",
134
+ formats: ["aas2"],
135
+ given: "$.channels",
136
+ severity: "warn",
137
+ then: { field: "@key", function: "pattern", functionOptions: { notMatch: ".+\\/$" } }
138
+ },
139
+ "asyncapi-channel-servers": {
140
+ description: "Channel servers must be defined in the servers object.",
141
+ formats: ["aas2"],
142
+ // Unresolved: this rule reads what the author wrote — names, addresses,
143
+ // variables — and never needs to see through a `$ref`. Its given already
144
+ // covers the reusable location, so nothing is missed by not dereferencing.
145
+ resolved: false,
146
+ given: "$",
147
+ severity: "error",
148
+ then: { function: "asyncApiChannelServers" }
149
+ },
150
+ "asyncapi-headers-schema-type-object": {
151
+ description: 'Headers schema type must be "object".',
152
+ formats: ["aas2"],
153
+ given: V2_ALL_MESSAGES.map((given) => `${given}.headers`),
154
+ severity: "error",
155
+ then: { function: "asyncApiHeadersObject" }
156
+ },
157
+ "asyncapi-message-examples": {
158
+ description: "Message examples must be valid against the payload and headers schemas.",
159
+ formats: ["aas2"],
160
+ // Messages only. The pass folds traits in — including a `$ref`'d one, since
161
+ // this runs resolved — and reports against whichever array the merge took,
162
+ // so matching each trait location as well printed the identical finding
163
+ // twice. The one thing this misses is a `components.messageTraits` entry no
164
+ // message ever references, which is an unused component.
165
+ given: V2_MESSAGES,
166
+ severity: "error",
167
+ then: { function: "asyncApiMessageExamples" }
168
+ },
169
+ "asyncapi-message-messageId-uniqueness": {
170
+ description: "Every messageId must be unique.",
171
+ formats: ["aas2"],
172
+ // Unresolved: a reusable `components` entry reached by two `$ref`s appears
173
+ // twice in the dereferenced tree, and counting those as two declarations
174
+ // reported a duplicate id for a document that only ever declared one.
175
+ resolved: false,
176
+ given: "$",
177
+ severity: "error",
178
+ then: { function: "asyncApiMessageIdUnique" }
179
+ },
180
+ "asyncapi-operation-description": {
181
+ description: "Operation must have a description.",
182
+ formats: ["aas2"],
183
+ given: V2_OPERATIONS,
184
+ then: { field: "description", function: "truthy" }
185
+ },
186
+ "asyncapi-operation-operationId": {
187
+ description: "Operation must have an operationId.",
188
+ formats: ["aas2"],
189
+ given: V2_OPERATIONS,
190
+ severity: "error",
191
+ then: { field: "operationId", function: "truthy" }
192
+ },
193
+ "asyncapi-operation-operationId-uniqueness": {
194
+ description: "Every operationId must be unique.",
195
+ formats: ["aas2"],
196
+ // Unresolved: a reusable `components` entry reached by two `$ref`s appears
197
+ // twice in the dereferenced tree, and counting those as two declarations
198
+ // reported a duplicate id for a document that only ever declared one.
199
+ resolved: false,
200
+ given: "$",
201
+ severity: "error",
202
+ then: { function: "asyncApiOperationIdUnique" }
203
+ },
204
+ "asyncapi-operation-security": {
205
+ description: "Operation security must reference a defined security scheme.",
206
+ formats: ["aas2"],
207
+ given: [`${V2_OPERATIONS}.security[*]`, `${V2_COMPONENT_OPERATIONS}.security[*]`],
208
+ severity: "error",
209
+ then: { function: "asyncApiSecurity", functionOptions: { objectType: "Operation" } }
210
+ },
211
+ "asyncapi-payload": {
212
+ description: "Payloads must be valid against the AsyncAPI Schema object.",
213
+ formats: ["aas2"],
214
+ // Given the message rather than the payload, so the `schemaFormat` gate can
215
+ // fold traits in first. A trait-supplied `schemaFormat` is invisible to a
216
+ // `given` filter, which judged an Avro payload as an AsyncAPI Schema Object.
217
+ given: V2_ALL_MESSAGES,
218
+ severity: "error",
219
+ then: { function: "asyncApiPayload" }
220
+ },
221
+ "asyncapi-payload-default": {
222
+ description: "Payload default must be valid against its schema.",
223
+ formats: ["aas2"],
224
+ given: payloadSiblings("default"),
225
+ severity: "error",
226
+ then: { function: "asyncApiSchemaValidation", functionOptions: { type: "default" } }
227
+ },
228
+ "asyncapi-payload-examples": {
229
+ description: "Payload examples must be valid against their schema.",
230
+ formats: ["aas2"],
231
+ given: payloadSiblings("examples"),
232
+ severity: "error",
233
+ then: { function: "asyncApiSchemaValidation", functionOptions: { type: "examples" } }
234
+ },
235
+ "asyncapi-payload-unsupported-schemaFormat": {
236
+ description: "Message payload validation is only supported with an unspecified schemaFormat.",
237
+ formats: ["aas2"],
238
+ given: V2_ALL_MESSAGES,
239
+ severity: "info",
240
+ resolved: false,
241
+ // Only a format this package cannot validate is worth a note; naming the
242
+ // AsyncAPI dialect explicitly is not "unsupported".
243
+ then: {
244
+ field: "schemaFormat",
245
+ function: "pattern",
246
+ functionOptions: { match: "^application/vnd\\.aai\\.asyncapi([+;])" }
247
+ }
248
+ },
249
+ "asyncapi-schema": {
250
+ description: "Validate structure of AsyncAPI v2 specification.",
251
+ formats: ["aas2"],
252
+ severity: "error",
253
+ resolved: false,
254
+ given: "$",
255
+ then: { function: "asyncApiDocumentSchema" }
256
+ },
257
+ "asyncapi-schema-default": {
258
+ description: "Schema default must be valid against its schema.",
259
+ formats: ["aas2"],
260
+ given: [
261
+ "$.components.schemas[*].default^",
262
+ "$.components.parameters[*].schema.default^",
263
+ "$.channels[*].parameters[*].schema.default^"
264
+ ],
265
+ severity: "error",
266
+ then: { function: "asyncApiSchemaValidation", functionOptions: { type: "default" } }
267
+ },
268
+ "asyncapi-schema-examples": {
269
+ description: "Schema examples must be valid against their schema.",
270
+ formats: ["aas2"],
271
+ given: [
272
+ "$.components.schemas[*].examples^",
273
+ "$.components.parameters[*].schema.examples^",
274
+ "$.channels[*].parameters[*].schema.examples^"
275
+ ],
276
+ severity: "error",
277
+ then: { function: "asyncApiSchemaValidation", functionOptions: { type: "examples" } }
278
+ },
279
+ "asyncapi-server-no-empty-variable": {
280
+ description: "Server URL must not have an empty variable substitution pattern.",
281
+ formats: ["aas2"],
282
+ given: "$.servers[*].url",
283
+ severity: "warn",
284
+ then: { function: "pattern", functionOptions: { notMatch: "{}" } }
285
+ },
286
+ "asyncapi-server-no-trailing-slash": {
287
+ description: "Server URL must not end with a slash.",
288
+ formats: ["aas2"],
289
+ given: "$.servers[*].url",
290
+ severity: "warn",
291
+ then: { function: "pattern", functionOptions: { notMatch: "/$" } }
292
+ },
293
+ "asyncapi-server-not-example-com": {
294
+ description: "Server URL must not point at example.com.",
295
+ formats: ["aas2"],
296
+ given: "$.servers[*].url",
297
+ severity: "warn",
298
+ recommended: false,
299
+ then: { function: "pattern", functionOptions: { notMatch: "example\\.com" } }
300
+ },
301
+ "asyncapi-server-security": {
302
+ description: "Server security must reference a defined security scheme.",
303
+ formats: ["aas2"],
304
+ given: ["$.servers[*].security[*]", "$.components.servers[*].security[*]"],
305
+ severity: "error",
306
+ then: { function: "asyncApiSecurity", functionOptions: { objectType: "Server" } }
307
+ },
308
+ "asyncapi-server-variables": {
309
+ description: "Server variables must be defined, and none may be redundant.",
310
+ formats: ["aas2"],
311
+ // Unresolved: this rule reads what the author wrote — names, addresses,
312
+ // variables — and never needs to see through a `$ref`. Its given already
313
+ // covers the reusable location, so nothing is missed by not dereferencing.
314
+ resolved: false,
315
+ given: ["$.servers[*]", "$.components.servers[*]"],
316
+ severity: "error",
317
+ then: { function: "aasServerVariables" }
318
+ },
319
+ "asyncapi-tag-description": {
320
+ description: "Tag object should have a description.",
321
+ formats: ["aas2"],
322
+ given: "$.tags[*]",
323
+ severity: "warn",
324
+ recommended: false,
325
+ then: { field: "description", function: "truthy" }
326
+ },
327
+ "asyncapi-tags": {
328
+ description: "AsyncAPI object should have a non-empty tags array.",
329
+ formats: ["aas2"],
330
+ given: "$",
331
+ severity: "warn",
332
+ then: { field: "tags", function: "truthy" }
333
+ },
334
+ "asyncapi-tags-alphabetical": {
335
+ description: "Tags should be in alphabetical order.",
336
+ formats: ["aas2"],
337
+ given: "$",
338
+ severity: "warn",
339
+ recommended: false,
340
+ then: { field: "tags", function: "alphabetical", functionOptions: { keyedBy: "name" } }
341
+ },
342
+ "asyncapi-tags-uniqueness": {
343
+ description: "Tag names must be unique within each tags array.",
344
+ formats: ["aas2"],
345
+ // Unresolved: this rule reads what the author wrote — names, addresses,
346
+ // variables — and never needs to see through a `$ref`. Its given already
347
+ // covers the reusable location, so nothing is missed by not dereferencing.
348
+ resolved: false,
349
+ given: [
350
+ "$.tags",
351
+ "$.servers[*].tags",
352
+ "$.components.servers[*].tags",
353
+ `${V2_OPERATIONS}.tags`,
354
+ `${V2_COMPONENT_OPERATIONS}.tags`,
355
+ `${V2_OPERATIONS}.traits[*].tags`,
356
+ `${V2_COMPONENT_OPERATIONS}.traits[*].tags`,
357
+ "$.components.operationTraits[*].tags",
358
+ ...V2_ALL_MESSAGES.map((given) => `${given}.tags`)
359
+ ],
360
+ severity: "error",
361
+ then: { function: "aasTagsUnique" }
362
+ }
363
+ };
364
+ const V3_MESSAGES = ["$.channels[*].messages[*]", "$.components.channels[*].messages[*]", "$.components.messages[*]"];
365
+ const V3_MESSAGE_TRAITS = [
366
+ "$.channels[*].messages[*].traits[*]",
367
+ "$.components.channels[*].messages[*].traits[*]",
368
+ "$.components.messages[*].traits[*]",
369
+ "$.components.messageTraits[*]"
370
+ ];
371
+ const V3_ALL_MESSAGES = [...V3_MESSAGES, ...V3_MESSAGE_TRAITS];
372
+ const v3Rules = {
373
+ "asyncapi-3-channel-no-empty-parameter": {
374
+ description: "Channel address must not have an empty parameter substitution pattern.",
375
+ formats: ["aas3"],
376
+ given: "$.channels[*]",
377
+ severity: "warn",
378
+ then: { field: "address", function: "pattern", functionOptions: { notMatch: "{}" } }
379
+ },
380
+ "asyncapi-3-channel-no-query-nor-fragment": {
381
+ description: 'Channel address must not include a query ("?") or fragment ("#") delimiter.',
382
+ formats: ["aas3"],
383
+ given: "$.channels[*]",
384
+ severity: "warn",
385
+ then: { field: "address", function: "pattern", functionOptions: { notMatch: "[\\?#]" } }
386
+ },
387
+ "asyncapi-3-channel-no-trailing-slash": {
388
+ description: "Channel address must not end with a slash.",
389
+ formats: ["aas3"],
390
+ given: "$.channels[*]",
391
+ severity: "warn",
392
+ then: { field: "address", function: "pattern", functionOptions: { notMatch: ".+\\/$" } }
393
+ },
394
+ "asyncapi-3-channel-servers": {
395
+ // 3.0 replaced the 2.x list of server *names* with references, so the check
396
+ // is that each one points into `#/servers`. Unresolved, or the `$ref` this
397
+ // reads would already have been replaced by the server it names.
398
+ description: "Channel servers must reference servers defined in the servers object.",
399
+ formats: ["aas3"],
400
+ // Unresolved, or the `$ref` this reads would already have been replaced by
401
+ // the server it names.
402
+ resolved: false,
403
+ given: "$",
404
+ severity: "error",
405
+ then: { function: "asyncApiChannelServers" }
406
+ },
407
+ "asyncapi-3-document-unresolved": {
408
+ // One structural pass per major, against the document as written — see
409
+ // `asyncApiDocumentSchema`. Spectral pairs this with an
410
+ // `asyncapi-3-document-resolved` twin; running both here reported every
411
+ // structural error twice whenever a `$ref` resolver was injected, which the
412
+ // CLI always does.
413
+ description: "Validate structure of AsyncAPI v3 specification.",
414
+ formats: ["aas3"],
415
+ severity: "error",
416
+ resolved: false,
417
+ given: "$",
418
+ then: { function: "asyncApiDocumentSchema" }
419
+ },
420
+ "asyncapi-3-headers-schema-type-object": {
421
+ description: 'Headers schema type must be "object".',
422
+ formats: ["aas3"],
423
+ given: V3_ALL_MESSAGES.map((given) => `${given}.headers`),
424
+ severity: "error",
425
+ then: { function: "asyncApiHeadersObject", functionOptions: { multiFormat: true } }
426
+ },
427
+ "asyncapi-3-operation-description": {
428
+ description: "Operation must have a description.",
429
+ formats: ["aas3"],
430
+ given: "$.operations[*]",
431
+ then: { field: "description", function: "truthy" }
432
+ },
433
+ "asyncapi-3-operation-security": {
434
+ description: "Operation security must reference a defined security scheme.",
435
+ formats: ["aas3"],
436
+ resolved: false,
437
+ given: ["$.operations[*].security[*]", "$.components.operations[*].security[*]"],
438
+ severity: "error",
439
+ then: { function: "asyncApiSecurity", functionOptions: { objectType: "Operation" } }
440
+ },
441
+ "asyncapi-3-payload-unsupported-schemaFormat": {
442
+ description: "Message payload validation is only supported with an unspecified schemaFormat.",
443
+ formats: ["aas3"],
444
+ given: V3_ALL_MESSAGES.map((given) => `${given}.payload`),
445
+ severity: "info",
446
+ resolved: false,
447
+ then: {
448
+ field: "schemaFormat",
449
+ function: "pattern",
450
+ functionOptions: { match: "^application/vnd\\.aai\\.asyncapi([+;])" }
451
+ }
452
+ },
453
+ "asyncapi-3-server-security": {
454
+ // 3.0 keeps `security` on the Server Object, in the same
455
+ // Reference-or-inline-scheme shape `asyncapi-3-operation-security` checks.
456
+ description: "Server security must reference a defined security scheme.",
457
+ formats: ["aas3"],
458
+ resolved: false,
459
+ given: ["$.servers[*].security[*]", "$.components.servers[*].security[*]"],
460
+ severity: "error",
461
+ then: { function: "asyncApiSecurity", functionOptions: { objectType: "Server" } }
462
+ },
463
+ "asyncapi-3-server-variables": {
464
+ // The 2.x rule reads `url`; 3.0 splits the address into `host` and
465
+ // `pathname`, which the shared check now understands.
466
+ description: "Server variables must be defined, and none may be redundant.",
467
+ formats: ["aas3"],
468
+ resolved: false,
469
+ given: ["$.servers[*]", "$.components.servers[*]"],
470
+ severity: "error",
471
+ then: { function: "aasServerVariables", functionOptions: { addressFields: ["host", "pathname"] } }
472
+ },
473
+ "asyncapi-3-server-no-empty-variable": {
474
+ // 3.0 split the 2.x `url` into `host` and `pathname`, and either may be
475
+ // templated.
476
+ description: "Server host and pathname must not have an empty variable substitution pattern.",
477
+ formats: ["aas3"],
478
+ given: ["$.servers[*].host", "$.servers[*].pathname"],
479
+ severity: "warn",
480
+ then: { function: "pattern", functionOptions: { notMatch: "{}" } }
481
+ },
482
+ "asyncapi-3-server-no-trailing-slash": {
483
+ description: "Server host must not end with a slash.",
484
+ formats: ["aas3"],
485
+ given: "$.servers[*].host",
486
+ severity: "warn",
487
+ then: { function: "pattern", functionOptions: { notMatch: "/$" } }
488
+ },
489
+ "asyncapi-3-server-not-example-com": {
490
+ description: "Server host must not point at example.com.",
491
+ formats: ["aas3"],
492
+ given: "$.servers[*].host",
493
+ severity: "warn",
494
+ recommended: false,
495
+ then: { function: "pattern", functionOptions: { notMatch: "example\\.com" } }
496
+ },
497
+ "asyncapi-3-tag-description": {
498
+ description: "Tag object should have a description.",
499
+ formats: ["aas3"],
500
+ given: "$.info.tags[*]",
501
+ severity: "warn",
502
+ recommended: false,
503
+ then: { field: "description", function: "truthy" }
504
+ },
505
+ "asyncapi-3-tags": {
506
+ // 3.0 moved the document's tags under `info`.
507
+ description: "Info object should have a non-empty tags array.",
508
+ formats: ["aas3"],
509
+ given: "$.info",
510
+ severity: "warn",
511
+ then: { field: "tags", function: "truthy" }
512
+ },
513
+ "asyncapi-3-tags-alphabetical": {
514
+ description: "Tags should be in alphabetical order.",
515
+ formats: ["aas3"],
516
+ given: "$.info",
517
+ severity: "warn",
518
+ recommended: false,
519
+ then: { field: "tags", function: "alphabetical", functionOptions: { keyedBy: "name" } }
520
+ },
521
+ "asyncapi-3-tags-uniqueness": {
522
+ description: "Tag names must be unique within each tags array.",
523
+ formats: ["aas3"],
524
+ // Unresolved: this rule reads what the author wrote — names, addresses,
525
+ // variables — and never needs to see through a `$ref`. Its given already
526
+ // covers the reusable location, so nothing is missed by not dereferencing.
527
+ resolved: false,
528
+ given: [
529
+ "$.info.tags",
530
+ "$.servers[*].tags",
531
+ "$.components.servers[*].tags",
532
+ // The Channel Object carries `tags` in 3.0 too, and it is the most obvious
533
+ // place for a duplicate to be written.
534
+ "$.channels[*].tags",
535
+ "$.components.channels[*].tags",
536
+ "$.operations[*].tags",
537
+ "$.components.operations[*].tags",
538
+ "$.operations[*].traits[*].tags",
539
+ "$.components.operations[*].traits[*].tags",
540
+ "$.components.operationTraits[*].tags",
541
+ "$.channels[*].messages[*].tags",
542
+ "$.channels[*].messages[*].traits[*].tags",
543
+ "$.components.channels[*].messages[*].tags",
544
+ "$.components.channels[*].messages[*].traits[*].tags",
545
+ "$.components.messages[*].tags",
546
+ "$.components.messages[*].traits[*].tags",
547
+ "$.components.messageTraits[*].tags"
548
+ ],
549
+ severity: "error",
550
+ then: { function: "aasTagsUnique" }
551
+ }
552
+ };
553
+ const asyncapi = {
554
+ formats: ["aas2", "aas3"],
555
+ rules: {
556
+ ...sharedRules,
557
+ ...v2Rules,
558
+ ...v3Rules
559
+ }
560
+ };
561
+ export {
562
+ asyncapi
563
+ };
@@ -0,0 +1,23 @@
1
+ import type { Format } from '../../core/index.js';
2
+ /** Matches any AsyncAPI 2.x (`asyncapi: 2.*`). */
3
+ export declare const aas2: Format;
4
+ /** Matches AsyncAPI 2.0.x specifically. */
5
+ export declare const aas2_0: Format;
6
+ /** Matches AsyncAPI 2.1.x specifically. */
7
+ export declare const aas2_1: Format;
8
+ /** Matches AsyncAPI 2.2.x specifically. */
9
+ export declare const aas2_2: Format;
10
+ /** Matches AsyncAPI 2.3.x specifically. */
11
+ export declare const aas2_3: Format;
12
+ /** Matches AsyncAPI 2.4.x specifically. */
13
+ export declare const aas2_4: Format;
14
+ /** Matches AsyncAPI 2.5.x specifically. */
15
+ export declare const aas2_5: Format;
16
+ /** Matches AsyncAPI 2.6.x specifically. */
17
+ export declare const aas2_6: Format;
18
+ /** Matches any AsyncAPI 3.x (`asyncapi: 3.*`). */
19
+ export declare const aas3: Format;
20
+ /** Matches AsyncAPI 3.0.x specifically. */
21
+ export declare const aas3_0: Format;
22
+ /** AsyncAPI format detectors keyed by Loupe-compatible names. */
23
+ export declare const aasFormats: Record<string, Format>;
@@ -0,0 +1,49 @@
1
+ const isObject = (value) => typeof value === "object" && value !== null;
2
+ const asyncApiVersion = (document) => isObject(document) && typeof document["asyncapi"] === "string" ? document["asyncapi"] : void 0;
3
+ const matchesMinor = (document, major, minor) => {
4
+ const version = asyncApiVersion(document);
5
+ return version !== void 0 && new RegExp(`^${major}\\.${minor}(\\.|$)`).test(version);
6
+ };
7
+ const aas2 = (document) => /^2\.\d/.test(asyncApiVersion(document) ?? "");
8
+ const aas2_0 = (document) => matchesMinor(document, 2, 0);
9
+ const aas2_1 = (document) => matchesMinor(document, 2, 1);
10
+ const aas2_2 = (document) => matchesMinor(document, 2, 2);
11
+ const aas2_3 = (document) => matchesMinor(document, 2, 3);
12
+ const aas2_4 = (document) => matchesMinor(document, 2, 4);
13
+ const aas2_5 = (document) => matchesMinor(document, 2, 5);
14
+ const aas2_6 = (document) => matchesMinor(document, 2, 6);
15
+ const aas3 = (document) => /^3\.\d/.test(asyncApiVersion(document) ?? "");
16
+ const aas3_0 = (document) => matchesMinor(document, 3, 0);
17
+ const aasFormats = {
18
+ aas2,
19
+ "aas2.0": aas2_0,
20
+ aas2_0,
21
+ "aas2.1": aas2_1,
22
+ aas2_1,
23
+ "aas2.2": aas2_2,
24
+ aas2_2,
25
+ "aas2.3": aas2_3,
26
+ aas2_3,
27
+ "aas2.4": aas2_4,
28
+ aas2_4,
29
+ "aas2.5": aas2_5,
30
+ aas2_5,
31
+ "aas2.6": aas2_6,
32
+ aas2_6,
33
+ aas3,
34
+ "aas3.0": aas3_0,
35
+ aas3_0
36
+ };
37
+ export {
38
+ aas2,
39
+ aas2_0,
40
+ aas2_1,
41
+ aas2_2,
42
+ aas2_3,
43
+ aas2_4,
44
+ aas2_5,
45
+ aas2_6,
46
+ aas3,
47
+ aas3_0,
48
+ aasFormats
49
+ };
@@ -0,0 +1,8 @@
1
+ import { type IServerVariablesOptions } from '../../shared/server-variables.js';
2
+ /**
3
+ * Validates a 2.x Server Object's `variables`. The AsyncAPI and OpenAPI Server
4
+ * Objects are the same shape here, so the implementation is shared — see
5
+ * {@link serverVariables}.
6
+ */
7
+ export declare const aasServerVariables: import("../../../core/types.js").RulesetFunction<unknown, IServerVariablesOptions | undefined>;
8
+ export type { IServerVariablesOptions };
@@ -0,0 +1,5 @@
1
+ import { serverVariables } from "../../shared/server-variables.js";
2
+ const aasServerVariables = serverVariables;
3
+ export {
4
+ aasServerVariables
5
+ };
@@ -0,0 +1,2 @@
1
+ /** Flags duplicate tag names within a tags array. Shared with OpenAPI — see {@link tagsUnique}. */
2
+ export declare const aasTagsUnique: import("../../../core/types.js").RulesetFunction;
@@ -0,0 +1,5 @@
1
+ import { tagsUnique } from "../../shared/tags-unique.js";
2
+ const aasTagsUnique = tagsUnique;
3
+ export {
4
+ aasTagsUnique
5
+ };
@@ -0,0 +1,7 @@
1
+ import type { RulesetFunction } from '../../../core/types.js';
2
+ /**
3
+ * Every `{parameter}` in a channel's address must be described in its
4
+ * `parameters` object, and nothing may be described that the address does not
5
+ * use.
6
+ */
7
+ export declare const asyncApiChannelParameters: RulesetFunction;