api-quality-spectral-ruleset 1.2.0 → 1.3.0-beta.1

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
@@ -23,6 +23,7 @@ functions:
23
23
  - apq-binary-format-check
24
24
  - apq-paged-response-check
25
25
  - apq-status-endpoint-check
26
+ - apq-validate-structure
26
27
  extends:
27
28
  - spectral:asyncapi
28
29
  - spectral:oas
@@ -436,8 +437,8 @@ rules:
436
437
  status-endpoint: "/status"
437
438
  method: "get"
438
439
  apiq:OAR031:
439
- description: The examples can help developers to understand the response data structure and representation."
440
- message: "OAR031: Examples - Parameters, Request Body and Responses must have an examples defined."
440
+ description: "The examples can help developers to understand the response data structure and representation."
441
+ message: "{{error}}"
441
442
  severity: error
442
443
  given:
443
444
  - $.paths.*.*.parameters.*
@@ -576,6 +577,17 @@ rules:
576
577
  function: pattern
577
578
  functionOptions:
578
579
  match: "^(/api-[^/]+/v[0-9]+|https?:\\/\\/[^/]+\\/api-[^/]+\\/v[0-9]+)$"
580
+ apiq:OAR043:
581
+ description: "OpenAPI definition contains structural errors that would be detected by a strict parser or validator."
582
+ message: "{{error}}"
583
+ severity: error
584
+ given:
585
+ - $.paths.*.*.parameters.*.in
586
+ - $.components.parameters.*.in
587
+ - $.paths.*.*.parameters.*.schema.type
588
+ - $.components.parameters.*.schema.type
589
+ then:
590
+ function: apq-validate-structure
579
591
  apiq:OAR044:
580
592
  description: "Declared media type should conform to RFC6838 and RFC7231."
581
593
  message: "OAR044: Declared media type range should conform to RFC7231."
@@ -6,61 +6,87 @@
6
6
  module.exports = (given, options, context) => {
7
7
  if (!given || typeof given !== 'object') return [];
8
8
 
9
- const hasExample = (node) => {
10
- if (!node || typeof node !== 'object') return false;
11
-
12
- if (node.example !== undefined || node.examples !== undefined) return true;
13
-
14
- if (node.content && typeof node.content === 'object') {
15
- return Object.values(node.content).some(mediaType => hasExample(mediaType));
16
- }
9
+ const issues = [];
10
+ const nodePath = context.path || [];
17
11
 
18
- if (node.schema) {
19
- return checkSchema(node.schema);
20
- }
21
-
22
- if (node.properties || node.items || node.type) {
23
- return checkSchema(node);
24
- }
25
-
26
- return false;
27
- };
12
+ const isParameter = nodePath.length >= 2 && nodePath[nodePath.length - 2] === 'parameters';
28
13
 
29
14
  const checkSchema = (schema) => {
30
15
  if (!schema || typeof schema !== 'object') return false;
31
-
32
16
  if (schema.example !== undefined || schema.examples !== undefined) return true;
33
-
34
17
  if (schema.properties && typeof schema.properties === 'object') {
35
18
  return Object.values(schema.properties).some(prop => checkSchema(prop));
36
19
  }
37
-
38
- if (schema.items) {
39
- return checkSchema(schema.items);
40
- }
41
-
42
- if (schema.allOf && Array.isArray(schema.allOf)) {
43
- return schema.allOf.some(subSchema => checkSchema(subSchema));
44
- }
45
-
46
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
47
- return schema.oneOf.some(subSchema => checkSchema(subSchema));
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
+ }
48
25
  }
26
+ return false;
27
+ };
49
28
 
50
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
51
- return schema.anyOf.some(subSchema => checkSchema(subSchema));
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));
52
34
  }
53
-
35
+ if (node.schema) return checkSchema(node.schema);
36
+ if (node.properties || node.items || node.type) return checkSchema(node);
54
37
  return false;
55
38
  };
56
39
 
57
40
  if (!hasExample(given)) {
58
- return [
59
- {
60
- message: context.rule.message
61
- },
62
- ];
41
+ issues.push({ message: context.rule.message });
42
+ }
43
+
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
+ }
86
+ } else if (given.schema) {
87
+ collectPropertyIssues(given.schema, ['schema']);
88
+ }
63
89
  }
64
90
 
65
- return [];
66
- };
91
+ return issues;
92
+ };
@@ -10,6 +10,7 @@ module.exports = (targetVal, options, context) => {
10
10
  const segments = targetVal
11
11
  .split('/')
12
12
  .filter(Boolean)
13
+ .filter(segment => !segment.startsWith('{') && !segment.endsWith('}'))
13
14
  .filter(segment => !ignore.includes(segment))
14
15
 
15
16
  if (segments.length > maxDepth) {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @param {string} given - The field value
3
+ * @param {object} options
4
+ * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
5
+ */
6
+ module.exports = (given, options, context) => {
7
+ if (given === null || given === undefined) return [];
8
+
9
+ const VALID_IN_VALUES = ['query', 'header', 'path', 'cookie', 'body', 'formData'];
10
+ const VALID_SCHEMA_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object', 'null'];
11
+
12
+ const nodePath = context.path || [];
13
+ const fieldName = nodePath[nodePath.length - 1];
14
+
15
+ if (fieldName === 'in') {
16
+ if (!VALID_IN_VALUES.includes(String(given))) {
17
+ return [{
18
+ message: `in: Expected one of [${VALID_IN_VALUES.map(v => `"${v}"`).join(', ')}]`,
19
+ }];
20
+ }
21
+ }
22
+
23
+ if (fieldName === 'type') {
24
+ if (!VALID_SCHEMA_TYPES.includes(String(given))) {
25
+ return [{
26
+ message: `type: Expected one of [${VALID_SCHEMA_TYPES.map(v => `"${v}"`).join(', ')}]`,
27
+ }];
28
+ }
29
+ }
30
+
31
+ return [];
32
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-beta.1",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [
@@ -11,7 +11,11 @@
11
11
  "test": "jest --coverage --detectOpenHandles --no-cache --forceExit",
12
12
  "make-badges": "coverage-badges"
13
13
  },
14
- "keywords": ["spectral", "linter", "openapi"],
14
+ "keywords": [
15
+ "spectral",
16
+ "linter",
17
+ "openapi"
18
+ ],
15
19
  "author": {
16
20
  "name": "APIQuality"
17
21
  },