Repository: stoplightio/spectral (ts)

Issue:
Drop `schemaPath` function
`schemaPath` function relies on Spectral's internals and is used only by AsyncAPI ruleset.
 
- [ ] Remove the SchemaPath usage in AsyncAPI ruleset.
- [ ] Create a custom function if we cannot use any other spectral function to achieve the same outcome. 
- [ ] Use the custom function in the AsyncAPI ruleset.

Pull request:
chore: drop schemaPath function
Closes #1621 


**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No




Patch:
diff --git a/docs/migration-guides/6.0.md b/docs/migration-guides/6.0.md
index 7337487c..31d9887e 100644
--- a/docs/migration-guides/6.0.md
+++ b/docs/migration-guides/6.0.md
@@ -2,9 +2,13 @@
 
 ## Core
 
+### Functions
+
 1. `schema` function uses Ajv v8 in favor of Ajv v6. It is preferred to provide JSON Schema Draft 7 schemas or newer, albeit older drafts are also supported via json-schema-migrate.
    `oasVersion` is no longer supported. Use `oasSchema` function from the OAS ruleset if you want to supply OAS Schema Object.
 
+2. `schemaPath` is no longer available. If you happened to use this function, you can write a custom function that implements the same functionality.
+
 ## CLI
 
 1. deprecated `--show-unmatched-globs` flag has been removed. Alternative available: `--fail-on-unmatched-globs`
diff --git a/docs/reference/functions.md b/docs/reference/functions.md
index bfb19aae..4fc47258 100644
--- a/docs/reference/functions.md
+++ b/docs/reference/functions.md
@@ -174,35 +174,6 @@ oas3-api-servers:
         minItems: 1
 ```
 
-## schemaPath
-
-The schema-path rule is very meta. It is an extension of the schema rule, but it looks for a schema which exists inside the description document. This may never be useful for anything other than the use-case of checking OpenAPI examples are valid:
-
-<!-- title: functionOptions -->
-
-| name       | description                                                            | type      | required? |
-| ---------- | ---------------------------------------------------------------------- | --------- | --------- |
-| field      | the field to check                                                     | `string`  | yes       |
-| schemaPath | a json path pointing to the json schema to use                         | `string`  | yes       |
-| allErrors  | returns all errors when `true`; otherwise only returns the first error | `boolean` | no        |
-
-<!-- title: example -->
-
-```yaml
-valid-oas-example-in-parameters:
-  description: Examples must be valid against their defined schema.
-  message: "{{error}}"
-  recommended: true
-  severity: 0
-  type: validation
-  given: "$..parameters..[?(@.example && @.schema)]"
-  then:
-    function: schemaPath
-    functionOptions:
-      field: example
-      schemaPath: "$.schema"
-```
-
 ## truthy
 
 The value should not be `false`, `""`, `0`, `null` or `undefined`. Basically anything that would not trigger this: `if (targetVal)`.
diff --git a/src/functions/index.ts b/src/functions/index.ts
index 5395e52f..0ebe485f 100644
--- a/src/functions/index.ts
+++ b/src/functions/index.ts
@@ -6,7 +6,6 @@ import { falsy } from './falsy';
 import { length } from './length';
 import { pattern } from './pattern';
 import { schema } from './schema';
-import { schemaPath } from './schemaPath';
 import { truthy } from './truthy';
 import { undefined } from './undefined';
 import { unreferencedReusableObject } from './unreferencedReusableObject';
@@ -21,7 +20,6 @@ export const functions = {
   pattern,
   falsy,
   schema,
-  schemaPath,
   truthy,
   undefined,
   xor,
diff --git a/src/functions/schemaPath.ts b/src/functions/schemaPath.ts
deleted file mode 100644
index ce829d50..00000000
--- a/src/functions/schemaPath.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Schema Path is a variant of the normal Schema rule with a slight twist.
- * Instead of passing the rule a schema to validate, there is already a schema
- * somewhere inside the target, and we are selecting it, and applying it to
- * a given field.
- *
- * The primary use case for this was validating OpenAPI examples
- * against their schema, but this could be used for other things.
- */
-import { JSONPath } from 'jsonpath-plus';
-
-import { Optional } from '@stoplight/types';
-
-import { getLintTargets } from '../runner/utils/getLintTargets';
-import { IFunction, IFunctionResult } from '../types';
-import { schema } from './schema';
-
-export interface ISchemaPathOptions {
-  schemaPath: string;
-  // the `path.to.prop` to field, or special `@key` value to target keys for matched `given` object
-  field?: string;
-  // The oasVersion, either 2 or 3 for OpenAPI Spec versions, could also be 3.1 or a larger number if there's a need for it, otherwise JSON Schema
-  oasVersion?: Optional<2 | 3 | 3.1>;
-  allErrors?: boolean;
-}
-
-export const schemaPath: IFunction<ISchemaPathOptions> = (targetVal, opts, paths, otherValues) => {
-  // The subsection of the targetVal which contains the good bit
-  const relevantItems = getLintTargets(targetVal, opts.field);
-
-  // The subsection of the targetValue which contains the schema for us to validate the good bit against
-  const schemaObject = JSONPath({ path: opts.schemaPath, json: targetVal })[0];
-
-  const results: IFunctionResult[] = [];
-
-  for (const relevantItem of relevantItems) {
-    const result = schema(
-      relevantItem.value,
-      {
-        schema: schemaObject,
-        allErrors: opts.allErrors,
-      },
-      {
-        given: paths.given,
-        target: [...(paths.target ?? paths.given), ...relevantItem.path],
-      },
-      otherValues,
-    );
-
-    if (Array.isArray(result)) {
-      results.push(...result);
-    }
-  }
-
-  return results;
-};
diff --git a/src/meta/rule.schema.json b/src/meta/rule.schema.json
index 80249aaf..defd85aa 100644
--- a/src/meta/rule.schema.json
+++ b/src/meta/rule.schema.json
@@ -193,41 +193,6 @@
           }
         },
 
-        {
-          "if": {
-            "properties": {
-              "function": {
-                "const": "schema-path"
-              }
-            }
-          },
-          "then": {
-            "required": ["functionOptions"],
-            "properties": {
-              "functionOptions": {
-                "additionalProperties": false,
-                "properties": {
-                  "schemaPath": {
-                    "type": "string"
-                  },
-                  "field": {
-                    "type": "string"
-                  },
-                  "oasVersion": {
-                    "enum": [2, 3, 3.1],
-                    "type": "number"
-                  },
-                  "allErrors": {
-                    "type": "boolean"
-                  }
-                },
-                "required": ["schemaPath"],
-                "type": "object"
-              }
-            }
-          }
-        },
-
         {
           "if": {
             "properties": {
diff --git a/src/rulesets/asyncapi/functions/asyncApi2SchemaValidation.ts b/src/rulesets/asyncapi/functions/asyncApi2SchemaValidation.ts
new file mode 100644
index 00000000..4e8dd7f3
--- /dev/null
+++ b/src/rulesets/asyncapi/functions/asyncApi2SchemaValidation.ts
@@ -0,0 +1,60 @@
+import type { IFunctionContext, IFunction, IFunctionResult } from '../../../spectral';
+import type { JsonPath } from '@stoplight/types';
+
+function getRelevantItems(
+  target: Record<string, unknown>,
+  type: 'default' | 'examples',
+): { path: JsonPath; value: unknown }[] {
+  if (type === 'default') {
+    return [{ path: ['default'], value: target.default }];
+  }
+
+  if (!Array.isArray(target.examples)) {
+    return [];
+  }
+
+  return Array.from(target.examples.entries()).map(([key, value]) => ({
+    path: ['examples', key],
+    value,
+  }));
+}
+
+const asyncApi2SchemaValidation: IFunction<{ type: 'default' | 'examples' }> = function (
+  this: IFunctionContext,
+  targetVal,
+  opts,
+  paths,
+  otherValues,
+) {
+  if (targetVal === null || typeof targetVal !== 'object') return;
+
+  const schemaObject = targetVal;
+  const relevantItems = getRelevantItems(targetVal, opts.type);
+
+  const results: IFunctionResult[] = [];
+
+  for (const relevantItem of relevantItems) {
+    const result = this.functions.schema.call(
+      this,
+      relevantItem.value,
+      {
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+        schema: schemaObject,
+        allErrors: true,
+      },
+      {
+        given: paths.given,
+        target: [...(paths.target ?? paths.given), ...relevantItem.path],
+      },
+      otherValues,
+    );
+
+    if (Array.isArray(result)) {
+      results.push(...result);
+    }
+  }
+
+  return results;
+};
+
+export { asyncApi2SchemaValidation as default };
diff --git a/src/rulesets/asyncapi/index.json b/src/rulesets/asyncapi/index.json
index d434673c..ec6e0f99 100644
--- a/src/rulesets/asyncapi/index.json
+++ b/src/rulesets/asyncapi/index.json
@@ -1,7 +1,7 @@
 {
   "documentationUrl": "https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md",
   "formats": ["asyncapi2"],
-  "functions": ["asyncApi2PayloadValidation"],
+  "functions": ["asyncApi2PayloadValidation", "asyncApi2SchemaValidation"],
   "rules": {
     "asyncapi-channel-no-empty-parameter": {
       "description": "Channel path should not have empty parameter substitution pattern.",
@@ -169,11 +169,9 @@
         "$.channels.*.[publish,subscribe][?(@property === 'message' && @.schemaFormat === void 0)].payload.default^"
       ],
       "then": {
-        "function": "schemaPath",
+        "function": "asyncApi2SchemaValidation",
         "functionOptions": {
-          "field": "default",
-          "schemaPath": "$",
-          "allErrors": true
+          "type": "default"
         }
       }
     },
@@ -189,11 +187,9 @@
         "$.channels.*.[publish,subscribe][?(@property === 'message' && @.schemaFormat === void 0)].payload.examples^"
       ],
       "then": {
-        "function": "schemaPath",
+        "function": "asyncApi2SchemaValidation",
         "functionOptions": {
-          "field": "$.examples.*",
-          "schemaPath": "$",
-          "allErrors": true
+          "type": "examples"
         }
       }
     },
@@ -235,11 +231,9 @@
         "$.channels.*.parameters.*.schema.default^"
       ],
       "then": {
-        "function": "schemaPath",
+        "function": "asyncApi2SchemaValidation",
         "functionOptions": {
-          "field": "default",
-          "schemaPath": "$",
-          "allErrors": true
+          "type": "default"
         }
       }
     },
@@ -255,11 +249,9 @@
         "$.channels.*.parameters.*.schema.examples^"
       ],
       "then": {
-        "function": "schemaPath",
+        "function": "asyncApi2SchemaValidation",
         "functionOptions": {
-          "field": "$.examples.*",
-          "schemaPath": "$",
-          "allErrors": true
+          "type": "examples"
         }
       }
     },


Test patch:
diff --git a/src/functions/__tests__/schemaPath.test.ts b/src/functions/__tests__/schemaPath.test.ts
deleted file mode 100644
index 42776a5b..00000000
--- a/src/functions/__tests__/schemaPath.test.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-import { schemaPath } from '../schemaPath';
-
-function runSchemaPath(target: any, field: string, schemaPathStr: string) {
-  return schemaPath(target, { field, schemaPath: schemaPathStr }, { given: [], target: [] }, {
-    given: null,
-    original: target,
-  } as any);
-}
-
-describe('schema-path', () => {
-  // Check the example field matches the contents of schema
-  const fieldToCheck = 'example';
-  const path = '$.schema';
-
-  test.each([
-    ['turtle', 'string'],
-    [0, 'number'],
-    [null, 'null'],
-  ])('will pass when %s example is valid', (example, type) => {
-    const target = {
-      schema: {
-        type,
-      },
-      example,
-    };
-
-    expect(runSchemaPath(target, fieldToCheck, path)).toHaveLength(0);
-  });
-
-  test('will pass when example is valid', () => {
-    const target = {
-      schema: {
-        type: 'object',
-        properties: {
-          url: {
-            type: 'string',
-          },
-        },
-        required: ['url'],
-      },
-      example: {
-        url: 'images/38.png',
-      },
-    };
-
-    expect(runSchemaPath(target, fieldToCheck, path)).toHaveLength(0);
-  });
-
-  test('will error with totally invalid input', () => {
-    const target = {
-      schema: {
-        type: 'object',
-        properties: {
-          url: {
-            type: 'string',
-          },
-        },
-        required: ['url'],
-      },
-      example: {
-        notUrl: 'images/38.png',
-      },
-    };
-    expect(runSchemaPath(target, fieldToCheck, path)).toEqual([
-      {
-        path: ['example'],
-        message: '`example` property must have required property `url`',
-      },
-    ]);
-  });
-
-  test('will error with invalid falsy input', () => {
-    const target = {
-      schema: {
-        type: 'string',
-      },
-      example: null,
-    };
-    expect(runSchemaPath(target, fieldToCheck, path)).toEqual([
-      {
-        path: ['example'],
-        message: '`example` property type must be string',
-      },
-    ]);
-  });
-
-  test('will error with invalid array-ish input', () => {
-    const target = {
-      schema: {
-        type: 'object',
-        properties: {
-          id: {
-            type: 'string',
-          },
-        },
-      },
-      examples: {
-        'application/json': {
-          id: 1,
-          name: 'get food',
-          completed: false,
-        },
-        'application/yaml': {
-          id: 1,
-          name: 'get food',
-          completed: false,
-        },
-      },
-    };
-    expect(runSchemaPath(target, '$.examples.*', path)).toEqual([
-      {
-        message: '`id` property type must be string',
-        path: ['examples', 'application/json', 'id'],
-      },
-      {
-        message: '`id` property type must be string',
-        path: ['examples', 'application/yaml', 'id'],
-      },
-    ]);
-  });
-
-  test('will pass with valid array input', () => {
-    const target = {
-      schema: {
-        type: 'string',
-        examples: ['one', 'another'],
-      },
-    };
-    expect(runSchemaPath(target, '$.schema.examples.*', path)).toHaveLength(0);
-  });
-
-  test('will error with invalid array input', () => {
-    const target = {
-      schema: {
-        type: 'string',
-        examples: [3, 'one', 17],
-      },
-    };
-    expect(runSchemaPath(target, '$.schema.examples.*', path)).toEqual([
-      {
-        message: '`0` property type must be string',
-        path: ['schema', 'examples', '0'],
-      },
-      {
-        message: '`2` property type must be string',
-        path: ['schema', 'examples', '2'],
-      },
-    ]);
-  });
-
-  test('will error formats', () => {
-    const target = {
-      schema: {
-        type: 'string',
-        format: 'url',
-      },
-      example: 'turtle',
-    };
-
-    expect(runSchemaPath(target, fieldToCheck, path)).toEqual([
-      {
-        message: '`example` property must match format `url`',
-        path: ['example'],
-      },
-    ]);
-  });
-
-  describe('when schema path is not there', () => {
-    const invalidFieldToCheck = 'nonsense';
-
-    test('will pass when field is not there', () => {
-      const target = {
-        schema: {
-          type: 'string',
-        },
-        notNonsense: 'turtle',
-      };
-      expect(runSchemaPath(target, invalidFieldToCheck, path)).toEqual([
-        {
-          message: '#{{print("property")}}does not exist',
-          path: ['nonsense'],
-        },
-      ]);
-    });
-  });
-});
diff --git a/src/ruleset/__tests__/validation.test.ts b/src/ruleset/__tests__/validation.test.ts
index 650d41b7..d1b777bf 100644
--- a/src/ruleset/__tests__/validation.test.ts
+++ b/src/ruleset/__tests__/validation.test.ts
@@ -299,63 +299,57 @@ describe('Ruleset Validation', () => {
   });
 
   describe('then validation', () => {
-    describe.each([
-      'casing',
-      'enumeration',
-      'length',
-      'pattern',
-      'schema',
-      'schema-path',
-      'unreferencedReusableObject',
-      'xor',
-    ])('%s function', name => {
-      it('complains about empty options', () => {
-        expect(
-          assertValidRuleset.bind(null, {
-            rules: {
-              rule: {
-                given: '$',
-                then: {
-                  function: name,
-                  functionOptions: {},
+    describe.each(['casing', 'enumeration', 'length', 'pattern', 'schema', 'unreferencedReusableObject', 'xor'])(
+      '%s function',
+      name => {
+        it('complains about empty options', () => {
+          expect(
+            assertValidRuleset.bind(null, {
+              rules: {
+                rule: {
+                  given: '$',
+                  then: {
+                    function: name,
+                    functionOptions: {},
+                  },
                 },
               },
-            },
-          }),
-        ).toThrow(ValidationError);
-      });
+            }),
+          ).toThrow(ValidationError);
+        });
 
-      it('complains about missing options', () => {
-        expect(
-          assertValidRuleset.bind(null, {
-            rules: {
-              rule: {
-                given: '$',
-                then: {
-                  function: name,
+        it('complains about missing options', () => {
+          expect(
+            assertValidRuleset.bind(null, {
+              rules: {
+                rule: {
+                  given: '$',
+                  then: {
+                    function: name,
+                  },
                 },
               },
-            },
-          }),
-        ).toThrow(ValidationError);
-      });
+            }),
+          ).toThrow(ValidationError);
+        });
 
-      it('complains about nullified options', () => {
-        expect(
-          assertValidRuleset.bind(null, {
-            rules: {
-              rule: {
-                given: '$',
-                then: {
-                  function: name,
-                  functionOptions: null,
+        it('complains about nullified options', () => {
+          expect(
+            assertValidRuleset.bind(null, {
+              rules: {
+                rule: {
+                  given: '$',
+                  then: {
+                    function: name,
+                    functionOptions: null,
+                  },
                 },
               },
-            },
-          }),
-        ).toThrow(ValidationError);
-      });
-    });
+            }),
+          ).toThrow(ValidationError);
+        });
+      },
+    );
 
     describe.each(['truthy', 'falsy', 'undefined'])('%s function', name => {
       it('given valid then, does not complain', () => {
diff --git a/src/rulesets/asyncapi/functions/__tests__/asyncApi2SchemaValidation.test.ts b/src/rulesets/asyncapi/functions/__tests__/asyncApi2SchemaValidation.test.ts
new file mode 100644
index 00000000..d66ba29b
--- /dev/null
+++ b/src/rulesets/asyncapi/functions/__tests__/asyncApi2SchemaValidation.test.ts
@@ -0,0 +1,50 @@
+import { functions } from '../../../../functions';
+import asyncApi2SchemaValidation from '../asyncApi2SchemaValidation';
+
+function runPayloadValidation(targetVal: any, opts: { type: 'examples' | 'default' }) {
+  return asyncApi2SchemaValidation.call(
+    { functions },
+    targetVal,
+    opts,
+    { given: [] },
+    { given: null, original: null, documentInventory: {} as any, rule: {} as any },
+  );
+}
+
+describe('asyncApi2SchemaValidation', () => {
+  test('validates examples', () => {
+    const payload = {
+      type: 'string',
+      examples: [17, 'one', 13],
+    };
+
+    const results = runPayloadValidation(payload, { type: 'examples' });
+
+    expect(results).toEqual([
+      {
+        message: '`0` property type must be string',
+        path: ['examples', 0],
+      },
+      {
+        message: '`2` property type must be string',
+        path: ['examples', 2],
+      },
+    ]);
+  });
+
+  test('validates default', () => {
+    const payload = {
+      type: 'string',
+      default: 18,
+    };
+
+    const results = runPayloadValidation(payload, { type: 'default' });
+
+    expect(results).toEqual([
+      {
+        message: '`default` property type must be string',
+        path: ['default'],
+      },
+    ]);
+  });
+});