Repository: stoplightio/spectral (ts)

Issue:
`example-value-or-externalValue` should only target OAS3
#882 puts under the light a minor issue: `value` or `externalValue` only make sense in the context of OAS3, but the rule `example-value-or-externalValue` apply to both OAS2 and OAS3.

#882 could be merged without incurring any breaking change. However, maybe should we rename the rule to `examples-value-or-externalValue` (and update the doco accordingly) to make it clear what it actually targets.

Pull request description:
fix: example-value-or-externalValue too broad JsonPath expressions
Related to #883 (partially, see comments).

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No

**Additional context**

$..examples is too broad and incorrectly complains about examples that contain a property examples, see https://github.com/stoplightio/spectral/issues/883#issuecomment-571580864

Please some one double-check whether I found all paths for the examples in the OAS3 spec. Not sure about Swagger2?!

Edit: I'm not sure why the CI fails. Is it because some JSON path expressions return an empty array? If yes, the old JSON path would also fail on OpenAPI specs with no examples, right?

Patch:
diff --git a/src/rulesets/oas/index.json b/src/rulesets/oas/index.json
index 31df2cf1..be7aafbd 100644
--- a/src/rulesets/oas/index.json
+++ b/src/rulesets/oas/index.json
@@ -115,7 +115,15 @@
       "description": "Example should have either a `value` or `externalValue` field.",
       "recommended": true,
       "type": "style",
-      "given": "$..examples.*",
+      "given": [
+        "$.components.examples.*",
+        "$.paths..content.*.examples.*",
+        "$.components..content.*.examples.*",
+        "$.paths..parameters.*.examples.*",
+        "$.components..parameters.*.examples.*",
+        "$.paths..headers.*.examples.*",
+        "$.components..headers.*.examples.*"
+      ],
       "then": {
         "function": "xor",
         "functionOptions": {


Test patch:
diff --git a/src/rulesets/oas/__tests__/example-value-or-externalValue.ts b/src/rulesets/oas/__tests__/example-value-or-externalValue.ts
index 796ccc07..8b24abc9 100644
--- a/src/rulesets/oas/__tests__/example-value-or-externalValue.ts
+++ b/src/rulesets/oas/__tests__/example-value-or-externalValue.ts
@@ -12,41 +12,82 @@ describe('example-value-or-externalValue', () => {
   });
 
   test('validate if just externalValue', async () => {
-    const results = await s.run({ examples: { first: { externalValue: 'value' } } });
+    const results = await s.run({ components: { examples: { first: { externalValue: 'value' } } } });
     expect(results.length).toEqual(0);
   });
 
   test('validate if just value', async () => {
-    const results = await s.run({ examples: { first: { value: 'value' } } });
+    const results = await s.run({ components: { examples: { first: { value: 'value' } } } });
+    expect(results.length).toEqual(0);
+  });
+
+  test('validate if example on top level', async () => {
+    const results = await s.run({ examples: { first: { value: 'value', externalValue: 'value' } } });
+    expect(results.length).toEqual(0);
+  });
+
+  test('validate if examples properties in examples', async () => {
+    const results = await s.run({
+      components: {
+        examples: {
+          first: {
+            value: {
+              examples: {
+                a: 'b',
+              },
+            },
+          },
+          second: {
+            value: {
+              components: {
+                examples: {
+                  value: 'value',
+                  externalValue: 'value',
+                },
+              },
+            },
+          },
+          third: {
+            value: {
+              examples: {
+                a: 'b',
+              },
+            },
+          },
+        },
+      },
+    });
     expect(results.length).toEqual(0);
   });
 
   test('multiple examples - validate all value or externalValue', async () => {
     const results = await s.run({
-      examples: {
-        first: { value: 'value1' },
-        second: { externalValue: 'external-value2' },
-        third: { value: 'value3' },
+      components: {
+        examples: {
+          first: { value: 'value1' },
+          second: { externalValue: 'external-value2' },
+          third: { value: 'value3' },
+        },
       },
     });
     expect(results.length).toEqual(0);
   });
 
   test('return warnings if missing externalValue and value', async () => {
-    const results = await s.run({ examples: { first: {} } });
+    const results = await s.run({ components: { examples: { first: {} } } });
     expect(results).toEqual([
       {
         code: 'example-value-or-externalValue',
         message: 'Example should have either a `value` or `externalValue` field.',
-        path: ['examples', 'first'],
+        path: ['components', 'examples', 'first'],
         range: {
           end: {
-            character: 15,
-            line: 2,
+            character: 17,
+            line: 3,
           },
           start: {
-            character: 12,
-            line: 2,
+            character: 14,
+            line: 3,
           },
         },
         severity: DiagnosticSeverity.Warning,
@@ -56,20 +97,108 @@ describe('example-value-or-externalValue', () => {
 
   test('multiple examples - return warnings if missing externalValue and value in one', async () => {
     const results = await s.run({
-      examples: { first: { value: 'value1' }, second: { externalValue: 'external-value2' }, third: {} },
+      components: { examples: { first: { value: 'value1' }, second: { externalValue: 'external-value2' }, third: {} } },
     });
     expect(results).toEqual([
       {
         code: 'example-value-or-externalValue',
         message: 'Example should have either a `value` or `externalValue` field.',
-        path: ['examples', 'third'],
+        path: ['components', 'examples', 'third'],
         range: {
           end: {
-            character: 15,
+            character: 17,
+            line: 9,
+          },
+          start: {
+            character: 14,
+            line: 9,
+          },
+        },
+        severity: DiagnosticSeverity.Warning,
+      },
+    ]);
+  });
+
+  test('return warnings if both externalValue and value', async () => {
+    const results = await s.run({
+      components: { examples: { first: { externalValue: 'externalValue', value: 'value' } } },
+    });
+    expect(results).toEqual([
+      {
+        code: 'example-value-or-externalValue',
+        message: 'Example should have either a `value` or `externalValue` field.',
+        path: ['components', 'examples', 'first'],
+        range: {
+          end: {
+            character: 24,
+            line: 5,
+          },
+          start: {
+            character: 14,
+            line: 3,
+          },
+        },
+        severity: DiagnosticSeverity.Warning,
+      },
+    ]);
+  });
+
+  test('multiple examples - return warnings if both externalValue and value in one (in components)', async () => {
+    const results = await s.run({
+      components: {
+        examples: {
+          first: { value: 'value1' },
+          second: { externalValue: 'external-value2', value: 'value2' },
+          third: { externalValue: 'external-value3' },
+        },
+      },
+    });
+    expect(results).toEqual([
+      {
+        code: 'example-value-or-externalValue',
+        message: 'Example should have either a `value` or `externalValue` field.',
+        path: ['components', 'examples', 'second'],
+        range: {
+          end: {
+            character: 25,
             line: 8,
           },
           start: {
-            character: 12,
+            character: 15,
+            line: 6,
+          },
+        },
+        severity: DiagnosticSeverity.Warning,
+      },
+    ]);
+  });
+
+  test('multiple examples - return warnings if both externalValue and value in one (in headers)', async () => {
+    const results = await s.run({
+      components: {
+        headers: {
+          headerName: {
+            examples: {
+              first: { value: 'value1' },
+              second: { externalValue: 'external-value2', value: 'value2' },
+              third: { externalValue: 'external-value3' },
+            },
+          },
+        },
+      },
+    });
+    expect(results).toEqual([
+      {
+        code: 'example-value-or-externalValue',
+        message: 'Example should have either a `value` or `externalValue` field.',
+        path: ['components', 'headers', 'headerName', 'examples', 'second'],
+        range: {
+          end: {
+            character: 29,
+            line: 10,
+          },
+          start: {
+            character: 19,
             line: 8,
           },
         },
@@ -78,21 +207,33 @@ describe('example-value-or-externalValue', () => {
     ]);
   });
 
-  test('return warnings if both externalValue and value', async () => {
-    const results = await s.run({ examples: { first: { externalValue: 'externalValue', value: 'value' } } });
+  test('multiple examples - return warnings if both externalValue and value in one (in parameters)', async () => {
+    const results = await s.run({
+      components: {
+        parameters: {
+          parameterName: {
+            examples: {
+              first: { value: 'value1' },
+              second: { externalValue: 'external-value2', value: 'value2' },
+              third: { externalValue: 'external-value3' },
+            },
+          },
+        },
+      },
+    });
     expect(results).toEqual([
       {
         code: 'example-value-or-externalValue',
         message: 'Example should have either a `value` or `externalValue` field.',
-        path: ['examples', 'first'],
+        path: ['components', 'parameters', 'parameterName', 'examples', 'second'],
         range: {
           end: {
-            character: 22,
-            line: 4,
+            character: 29,
+            line: 10,
           },
           start: {
-            character: 12,
-            line: 2,
+            character: 19,
+            line: 8,
           },
         },
         severity: DiagnosticSeverity.Warning,
@@ -100,27 +241,41 @@ describe('example-value-or-externalValue', () => {
     ]);
   });
 
-  test('multiple examples - return warnings if both externalValue and value in one', async () => {
+  test('multiple examples - return warnings if both externalValue and value in one (in content)', async () => {
     const results = await s.run({
-      examples: {
-        first: { value: 'value1' },
-        second: { externalValue: 'external-value2', value: 'value2' },
-        third: { externalValue: 'external-value3' },
+      paths: {
+        '/path': {
+          get: {
+            responses: {
+              200: {
+                content: {
+                  'application/json': {
+                    examples: {
+                      first: { value: 'value1' },
+                      second: { externalValue: 'external-value2', value: 'value2' },
+                      third: { externalValue: 'external-value3' },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
       },
     });
     expect(results).toEqual([
       {
         code: 'example-value-or-externalValue',
         message: 'Example should have either a `value` or `externalValue` field.',
-        path: ['examples', 'second'],
+        path: ['paths', '/path', 'get', 'responses', '200', 'content', 'application/json', 'examples', 'second'],
         range: {
           end: {
-            character: 23,
-            line: 7,
+            character: 37,
+            line: 14,
           },
           start: {
-            character: 13,
-            line: 5,
+            character: 27,
+            line: 12,
           },
         },
         severity: DiagnosticSeverity.Warning,