api-quality-spectral-ruleset 1.5.0 → 1.6.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apq-spectral.yaml +62 -60
- package/functions/apq-check-examples-coverage.js +11 -18
- package/functions/apq-compare-insensitive.js +4 -3
- package/functions/apq-password-format.js +5 -2
- package/functions/apq-path-param-query-conflict.js +20 -3
- package/functions/apq-post-201-location-header.js +36 -0
- package/functions/apq-require-response-on-path-params.js +6 -4
- package/functions/apq-response-content-required.js +24 -0
- package/functions/apq-response-media-type.js +22 -19
- package/functions/apq-response-no-content.js +27 -0
- package/functions/apq-string-parameter-integrity.js +10 -0
- package/functions/apq-tags-consistency.js +71 -0
- package/functions/apq-total-param-default-value.js +24 -0
- package/functions/apq-undefined-response-media-type.js +46 -0
- package/functions/apq-valid-response-schema.js +71 -20
- package/functions/apq-validate-structure.js +34 -5
- package/functions/apq-wso2-auth-type-required.js +134 -0
- package/functions/apq-wso2-scope-defined.js +271 -0
- package/package.json +1 -1
package/apq-spectral.yaml
CHANGED
|
@@ -18,8 +18,11 @@ functions:
|
|
|
18
18
|
- apq-path-depth
|
|
19
19
|
- apq-forbidden-characters
|
|
20
20
|
- apq-valid-response-schema
|
|
21
|
+
- apq-post-201-location-header
|
|
22
|
+
- apq-response-no-content
|
|
21
23
|
- apq-valid-openapi-version
|
|
22
24
|
- apq-collection-query-param-required
|
|
25
|
+
- apq-total-param-default-value
|
|
23
26
|
- apq-path-param-query-conflict
|
|
24
27
|
- apq-schema-format
|
|
25
28
|
- apq-security-required-response
|
|
@@ -45,9 +48,11 @@ functions:
|
|
|
45
48
|
- apq-mandatory-response-codes
|
|
46
49
|
- apq-rate-limit-response
|
|
47
50
|
- apq-forbidden-query-format
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
+
- apq-wso2-scope-defined
|
|
52
|
+
- apq-undefined-response-media-type
|
|
53
|
+
- apq-wso2-auth-type-required
|
|
54
|
+
- apq-response-content-required
|
|
55
|
+
- apq-tags-consistency
|
|
51
56
|
rules:
|
|
52
57
|
apiq:OAR001:
|
|
53
58
|
description: "For security reasons and as a REST best practice, the HTTPS protocol is mandatory."
|
|
@@ -89,17 +94,13 @@ rules:
|
|
|
89
94
|
pattern: "^[a-zA-Z0-9_\\-., ]+$"
|
|
90
95
|
apiq:OAR005:
|
|
91
96
|
description: "A wrong scope may cause problems to import the API definition into WSO2 or allow all users to call the endpoint."
|
|
92
|
-
message: "
|
|
97
|
+
message: "{{error}}"
|
|
98
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR005.md"
|
|
93
99
|
severity: "error"
|
|
94
|
-
|
|
100
|
+
resolved: false
|
|
101
|
+
given: "$"
|
|
95
102
|
then:
|
|
96
|
-
|
|
97
|
-
message: "Scope must be defined in the operation."
|
|
98
|
-
- function: enumeration
|
|
99
|
-
functionOptions:
|
|
100
|
-
values:
|
|
101
|
-
- read
|
|
102
|
-
- write
|
|
103
|
+
function: apq-wso2-scope-defined
|
|
103
104
|
apiq:OAR006:
|
|
104
105
|
description: "Routes must define request media types supported by the API."
|
|
105
106
|
message: "OAR006: Specify at least one Media Type in the content of the request body."
|
|
@@ -116,10 +117,13 @@ rules:
|
|
|
116
117
|
message: "OAR007: Specify at least one Media Type in the content of the response body."
|
|
117
118
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR007.md"
|
|
118
119
|
severity: "error"
|
|
119
|
-
given:
|
|
120
|
+
given:
|
|
121
|
+
- "$.paths[*][post,put,patch]"
|
|
122
|
+
- "$.webhooks[*][post,put,patch]"
|
|
123
|
+
- "$.paths[*][get,post,put,patch,delete].responses[?(@property != '204')]"
|
|
124
|
+
- "$.webhooks[*][get,post,put,patch,delete].responses[?(@property != '204')]"
|
|
120
125
|
then:
|
|
121
|
-
|
|
122
|
-
function: truthy
|
|
126
|
+
function: apq-undefined-response-media-type
|
|
123
127
|
apiq:OAR008:
|
|
124
128
|
description: "HTTP verbs not encouraged."
|
|
125
129
|
message: "{{error}}"
|
|
@@ -142,7 +146,13 @@ rules:
|
|
|
142
146
|
description: "Default response media type should be defined for responses."
|
|
143
147
|
message: "OAR010: Default response media type is mandatory."
|
|
144
148
|
severity: warn
|
|
145
|
-
given:
|
|
149
|
+
given:
|
|
150
|
+
- "$.paths[*][post,put,patch]"
|
|
151
|
+
- "$.webhooks[*][post,put,patch]"
|
|
152
|
+
- "$.paths[*][get,post,put,patch,delete].responses[?(@property != '204')]"
|
|
153
|
+
- "$.webhooks[*][get,post,put,patch,delete].responses[?(@property != '204')]"
|
|
154
|
+
- "$.paths[*][get,post,put,patch,delete].responses[?(@property != '204')].content"
|
|
155
|
+
- "$.webhooks[*][get,post,put,patch,delete].responses[?(@property != '204')].content"
|
|
146
156
|
then:
|
|
147
157
|
function: apq-response-media-type
|
|
148
158
|
functionOptions:
|
|
@@ -350,20 +360,18 @@ rules:
|
|
|
350
360
|
pathValidationStrategy: "/include"
|
|
351
361
|
apiq:OAR026:
|
|
352
362
|
description: "The $total parameter default value should be false."
|
|
353
|
-
message: "
|
|
363
|
+
message: "{{error}}"
|
|
354
364
|
severity: error
|
|
355
365
|
given: "$.paths[*].get.parameters[?(@.name == '$total' && @.in == 'query')]"
|
|
356
366
|
then:
|
|
357
|
-
|
|
358
|
-
function: falsy
|
|
367
|
+
function: apq-total-param-default-value
|
|
359
368
|
apiq:OAR027:
|
|
360
369
|
description: "Location header is required in responses with code 201 from POST operations."
|
|
361
|
-
message: "
|
|
370
|
+
message: "{{error}}"
|
|
362
371
|
severity: error
|
|
363
372
|
given: "$.paths.*.post.responses['201']"
|
|
364
373
|
then:
|
|
365
|
-
|
|
366
|
-
function: truthy
|
|
374
|
+
function: apq-post-201-location-header
|
|
367
375
|
apiq:OAR028:
|
|
368
376
|
description: "$filter must be defined as a query parameter in the collection GET operations selected by the configured paths."
|
|
369
377
|
message: "{{error}}"
|
|
@@ -471,9 +479,7 @@ rules:
|
|
|
471
479
|
description: "The 201 response schema of a POST operation must have properties named 'data' or 'error' with at least one sub-property."
|
|
472
480
|
message: "{{error}}"
|
|
473
481
|
severity: error
|
|
474
|
-
given:
|
|
475
|
-
- "$.paths.*.post.responses.*.content..schema.properties"
|
|
476
|
-
- "$.paths.*.post.responses.*.schema.properties"
|
|
482
|
+
given: "$.paths.*.post.responses['201']"
|
|
477
483
|
then:
|
|
478
484
|
function: apq-valid-response-schema
|
|
479
485
|
functionOptions:
|
|
@@ -524,13 +530,14 @@ rules:
|
|
|
524
530
|
functionOptions:
|
|
525
531
|
pattern: "^[a-zA-Z]{4,}_(SC|sc)_[a-zA-Z0-9]{1,}$"
|
|
526
532
|
apiq:OAR041:
|
|
527
|
-
description:
|
|
528
|
-
message: "
|
|
533
|
+
description: "A WSO2 x-scope on an operation always requires an x-auth-type definition."
|
|
534
|
+
message: "{{error}}"
|
|
535
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR041.md"
|
|
529
536
|
severity: "error"
|
|
530
|
-
|
|
537
|
+
resolved: false
|
|
538
|
+
given: "$"
|
|
531
539
|
then:
|
|
532
|
-
|
|
533
|
-
function: truthy
|
|
540
|
+
function: apq-wso2-auth-type-required
|
|
534
541
|
apiq:OAR042:
|
|
535
542
|
description: "Base path must be compliant with the standard."
|
|
536
543
|
message: "OAR042: Base path must follow the '/api-<name>/v<version>' standard."
|
|
@@ -545,12 +552,19 @@ rules:
|
|
|
545
552
|
apiq:OAR043:
|
|
546
553
|
description: "OpenAPI definition contains structural errors that would be detected by a strict parser or validator."
|
|
547
554
|
message: "{{error}}"
|
|
555
|
+
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR043.md"
|
|
548
556
|
severity: error
|
|
549
557
|
given:
|
|
550
558
|
- $.paths.*.*.parameters.*.in
|
|
551
559
|
- $.components.parameters.*.in
|
|
560
|
+
- $.paths.*.*.parameters.*.type
|
|
561
|
+
- $.components.parameters.*.type
|
|
552
562
|
- $.paths.*.*.parameters.*.schema.type
|
|
553
563
|
- $.components.parameters.*.schema.type
|
|
564
|
+
- $.webhooks.*.*.parameters.*.in
|
|
565
|
+
- $.webhooks.*.*.parameters.*.schema.type
|
|
566
|
+
- $.components.pathItems.*.*.parameters.*.in
|
|
567
|
+
- $.components.pathItems.*.*.parameters.*.schema.type
|
|
554
568
|
then:
|
|
555
569
|
function: apq-validate-structure
|
|
556
570
|
apiq:OAR044:
|
|
@@ -575,8 +589,7 @@ rules:
|
|
|
575
589
|
severity: error
|
|
576
590
|
given: "$.paths[*][get,post,put,patch,delete].responses[?(@property !== '204')]"
|
|
577
591
|
then:
|
|
578
|
-
|
|
579
|
-
function: truthy
|
|
592
|
+
function: apq-response-content-required
|
|
580
593
|
apiq:OAR046:
|
|
581
594
|
description: "Each operation SHOULD have a tag."
|
|
582
595
|
message: "OAR046: You should categorize the operations of your contract with tags."
|
|
@@ -598,48 +611,28 @@ rules:
|
|
|
598
611
|
type: array
|
|
599
612
|
minItems: 1
|
|
600
613
|
apiq:OAR047:
|
|
601
|
-
description: "Tags required and each tag must have a short description."
|
|
602
|
-
message: "
|
|
614
|
+
description: "Tags required and each tag must have a short description, must not be duplicated, and every tag used by an operation must be declared at the top level."
|
|
615
|
+
message: "{{error}}"
|
|
603
616
|
severity: error
|
|
604
617
|
given: "$"
|
|
605
618
|
then:
|
|
606
|
-
|
|
607
|
-
function: schema
|
|
608
|
-
functionOptions:
|
|
609
|
-
schema:
|
|
610
|
-
type: array
|
|
611
|
-
minLength: 1
|
|
612
|
-
items:
|
|
613
|
-
type: object
|
|
614
|
-
required:
|
|
615
|
-
- name
|
|
616
|
-
- description
|
|
617
|
-
properties:
|
|
618
|
-
name:
|
|
619
|
-
type: string
|
|
620
|
-
minLength: 1
|
|
621
|
-
description:
|
|
622
|
-
type: string
|
|
623
|
-
minLength: 1
|
|
619
|
+
function: apq-tags-consistency
|
|
624
620
|
apiq:OAR048:
|
|
625
621
|
description: APIs must define at most one body parameter.
|
|
626
622
|
message: "OAR048: An operation can have at most one body parameter"
|
|
627
623
|
documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR048.md"
|
|
628
624
|
severity: error
|
|
629
|
-
formats:
|
|
630
|
-
- oas2
|
|
631
625
|
given: "$.paths[*][*]"
|
|
632
626
|
then:
|
|
633
627
|
field: parameters
|
|
634
628
|
function: apq-at-most-one-body-parameter
|
|
635
629
|
apiq:OAR049:
|
|
636
630
|
description: "204 No Content MUST NOT return any content."
|
|
637
|
-
message: "
|
|
631
|
+
message: "{{error}}"
|
|
638
632
|
severity: error
|
|
639
633
|
given: "$.paths[*][get,post,put,patch,delete].responses['204']"
|
|
640
634
|
then:
|
|
641
|
-
|
|
642
|
-
function: falsy
|
|
635
|
+
function: apq-response-no-content
|
|
643
636
|
apiq:OAR050:
|
|
644
637
|
description: "Provide a summary for each operation."
|
|
645
638
|
message: "OAR050: Provide a summary for each operation."
|
|
@@ -650,7 +643,7 @@ rules:
|
|
|
650
643
|
function: truthy
|
|
651
644
|
apiq:OAR051:
|
|
652
645
|
description: "Summary and description must be different - not just case variations or semantic duplicates."
|
|
653
|
-
message: "
|
|
646
|
+
message: "{{error}}"
|
|
654
647
|
severity: "error"
|
|
655
648
|
given: "$.paths[*][get,post,put,patch,delete]"
|
|
656
649
|
then:
|
|
@@ -856,6 +849,10 @@ rules:
|
|
|
856
849
|
given:
|
|
857
850
|
- "$.paths[*][*].parameters[*]"
|
|
858
851
|
- "$.paths[*].parameters[*]"
|
|
852
|
+
- "$.paths[*].additionalOperations[*].parameters[*]"
|
|
853
|
+
- "$.webhooks[*][*].parameters[*]"
|
|
854
|
+
- "$.webhooks[*].parameters[*]"
|
|
855
|
+
- "$.webhooks[*].additionalOperations[*].parameters[*]"
|
|
859
856
|
- "$.components.parameters[*]"
|
|
860
857
|
- "$.parameters[*]"
|
|
861
858
|
then:
|
|
@@ -875,7 +872,9 @@ rules:
|
|
|
875
872
|
description: "All parameters in query must be snake_case."
|
|
876
873
|
message: "OAR077: All parameters in query must be snake_case."
|
|
877
874
|
severity: warn
|
|
878
|
-
given:
|
|
875
|
+
given:
|
|
876
|
+
- "$.paths[*][*].parameters[?(@.in == 'query')]"
|
|
877
|
+
- "$.paths[*].parameters[?(@.in == 'query')]"
|
|
879
878
|
then:
|
|
880
879
|
field: "name"
|
|
881
880
|
function: pattern
|
|
@@ -940,7 +939,9 @@ rules:
|
|
|
940
939
|
description: "Some formats should not pass through this querystring."
|
|
941
940
|
message: "{{error}}"
|
|
942
941
|
severity: error
|
|
943
|
-
given:
|
|
942
|
+
given:
|
|
943
|
+
- "$.paths[*][get,post,put,patch,delete].parameters[?(@.in == 'query')]"
|
|
944
|
+
- "$.paths[*].parameters[?(@.in == 'query')]"
|
|
944
945
|
then:
|
|
945
946
|
function: apq-forbidden-query-format
|
|
946
947
|
functionOptions:
|
|
@@ -989,6 +990,7 @@ rules:
|
|
|
989
990
|
description: "The $ref of a request body must end with the suffix Body."
|
|
990
991
|
message: "OAR089: The $ref of a request body must end with the suffix Body."
|
|
991
992
|
severity: "warn"
|
|
993
|
+
resolved: false
|
|
992
994
|
given: "$..requestBody.$ref"
|
|
993
995
|
then:
|
|
994
996
|
function: pattern
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
const COMBINERS = ['allOf', 'oneOf', 'anyOf'];
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const typeIncludes = (type, name) => (Array.isArray(type) ? type.includes(name) : type === name);
|
|
4
|
+
|
|
4
5
|
const isOn = (value) => value !== false;
|
|
5
6
|
|
|
6
|
-
// Whole-body (root) example declared directly on a schema node.
|
|
7
7
|
const schemaHasRootExample = (schema) =>
|
|
8
8
|
!!schema && typeof schema === 'object'
|
|
9
9
|
&& (schema.example !== undefined || schema.examples !== undefined);
|
|
10
10
|
|
|
11
|
-
// Parameter-level example: param.example/examples, the param schema's root example,
|
|
12
|
-
// or a media-type example under param.content (OAS3). OAS2 params use param.example.
|
|
13
11
|
const hasParameterExample = (param) => {
|
|
14
12
|
if (!param || typeof param !== 'object') return false;
|
|
15
13
|
if (param.example !== undefined || param.examples !== undefined) return true;
|
|
@@ -23,9 +21,6 @@ const hasParameterExample = (param) => {
|
|
|
23
21
|
return false;
|
|
24
22
|
};
|
|
25
23
|
|
|
26
|
-
// Body-level (whole response / requestBody) example: a media-type example/examples or a
|
|
27
|
-
// root schema example. OAS3 -> content.<mt>.{example|examples|schema.example}.
|
|
28
|
-
// OAS2 -> node.examples map or node.schema root example.
|
|
29
24
|
const hasBodyLevelExample = (node) => {
|
|
30
25
|
if (!node || typeof node !== 'object') return false;
|
|
31
26
|
if (node.content && typeof node.content === 'object') {
|
|
@@ -34,16 +29,15 @@ const hasBodyLevelExample = (node) => {
|
|
|
34
29
|
&& (mt.example !== undefined || mt.examples !== undefined || schemaHasRootExample(mt.schema)),
|
|
35
30
|
);
|
|
36
31
|
}
|
|
37
|
-
if (node.examples !== undefined) return true;
|
|
38
|
-
if (schemaHasRootExample(node.schema)) return true;
|
|
32
|
+
if (node.examples !== undefined) return true;
|
|
33
|
+
if (schemaHasRootExample(node.schema)) return true;
|
|
39
34
|
return false;
|
|
40
35
|
};
|
|
41
36
|
|
|
42
|
-
// Per-leaf-property example check (recurses objects, arrays and combiners).
|
|
43
37
|
const collectPropertyIssues = (schema, issues, basePath) => {
|
|
44
38
|
if (!schema || typeof schema !== 'object' || schema.$ref) return;
|
|
45
39
|
|
|
46
|
-
if (schema.type
|
|
40
|
+
if (typeIncludes(schema.type, 'array')) {
|
|
47
41
|
if (schema.items) collectPropertyIssues(schema.items, issues, [...basePath, 'items']);
|
|
48
42
|
return;
|
|
49
43
|
}
|
|
@@ -53,7 +47,7 @@ const collectPropertyIssues = (schema, issues, basePath) => {
|
|
|
53
47
|
if (!propSchema || typeof propSchema !== 'object' || propSchema.$ref) continue;
|
|
54
48
|
const propPath = [...basePath, 'properties', propName];
|
|
55
49
|
const propType = propSchema.type;
|
|
56
|
-
if (propType
|
|
50
|
+
if (typeIncludes(propType, 'object') || typeIncludes(propType, 'array') || (!propType && propSchema.properties)) {
|
|
57
51
|
collectPropertyIssues(propSchema, issues, propPath);
|
|
58
52
|
} else if (propType !== undefined && propSchema.example === undefined && propSchema.examples === undefined) {
|
|
59
53
|
issues.push({
|
|
@@ -71,7 +65,6 @@ const collectPropertyIssues = (schema, issues, basePath) => {
|
|
|
71
65
|
});
|
|
72
66
|
};
|
|
73
67
|
|
|
74
|
-
// Property-level coverage for a response / requestBody node (OAS3 content.* or OAS2 schema).
|
|
75
68
|
const collectBodyProperties = (node, issues, basePath) => {
|
|
76
69
|
if (node.content && typeof node.content === 'object') {
|
|
77
70
|
Object.entries(node.content).forEach(([mediaType, mt]) => {
|
|
@@ -83,10 +76,6 @@ const collectBodyProperties = (node, issues, basePath) => {
|
|
|
83
76
|
};
|
|
84
77
|
|
|
85
78
|
/**
|
|
86
|
-
* OAR031 — examples coverage, validated independently per level. Each level can be
|
|
87
|
-
* switched off via functionOptions (all default on):
|
|
88
|
-
* validateResponse, validateRequestBody, validateParameter, validateProperty
|
|
89
|
-
*
|
|
90
79
|
* @param {object} given
|
|
91
80
|
* @param {object} options
|
|
92
81
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
@@ -106,13 +95,17 @@ module.exports = function oar031ExamplesCoverage(given, options, context) {
|
|
|
106
95
|
const isRequestBody = nodePath.at(-1) === 'requestBody';
|
|
107
96
|
|
|
108
97
|
if (isParameter) {
|
|
98
|
+
const doc = context.document?.data ?? {};
|
|
99
|
+
const isOAP2 = typeof doc.swagger === 'string';
|
|
100
|
+
if (isOAP2 && given.in !== 'body') {
|
|
101
|
+
return issues;
|
|
102
|
+
}
|
|
109
103
|
if (validateParameter && !hasParameterExample(given)) {
|
|
110
104
|
issues.push({
|
|
111
105
|
message: `OAR031: Parameter '${given.name || ''}' must have an example defined`,
|
|
112
106
|
path: nodePath,
|
|
113
107
|
});
|
|
114
108
|
}
|
|
115
|
-
// OAS2 body parameters carry a schema, so property-level coverage applies to them too.
|
|
116
109
|
if (given.in === 'body' && validateProperty && given.schema) {
|
|
117
110
|
collectPropertyIssues(given.schema, issues, [...nodePath, 'schema']);
|
|
118
111
|
}
|
|
@@ -26,12 +26,13 @@ module.exports = (given, options, context) => {
|
|
|
26
26
|
return errors;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const threshold = options.threshold
|
|
29
|
+
const threshold = typeof options.threshold === 'number' ? options.threshold : 0.6;
|
|
30
|
+
const baseMessage = 'OAR051: Summary and description must be meaningfully different from each other.';
|
|
30
31
|
|
|
31
32
|
// Exact match (case-insensitive)
|
|
32
33
|
if (propA.trim().toUpperCase() === propB.trim().toUpperCase()) {
|
|
33
34
|
errors.push({
|
|
34
|
-
message:
|
|
35
|
+
message: baseMessage,
|
|
35
36
|
path: [...context.path, options.property]
|
|
36
37
|
});
|
|
37
38
|
return errors;
|
|
@@ -41,7 +42,7 @@ module.exports = (given, options, context) => {
|
|
|
41
42
|
const similarity = calculateSimilarity(propA, propB);
|
|
42
43
|
if (similarity >= threshold) {
|
|
43
44
|
errors.push({
|
|
44
|
-
message: `${
|
|
45
|
+
message: `${baseMessage} (${Math.round(similarity * 100)}% similar)`,
|
|
45
46
|
path: [...context.path, options.property]
|
|
46
47
|
});
|
|
47
48
|
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* @param {object} options - Function options
|
|
5
5
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
6
6
|
*/
|
|
7
|
+
function typeIncludes(type, name) {
|
|
8
|
+
return Array.isArray(type) ? type.includes(name) : type === name;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
module.exports = (given, options, context) => {
|
|
8
12
|
const results = [];
|
|
9
13
|
|
|
@@ -17,10 +21,9 @@ module.exports = (given, options, context) => {
|
|
|
17
21
|
return;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
// Check if this is a password-related field (name contains "password")
|
|
21
24
|
const isPasswordField = propName.toLowerCase().includes('password');
|
|
22
25
|
|
|
23
|
-
if (isPasswordField && propSchema.type
|
|
26
|
+
if (isPasswordField && typeIncludes(propSchema.type, 'string')) {
|
|
24
27
|
// Password fields must have format: password
|
|
25
28
|
if (propSchema.format !== 'password') {
|
|
26
29
|
results.push({
|
|
@@ -18,14 +18,16 @@ module.exports = (given, options, context) => {
|
|
|
18
18
|
continue;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const sharedParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
|
|
22
|
+
|
|
21
23
|
for (const [operationKey, operation] of Object.entries(pathItem)) {
|
|
22
24
|
if (!operation || typeof operation !== 'object' ||
|
|
23
25
|
!httpMethods.includes(operationKey)) {
|
|
24
26
|
continue;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
+
const operationParams = Array.isArray(operation.parameters) ? operation.parameters : [];
|
|
30
|
+
if (sharedParams.length === 0 && operationParams.length === 0) {
|
|
29
31
|
continue;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -35,7 +37,13 @@ module.exports = (given, options, context) => {
|
|
|
35
37
|
continue;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
const overridden = new Set(
|
|
41
|
+
operationParams
|
|
42
|
+
.filter((p) => p && p.name && p.in)
|
|
43
|
+
.map((p) => `${p.in}:${p.name}`)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
operationParams.forEach((param, index) => {
|
|
39
47
|
if (param && (param.in === 'path' || param.in === 'query')) {
|
|
40
48
|
errors.push({
|
|
41
49
|
message: context.rule.message,
|
|
@@ -43,6 +51,15 @@ module.exports = (given, options, context) => {
|
|
|
43
51
|
});
|
|
44
52
|
}
|
|
45
53
|
});
|
|
54
|
+
|
|
55
|
+
sharedParams.forEach((param, index) => {
|
|
56
|
+
if (!param || (param.in !== 'path' && param.in !== 'query')) return;
|
|
57
|
+
if (overridden.has(`${param.in}:${param.name}`)) return;
|
|
58
|
+
errors.push({
|
|
59
|
+
message: context.rule.message,
|
|
60
|
+
path: [...context.path, pathKey, 'parameters', index]
|
|
61
|
+
});
|
|
62
|
+
});
|
|
46
63
|
}
|
|
47
64
|
}
|
|
48
65
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given - the 201 response object of a POST operation
|
|
3
|
+
* @param {object} options - unused
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (!given || typeof given !== 'object') {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const doc = context.document?.data ?? {};
|
|
12
|
+
const isOAP2 = typeof doc.swagger === 'string';
|
|
13
|
+
const path = context.path || [];
|
|
14
|
+
const message = 'OAR027: Location header is required in responses with code 201 from POST operations.';
|
|
15
|
+
|
|
16
|
+
const headers = given.headers;
|
|
17
|
+
const headerKey = headers && typeof headers === 'object'
|
|
18
|
+
? Object.keys(headers).find((key) => key.toLowerCase() === 'location')
|
|
19
|
+
: undefined;
|
|
20
|
+
|
|
21
|
+
if (!headerKey) {
|
|
22
|
+
return [{ message, path: [...path, 'headers'] }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const header = headers[headerKey];
|
|
26
|
+
const type = isOAP2 ? header && header.type : header && header.schema && header.schema.type;
|
|
27
|
+
|
|
28
|
+
if (!type) {
|
|
29
|
+
const typePath = isOAP2
|
|
30
|
+
? [...path, 'headers', headerKey, 'type']
|
|
31
|
+
: [...path, 'headers', headerKey, 'schema', 'type'];
|
|
32
|
+
return [{ message, path: typePath }];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [];
|
|
36
|
+
};
|
|
@@ -20,10 +20,12 @@ module.exports = (given, options, context) => {
|
|
|
20
20
|
const shouldExclude = strategy === '/exclude' ? isListed : !isListed;
|
|
21
21
|
if (shouldExclude) return [];
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const pathKey = context.path[1];
|
|
24
|
+
const rawPaths = (context.document
|
|
25
|
+
&& context.document.parserResult
|
|
26
|
+
&& context.document.parserResult.data
|
|
27
|
+
&& context.document.parserResult.data.paths) || {};
|
|
28
|
+
const pathItem = rawPaths[pathKey] || null;
|
|
27
29
|
|
|
28
30
|
const pathParams = (pathItem?.parameters || []).filter(
|
|
29
31
|
p => p?.in === 'path'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given - the response object (e.g. responses['201'])
|
|
3
|
+
* @param {object} options - unused
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (!given || typeof given !== 'object') {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const doc = (context.document
|
|
12
|
+
&& context.document.parserResult
|
|
13
|
+
&& context.document.parserResult.data) || {};
|
|
14
|
+
const field = doc.swagger ? 'schema' : 'content';
|
|
15
|
+
const value = given[field];
|
|
16
|
+
|
|
17
|
+
const isEmpty = value === undefined || value === null
|
|
18
|
+
|| (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
|
19
|
+
|
|
20
|
+
if (isEmpty) {
|
|
21
|
+
return [{ message: context.rule.message }];
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const BODY_ALLOWED_METHODS = new Set(['post', 'put', 'patch']);
|
|
10
10
|
|
|
11
11
|
function parseExceptions(value) {
|
|
12
12
|
return new Set(
|
|
@@ -29,39 +29,42 @@ module.exports = (given, options, context) => {
|
|
|
29
29
|
const exceptions = parseExceptions(options && options['media-type-exceptions']);
|
|
30
30
|
|
|
31
31
|
const root = context.document.parserResult.data;
|
|
32
|
+
const path = context.path;
|
|
33
|
+
const lastSegment = path[path.length - 1];
|
|
32
34
|
|
|
33
35
|
if (root.swagger) {
|
|
34
|
-
|
|
36
|
+
if (given.responses === undefined) return errors;
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const method = path[path.length - 1];
|
|
39
|
+
if (!BODY_ALLOWED_METHODS.has(method)) return errors;
|
|
40
|
+
|
|
41
|
+
const hasOwnProduces = given.produces !== undefined && given.produces !== null;
|
|
42
|
+
const produces = hasOwnProduces ? given.produces : root.produces;
|
|
43
|
+
const supported = Array.isArray(produces) && produces.some((mediaType) => isSupported(String(mediaType).toLowerCase(), defaultMediaType, exceptions));
|
|
44
|
+
if (!supported) {
|
|
41
45
|
errors.push({
|
|
42
46
|
message: context.rule.message,
|
|
43
|
-
path: [...
|
|
47
|
+
path: hasOwnProduces ? [...path, 'produces'] : [...path]
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
return errors;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
if (!responses) return errors;
|
|
52
|
-
|
|
53
|
-
for (const [statusCode, response] of Object.entries(responses)) {
|
|
54
|
-
if (NO_BODY_CODES.has(statusCode)) continue;
|
|
54
|
+
if (given.responses !== undefined) return errors;
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
if (lastSegment === 'content') {
|
|
57
|
+
const contentTypes = Object.keys(given);
|
|
57
58
|
const supported = contentTypes.some((mediaType) => isSupported(mediaType.toLowerCase(), defaultMediaType, exceptions));
|
|
58
|
-
|
|
59
59
|
if (!supported) {
|
|
60
|
-
errors.push({
|
|
61
|
-
message: context.rule.message,
|
|
62
|
-
path: [...context.path, 'responses', statusCode]
|
|
63
|
-
});
|
|
60
|
+
errors.push({ message: context.rule.message, path: [...path] });
|
|
64
61
|
}
|
|
62
|
+
return errors;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const hasContentKey = given.content !== undefined && given.content !== null;
|
|
66
|
+
if (!hasContentKey) {
|
|
67
|
+
errors.push({ message: context.rule.message, path: [...path] });
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
return errors;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {object} given - the 204 response object
|
|
3
|
+
* @param {object} options - unused
|
|
4
|
+
* @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
|
|
5
|
+
*/
|
|
6
|
+
module.exports = (given, options, context) => {
|
|
7
|
+
if (!given || typeof given !== 'object') {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const doc = context.document?.data ?? {};
|
|
12
|
+
const isOAP2 = typeof doc.swagger === 'string';
|
|
13
|
+
const field = isOAP2 ? 'schema' : 'content';
|
|
14
|
+
const value = given[field];
|
|
15
|
+
|
|
16
|
+
const isEmpty = value === undefined || value === null
|
|
17
|
+
|| (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0);
|
|
18
|
+
|
|
19
|
+
if (isEmpty) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return [{
|
|
24
|
+
message: 'OAR049: 204 No Content MUST NOT return any content.',
|
|
25
|
+
path: [...(context.path || []), field],
|
|
26
|
+
}];
|
|
27
|
+
};
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
const DEFAULT_INTEGRITY = 'minLength,maxLength,pattern,enum';
|
|
2
2
|
|
|
3
|
+
const SELF_CONSTRAINED_FORMATS = new Set([
|
|
4
|
+
'date', 'date-time', 'uuid', 'ipv4', 'ipv6',
|
|
5
|
+
]);
|
|
6
|
+
|
|
3
7
|
function isStringType(type) {
|
|
4
8
|
return type === 'string' || (Array.isArray(type) && type.indexOf('string') > -1);
|
|
5
9
|
}
|
|
6
10
|
|
|
11
|
+
function hasSelfConstrainedFormat(format) {
|
|
12
|
+
return typeof format === 'string' && SELF_CONSTRAINED_FORMATS.has(format.trim().toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
/**
|
|
8
16
|
* @param {object} targetVal a Parameter Object
|
|
9
17
|
* @param {object} options
|
|
@@ -20,6 +28,8 @@ module.exports = (targetVal, options, context) => {
|
|
|
20
28
|
|
|
21
29
|
if (!isStringType(container.type)) return [];
|
|
22
30
|
|
|
31
|
+
if (hasSelfConstrainedFormat(container.format)) return [];
|
|
32
|
+
|
|
23
33
|
const raw = options && options.parameter_integrity !== undefined && options.parameter_integrity !== null
|
|
24
34
|
? String(options.parameter_integrity)
|
|
25
35
|
: DEFAULT_INTEGRITY;
|