api-quality-spectral-ruleset 1.3.0-beta.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/apq-spectral.yaml CHANGED
@@ -213,7 +213,7 @@ rules:
213
213
  enum: [float, double, decimal]
214
214
  apiq:OAR017:
215
215
  description: "Resource path should alternate static and parametrized parts."
216
- message: "OAR017: Resource path should alternate static and parametrized parts."
216
+ message: "OAR017: Resource path should alternate static and parametrized parts"
217
217
  documentationUrl: "https://github.com/apiaddicts/apquality-spectral/blob/main/docs/resources/OAR017.md"
218
218
  severity: error
219
219
  given: "$.paths.*~"
@@ -829,7 +829,7 @@ rules:
829
829
  severity: "warn"
830
830
  given:
831
831
  - "$.paths.*.*[responses,requestBody]..content..schema..properties.*~"
832
- - "$.paths.*.*.parameters[*].schema..properties.*~"
832
+ - "$.paths.*.*.parameters[?(@.in=='body')].schema..properties.*~"
833
833
  - "$.paths.*.*.responses[*].schema..properties.*~"
834
834
  then:
835
835
  function: pattern
@@ -1,90 +1,70 @@
1
+ const COMBINERS = ['allOf', 'oneOf', 'anyOf'];
2
+
3
+ const checkSchema = (schema) => {
4
+ if (!schema || typeof schema !== 'object') return false;
5
+ if (schema.example !== undefined || schema.examples !== undefined) return true;
6
+ if (schema.properties) return Object.values(schema.properties).some(checkSchema);
7
+ if (schema.items) return checkSchema(schema.items);
8
+ return COMBINERS.some(c => Array.isArray(schema[c]) && schema[c].some(checkSchema));
9
+ };
10
+
11
+ const hasExample = (node) => {
12
+ if (!node || typeof node !== 'object') return false;
13
+ if (node.example !== undefined || node.examples !== undefined) return true;
14
+ if (node.content) return Object.values(node.content).some(hasExample);
15
+ if (node.schema) return checkSchema(node.schema);
16
+ return !!(node.properties || node.items || node.type) && checkSchema(node);
17
+ };
18
+
19
+ const collectPropertyIssues = (schema, issues) => {
20
+ if (!schema || typeof schema !== 'object' || schema.$ref) return;
21
+
22
+ if (schema.type === 'array') {
23
+ schema.items && collectPropertyIssues(schema.items, issues);
24
+ return;
25
+ }
26
+
27
+ if (schema.properties) {
28
+ for (const [propName, propSchema] of Object.entries(schema.properties)) {
29
+ if (!propSchema || typeof propSchema !== 'object' || propSchema.$ref) continue;
30
+ const propType = propSchema.type;
31
+ if (propType === 'object' || propType === 'array' || (!propType && propSchema.properties)) {
32
+ collectPropertyIssues(propSchema, issues);
33
+ } else if (propType !== undefined && propSchema.example === undefined && propSchema.examples === undefined) {
34
+ issues.push({ message: `OAR031: Property '${propName}' is missing an example.` });
35
+ }
36
+ }
37
+ }
38
+
39
+ COMBINERS.forEach(c => {
40
+ if (Array.isArray(schema[c])) schema[c].forEach(sub => collectPropertyIssues(sub, issues));
41
+ });
42
+ };
43
+
1
44
  /**
2
45
  * @param {object} given
3
46
  * @param {object} options
4
47
  * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
48
  */
6
- module.exports = (given, options, context) => {
49
+ module.exports = function oar031ExamplesCoverage(given, options, context) {
7
50
  if (!given || typeof given !== 'object') return [];
8
51
 
9
52
  const issues = [];
10
53
  const nodePath = context.path || [];
11
-
12
- const isParameter = nodePath.length >= 2 && nodePath[nodePath.length - 2] === 'parameters';
13
-
14
- const checkSchema = (schema) => {
15
- if (!schema || typeof schema !== 'object') return false;
16
- if (schema.example !== undefined || schema.examples !== undefined) return true;
17
- if (schema.properties && typeof schema.properties === 'object') {
18
- return Object.values(schema.properties).some(prop => checkSchema(prop));
19
- }
20
- if (schema.items) return checkSchema(schema.items);
21
- for (const combiner of ['allOf', 'oneOf', 'anyOf']) {
22
- if (schema[combiner] && Array.isArray(schema[combiner])) {
23
- if (schema[combiner].some(sub => checkSchema(sub))) return true;
24
- }
25
- }
26
- return false;
27
- };
28
-
29
- const hasExample = (node) => {
30
- if (!node || typeof node !== 'object') return false;
31
- if (node.example !== undefined || node.examples !== undefined) return true;
32
- if (node.content && typeof node.content === 'object') {
33
- return Object.values(node.content).some(mediaType => hasExample(mediaType));
34
- }
35
- if (node.schema) return checkSchema(node.schema);
36
- if (node.properties || node.items || node.type) return checkSchema(node);
37
- return false;
38
- };
54
+ const isParameter = nodePath.length >= 2 && nodePath.at(-2) === 'parameters';
55
+ const isBodyParam = isParameter && given.in === 'body';
39
56
 
40
57
  if (!hasExample(given)) {
41
58
  issues.push({ message: 'OAR031: Must have one or more examples defined' });
42
59
  }
43
60
 
44
- if (!isParameter) {
45
- const collectPropertyIssues = (schema, path) => {
46
- if (!schema || typeof schema !== 'object' || schema.$ref) return;
47
-
48
- if (schema.type === 'array' && schema.items) {
49
- collectPropertyIssues(schema.items, [...path, 'items']);
50
- return;
51
- }
52
-
53
- if (schema.properties && typeof schema.properties === 'object') {
54
- for (const [propName, propSchema] of Object.entries(schema.properties)) {
55
- if (!propSchema || typeof propSchema !== 'object' || propSchema.$ref) continue;
56
-
57
- const propType = propSchema.type;
58
-
59
- if (propType === 'object' || (!propType && propSchema.properties)) {
60
- collectPropertyIssues(propSchema, [...path, 'properties', propName]);
61
- } else if (propType === 'array') {
62
- collectPropertyIssues(propSchema, [...path, 'properties', propName]);
63
- } else {
64
- if (propSchema.example === undefined && propSchema.examples === undefined) {
65
- issues.push({ message: `OAR031: Property '${propName}' is missing an example.` });
66
- }
67
- }
68
- }
69
- }
70
-
71
- for (const combiner of ['allOf', 'oneOf', 'anyOf']) {
72
- if (schema[combiner] && Array.isArray(schema[combiner])) {
73
- schema[combiner].forEach((sub, i) => {
74
- collectPropertyIssues(sub, [...path, combiner, i]);
75
- });
76
- }
77
- }
78
- };
79
-
80
- if (given.content && typeof given.content === 'object') {
81
- for (const [mediaType, mediaTypeNode] of Object.entries(given.content)) {
82
- if (mediaTypeNode && mediaTypeNode.schema) {
83
- collectPropertyIssues(mediaTypeNode.schema, ['content', mediaType, 'schema']);
84
- }
85
- }
61
+ if (!isParameter || isBodyParam) {
62
+ if (given.content) {
63
+ Object.values(given.content).forEach(mt => {
64
+ if (mt?.schema) collectPropertyIssues(mt.schema, issues);
65
+ });
86
66
  } else if (given.schema) {
87
- collectPropertyIssues(given.schema, ['schema']);
67
+ collectPropertyIssues(given.schema, issues);
88
68
  }
89
69
  }
90
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.3.0-beta.5",
3
+ "version": "1.3.0",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [