api-quality-spectral-ruleset 1.3.0 → 1.4.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
@@ -475,6 +475,11 @@ rules:
475
475
  - $.paths.*.*.responses[?(@property !== "204")]
476
476
  then:
477
477
  function: apq-check-examples-coverage
478
+ functionOptions:
479
+ validateResponse: true
480
+ validateRequestBody: true
481
+ validateParameter: true
482
+ validateProperty: true
478
483
  apiq:OAR032:
479
484
  description: "Ambiguous path parts not encouraged."
480
485
  message: "OAR032: When reading a URL, it must be possible to interpret which resources it is referring to in a concrete way. Ambiguous names in resources must be avoided."
@@ -1,47 +1,92 @@
1
1
  const COMBINERS = ['allOf', 'oneOf', 'anyOf'];
2
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));
3
+ // A level is validated unless explicitly turned off with `false`.
4
+ const isOn = (value) => value !== false;
5
+
6
+ // Whole-body (root) example declared directly on a schema node.
7
+ const schemaHasRootExample = (schema) =>
8
+ !!schema && typeof schema === 'object'
9
+ && (schema.example !== undefined || schema.examples !== undefined);
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
+ const hasParameterExample = (param) => {
14
+ if (!param || typeof param !== 'object') return false;
15
+ if (param.example !== undefined || param.examples !== undefined) return true;
16
+ if (schemaHasRootExample(param.schema)) return true;
17
+ if (param.content && typeof param.content === 'object') {
18
+ return Object.values(param.content).some(
19
+ (mt) => mt && typeof mt === 'object'
20
+ && (mt.example !== undefined || mt.examples !== undefined || schemaHasRootExample(mt.schema)),
21
+ );
22
+ }
23
+ return false;
9
24
  };
10
25
 
11
- const hasExample = (node) => {
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
+ const hasBodyLevelExample = (node) => {
12
30
  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);
31
+ if (node.content && typeof node.content === 'object') {
32
+ return Object.values(node.content).some(
33
+ (mt) => mt && typeof mt === 'object'
34
+ && (mt.example !== undefined || mt.examples !== undefined || schemaHasRootExample(mt.schema)),
35
+ );
36
+ }
37
+ if (node.examples !== undefined) return true; // OAS2 response-level examples map
38
+ if (schemaHasRootExample(node.schema)) return true; // OAS2 root schema example
39
+ return false;
17
40
  };
18
41
 
19
- const collectPropertyIssues = (schema, issues) => {
42
+ // Per-leaf-property example check (recurses objects, arrays and combiners).
43
+ const collectPropertyIssues = (schema, issues, basePath) => {
20
44
  if (!schema || typeof schema !== 'object' || schema.$ref) return;
21
45
 
22
46
  if (schema.type === 'array') {
23
- schema.items && collectPropertyIssues(schema.items, issues);
47
+ if (schema.items) collectPropertyIssues(schema.items, issues, [...basePath, 'items']);
24
48
  return;
25
49
  }
26
50
 
27
51
  if (schema.properties) {
28
52
  for (const [propName, propSchema] of Object.entries(schema.properties)) {
29
53
  if (!propSchema || typeof propSchema !== 'object' || propSchema.$ref) continue;
54
+ const propPath = [...basePath, 'properties', propName];
30
55
  const propType = propSchema.type;
31
56
  if (propType === 'object' || propType === 'array' || (!propType && propSchema.properties)) {
32
- collectPropertyIssues(propSchema, issues);
57
+ collectPropertyIssues(propSchema, issues, propPath);
33
58
  } else if (propType !== undefined && propSchema.example === undefined && propSchema.examples === undefined) {
34
- issues.push({ message: `OAR031: Property '${propName}' is missing an example.` });
59
+ issues.push({
60
+ message: `OAR031: Property '${propName}' is missing an example.`,
61
+ path: propPath,
62
+ });
35
63
  }
36
64
  }
37
65
  }
38
66
 
39
- COMBINERS.forEach(c => {
40
- if (Array.isArray(schema[c])) schema[c].forEach(sub => collectPropertyIssues(sub, issues));
67
+ COMBINERS.forEach((c) => {
68
+ if (Array.isArray(schema[c])) {
69
+ schema[c].forEach((sub, i) => collectPropertyIssues(sub, issues, [...basePath, c, i]));
70
+ }
41
71
  });
42
72
  };
43
73
 
74
+ // Property-level coverage for a response / requestBody node (OAS3 content.* or OAS2 schema).
75
+ const collectBodyProperties = (node, issues, basePath) => {
76
+ if (node.content && typeof node.content === 'object') {
77
+ Object.entries(node.content).forEach(([mediaType, mt]) => {
78
+ if (mt?.schema) collectPropertyIssues(mt.schema, issues, [...basePath, 'content', mediaType, 'schema']);
79
+ });
80
+ } else if (node.schema) {
81
+ collectPropertyIssues(node.schema, issues, [...basePath, 'schema']);
82
+ }
83
+ };
84
+
44
85
  /**
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
+ *
45
90
  * @param {object} given
46
91
  * @param {object} options
47
92
  * @param {import('@stoplight/spectral-core').RulesetFunctionContext} context
@@ -49,23 +94,42 @@ const collectPropertyIssues = (schema, issues) => {
49
94
  module.exports = function oar031ExamplesCoverage(given, options, context) {
50
95
  if (!given || typeof given !== 'object') return [];
51
96
 
97
+ const opts = options || {};
98
+ const validateResponse = isOn(opts.validateResponse);
99
+ const validateRequestBody = isOn(opts.validateRequestBody);
100
+ const validateParameter = isOn(opts.validateParameter);
101
+ const validateProperty = isOn(opts.validateProperty);
102
+
52
103
  const issues = [];
53
104
  const nodePath = context.path || [];
54
105
  const isParameter = nodePath.length >= 2 && nodePath.at(-2) === 'parameters';
55
- const isBodyParam = isParameter && given.in === 'body';
106
+ const isRequestBody = nodePath.at(-1) === 'requestBody';
56
107
 
57
- if (!hasExample(given)) {
58
- issues.push({ message: 'OAR031: Must have one or more examples defined' });
59
- }
60
-
61
- if (!isParameter || isBodyParam) {
62
- if (given.content) {
63
- Object.values(given.content).forEach(mt => {
64
- if (mt?.schema) collectPropertyIssues(mt.schema, issues);
108
+ if (isParameter) {
109
+ if (validateParameter && !hasParameterExample(given)) {
110
+ issues.push({
111
+ message: `OAR031: Parameter '${given.name || ''}' must have an example defined`,
112
+ path: nodePath,
65
113
  });
66
- } else if (given.schema) {
67
- collectPropertyIssues(given.schema, issues);
68
114
  }
115
+ // OAS2 body parameters carry a schema, so property-level coverage applies to them too.
116
+ if (given.in === 'body' && validateProperty && given.schema) {
117
+ collectPropertyIssues(given.schema, issues, [...nodePath, 'schema']);
118
+ }
119
+ return issues;
120
+ }
121
+
122
+ const levelOn = isRequestBody ? validateRequestBody : validateResponse;
123
+ const levelMessage = isRequestBody
124
+ ? 'OAR031: Request body must have an example defined'
125
+ : 'OAR031: Response must have an example defined';
126
+
127
+ if (levelOn && !hasBodyLevelExample(given)) {
128
+ issues.push({ message: levelMessage, path: nodePath });
129
+ }
130
+
131
+ if (validateProperty) {
132
+ collectBodyProperties(given, issues, nodePath);
69
133
  }
70
134
 
71
135
  return issues;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-quality-spectral-ruleset",
3
- "version": "1.3.0",
3
+ "version": "1.4.0-beta.1",
4
4
  "description": "Spectral ruleset by API Quality",
5
5
  "main": "apq-spectral.yaml",
6
6
  "files": [