diff --git a/rulesets/src/.spectral.yml b/rulesets/src/.spectral.yml
index 2fae059..b861c36 100644
--- a/rulesets/src/.spectral.yml
+++ b/rulesets/src/.spectral.yml
@@ -8,6 +8,7 @@ extends:
- serialization.ruleset.yml
- url-structure.ruleset.yml
- webhooks.ruleset.yml
+ - collections.ruleset.yml
rules:
# not keeping this, just off for testing in this repo
diff --git a/rulesets/src/collections.ruleset.yml b/rulesets/src/collections.ruleset.yml
new file mode 100644
index 0000000..e9ad3ef
--- /dev/null
+++ b/rulesets/src/collections.ruleset.yml
@@ -0,0 +1,168 @@
+rules:
+ ##### General #####
+ sps-no-collection-paging-capability:
+ description: "Response bodies from collection endpoints SHOULD offer paging capability."
+ severity: warn
+ given: $.paths[?(!@property.match(/.*\/\{[^}]+\}$/))].get.responses['200'].content.application/json.schema.properties
+ then:
+ - field: "paging"
+ function: truthy
+ - field: "paging.type"
+ function: pattern
+ functionOptions:
+ match: "object"
+
+ ##### Root Element #####
+ sps-collection-missing-results-array:
+ description: "Response bodies must have a root element called results and is an array of objects."
+ severity: error
+ given: $.paths[?(!@property.match(/.*\/\{[^}]+\}$/))].get.responses['200'].content.application/json.schema.properties.results
+ then:
+ - field: type
+ function: pattern
+ functionOptions:
+ match: "array"
+ - field: items.type
+ function: pattern
+ functionOptions:
+ match: "object"
+
+ ##### Pagination #####
+ sps-missing-pagination-query-parameters:
+ description: "Collection GET endpoints SHOULD support pagination using query parameters. Offset or cursor based pagination is required."
+ severity: warn
+ given: $.paths[?(!@property.match(/.*\/\{[^}]+\}\/*.*/))].get
+ then:
+ - field: parameters
+ function: schema
+ functionOptions:
+ schema:
+ type: array
+ items:
+ type: object
+ contains:
+ type: object
+ properties:
+ name:
+ const: limit
+ in:
+ const: query
+ allOf:
+ - anyOf:
+ - contains:
+ type: object
+ properties:
+ name:
+ const: offset
+ in:
+ const: query
+ - contains:
+ type: object
+ properties:
+ name:
+ const: cursor
+ in:
+ const: query
+
+ sps-post-request-body-missing-paging-object:
+ description: "POST collection endpoints MUST have a request body schema that includes paging parameters."
+ severity: error
+ given: $.paths[?(!@property.match(/.*\/\{[^}]+\}$/))].post.requestBody.content.application/json.schema.properties.paging
+ then:
+ field: "type"
+ function: pattern
+ functionOptions:
+ match: "object"
+
+ ##### FILTERING #####
+ sps-disallow-resource-identifier-filtering:
+ description: "Resource identifier filtering is not allowed as a query parameter. Use the resource identifier in the URL path."
+ severity: warn
+ given: $.paths..get.parameters.[?(@.in=='query' && @.name=='id')]
+ then:
+ field: "name"
+ function: pattern
+ functionOptions:
+ notMatch: "^id$"
+
+ sps-filtering-only-get-requests:
+ description: "Only GET-based endpoints SHOULD have have the query parameter 'filter'."
+ severity: error
+ given: $.paths.*[?(@property!='get')].parameters.[?(@.in=='query' && @.name=='filter')].name
+ then:
+ function: falsy
+
+# Commented out because
+# https://github.com/SPSCommerce/sps-api-standards/pull/86#discussion_r1680361945
+ # sps-unreasonable-query-parameters-limit:
+ # description: "Filtering query parameters SHOULD have a reasonable limit, no more than 12."
+ # severity: warn
+ # given: $.paths..get
+ # then:
+ # function: schema
+ # functionOptions:
+ # schema:
+ # type: object
+ # properties:
+ # parameters:
+ # type: array
+ # minContains: 0
+ # maxContains: 12
+ # contains:
+ # type: object
+ # properties:
+ # in:
+ # const: query
+
+ sps-hybird-filtering-exists-with-root-filter:
+ description: "Hybrid filtering MAY be offered on multiple attributes, but MUST never exist if a root \"filter\" query parameter is present."
+ severity: error
+ given: $.paths..get.parameters^
+ then:
+ function: schema
+ functionOptions:
+ schema:
+ type: object
+ properties:
+ parameters:
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ in:
+ type: string
+ allOf:
+ - if:
+ properties:
+ parameters:
+ type: array
+ contains:
+ type: object
+ properties:
+ name:
+ const: filter
+ then:
+ not:
+ properties:
+ parameters:
+ type: array
+ contains:
+ type: object
+ properties:
+ name:
+ type: string
+ pattern: \w+Filter
+
+ ##### SORTING #####
+ sps-sorting-parameters-only-get-requests:
+ description: "Non-GET endpoints MUST NOT have sorting query parameters. Parameter names such as sort, sorting, orderBy, etc."
+ severity: error
+ given: $.paths.*[?(@property!='get')].parameters.[?(@.in=='query')]
+ then:
+ field: "name"
+ function: pattern
+ functionOptions:
+ notMatch: "^sort|sorting|sortBy|order|ordering|orderBy$"
+
diff --git a/rulesets/test/collections/sps-collection-missing-results-array.test.js b/rulesets/test/collections/sps-collection-missing-results-array.test.js
new file mode 100644
index 0000000..242b86a
--- /dev/null
+++ b/rulesets/test/collections/sps-collection-missing-results-array.test.js
@@ -0,0 +1,93 @@
+const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-collection-missing-results-array", () => {
+ let spectral = null;
+ const ruleName = "sps-collection-missing-results-array";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - collection response with results array of objects", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ results:
+ type: array
+ items:
+ type: object
+ paging:
+ type: object
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - collection response - results is not an array", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ results:
+ type: object
+ paging:
+ type: object
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Error", 1);
+ });
+
+ test("invalid - collection response - results is not an array of objects", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ results:
+ type: array
+ items:
+ type: string
+ paging:
+ type: object
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Error", 1);
+ });
+});
diff --git a/rulesets/test/collections/sps-disallow-resource-identifier-filtering.test.js b/rulesets/test/collections/sps-disallow-resource-identifier-filtering.test.js
new file mode 100644
index 0000000..d72d390
--- /dev/null
+++ b/rulesets/test/collections/sps-disallow-resource-identifier-filtering.test.js
@@ -0,0 +1,52 @@
+ const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-disallow-resource-identifier-filtering", () => {
+ let spectral = null;
+ const ruleName = "sps-disallow-resource-identifier-filtering";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - resource identifier is within the path of the endpoint", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users/{id}:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: id
+ in: path
+ required: true
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - resource identifier is defined as a query parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: id
+ in: query
+ required: false
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Warning", 1);
+ });
+});
diff --git a/rulesets/test/collections/sps-filtering-only-get-requests.test.js b/rulesets/test/collections/sps-filtering-only-get-requests.test.js
new file mode 100644
index 0000000..48cef1e
--- /dev/null
+++ b/rulesets/test/collections/sps-filtering-only-get-requests.test.js
@@ -0,0 +1,65 @@
+ const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-filtering-only-get-requests", () => {
+ let spectral = null;
+ const ruleName = "sps-filtering-only-get-requests";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - GET endpoint has a filter query parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ /employees:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - non-GET endpoints has filter query parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ post:
+ summary: Create a user
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: Create a user
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Error", 1);
+ });
+});
diff --git a/rulesets/test/collections/sps-hybird-filtering-exists-with-root-filter.test.js b/rulesets/test/collections/sps-hybird-filtering-exists-with-root-filter.test.js
new file mode 100644
index 0000000..e6ab923
--- /dev/null
+++ b/rulesets/test/collections/sps-hybird-filtering-exists-with-root-filter.test.js
@@ -0,0 +1,156 @@
+ const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-hybird-filtering-exists-with-root-filter", () => {
+ let spectral = null;
+ const ruleName = "sps-hybird-filtering-exists-with-root-filter";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - endpoint only has root filter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ - name: active
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - endpoint has no filter query parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: active
+ in: query
+ required: false
+ - name: foo
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - endpoint has no query parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /ping:
+ get:
+ responses:
+ '200':
+ description: health check
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - endpoint has hybrid filtering", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: active
+ in: query
+ required: false
+ - name: userFilter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - endpoint has hybrid filtering and a root filter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: filter
+ in: query
+ required: false
+ - name: active
+ in: query
+ required: false
+ - name: userFilter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Error", 1);
+ });
+
+ test("valid - endpoint has 2 hybrid filters", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: activeFilter
+ in: query
+ required: false
+ - name: userFilter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+});
diff --git a/rulesets/test/collections/sps-missing-pagination-query-parameters.test.js b/rulesets/test/collections/sps-missing-pagination-query-parameters.test.js
new file mode 100644
index 0000000..70c8b78
--- /dev/null
+++ b/rulesets/test/collections/sps-missing-pagination-query-parameters.test.js
@@ -0,0 +1,160 @@
+const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-missing-pagination-query-parameters", () => {
+ let spectral = null;
+ const ruleName = "sps-missing-pagination-query-parameters";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - query parameter is not at the end of the path", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/items/{itemId}/views:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: userId
+ in: path
+ required: true
+ - name: limit
+ in: query
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - GET endpoint is missing pagination query parameters", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: officeLocation
+ in: query
+ `;
+ await spectral.validateFailure(spec, ruleName, "Warning", 2);
+ });
+
+ test("valid - rule does not flag query parameters on GET endpoints that searches on a certain id", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users/{userId}:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: userId
+ in: path
+ required: true
+ - name: limit
+ in: query
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ describe("offset based pagination", () => {
+ test("valid - GET endpoint has offset based pagination query parameters", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: offset
+ in: query
+ - name: limit
+ in: query
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - GET endpoint has offset based pagination and other query parameters", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: offset
+ in: query
+ - name: limit
+ in: query
+ - name: officeLocation
+ in: query
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - GET endpoint has offset pagination parameters but not limit", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: offset
+ in: query
+ `;
+ await spectral.validateFailure(spec, ruleName, "Warning", 1);
+ });
+ });
+
+ describe("cursor based pagination", () => {
+ test("valid - GET endpoint has cursor based pagination query parameters", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: cursor
+ in: query
+ - name: limit
+ in: query
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - GET endpoint has cursor pagination and other query parameters", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: cursor
+ in: query
+ - name: limit
+ in: query
+ - name: officeLocation
+ in: query
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - GET endpoint has cursor pagination parameters but not limit", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ get:
+ summary: Get User by ID
+ parameters:
+ - name: cursor
+ in: query
+ `;
+ await spectral.validateFailure(spec, ruleName, "Warning", 1);
+ });
+ });
+
+});
diff --git a/rulesets/test/collections/sps-no-collection-paging-capability.test.js b/rulesets/test/collections/sps-no-collection-paging-capability.test.js
new file mode 100644
index 0000000..a8d10ef
--- /dev/null
+++ b/rulesets/test/collections/sps-no-collection-paging-capability.test.js
@@ -0,0 +1,125 @@
+const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-no-collection-paging-capability", () => {
+ let spectral = null;
+ const ruleName = "sps-no-collection-paging-capability";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - paging within response body", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users/{id}:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ lastName:
+ type: string
+ firstName:
+ type: string
+ age:
+ type: integer
+ /users:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ results:
+ type: array
+ items:
+ type: object
+ paging:
+ type: object
+ properties:
+ cursor:
+ type: string
+ offset:
+ type: integer
+ limit:
+ type: integer
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - response body - missing paging object", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ results:
+ type: array
+ items:
+ type: object
+ collection:
+ type: object
+ properties:
+ cursor:
+ type: string
+ offset:
+ type: integer
+ limit:
+ type: integer
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Warning", 1);
+ });
+
+ test("invalid - response body - paging element must be an object", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ responses:
+ '200':
+ description: A list of users
+ content:
+ application/json:
+ schema:
+ properties:
+ results:
+ type: array
+ items:
+ type: object
+ paging:
+ type: string
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Warning", 1);
+ });
+});
diff --git a/rulesets/test/collections/sps-post-request-body-missing-paging-object.test.js b/rulesets/test/collections/sps-post-request-body-missing-paging-object.test.js
new file mode 100644
index 0000000..581d358
--- /dev/null
+++ b/rulesets/test/collections/sps-post-request-body-missing-paging-object.test.js
@@ -0,0 +1,84 @@
+const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-post-request-body-missing-paging-object", () => {
+ let spectral = null;
+ const ruleName = "sps-post-request-body-missing-paging-object";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - POST endpoint with paging object within request body", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ post:
+ summary: Get User by ID
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ paging:
+ type: object
+ properties:
+ limit:
+ type: integer
+ offset:
+ type: integer
+ cursor:
+ type: string
+ user:
+ type: object
+ properties:
+ firstName:
+ type: string
+ lastName:
+ type: string
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - POST endpoint does not have paging object with request body", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ post:
+ summary: Get User by ID
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ firstName:
+ type: string
+ lastName:
+ type: string
+ `;
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - POST endpoint has incorrect paging type - string", async () => {
+ const spec = `
+ openapi: 3.1.0
+ paths:
+ /v1/users:
+ post:
+ summary: Get User by ID
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ paging:
+ type: string
+ `;
+ await spectral.validateFailure(spec, ruleName, "Error", 1);
+ });
+});
diff --git a/rulesets/test/collections/sps-sorting-parameters-only-get-requests.test.js b/rulesets/test/collections/sps-sorting-parameters-only-get-requests.test.js
new file mode 100644
index 0000000..1902439
--- /dev/null
+++ b/rulesets/test/collections/sps-sorting-parameters-only-get-requests.test.js
@@ -0,0 +1,80 @@
+ const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe("sps-sorting-parameters-only-get-requests", () => {
+ let spectral = null;
+ const ruleName = "sps-sorting-parameters-only-get-requests";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - no errors should happen when sorting query parameter only on GET endpoints", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: sortBy
+ in: query
+ required: false
+ - name: limit
+ in: query
+ required: false
+ - name: offset
+ in: query
+ required: false
+ - name: cursor
+ in: query
+ required: false
+ - name: filter
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ /employees:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: orderBy
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - non-GET endpoints should not have sorting parameters as query parameters", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users/{id}:
+ patch:
+ summary: Get a list of users
+ parameters:
+ - name: id
+ in: path
+ required: true
+ - name: sorting
+ in: query
+ required: false
+ responses:
+ '200':
+ description: A list of users
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Error", 1);
+ });
+});
diff --git a/rulesets/test/collections/sps-unreasonable-query-parameters-limit.test.js b/rulesets/test/collections/sps-unreasonable-query-parameters-limit.test.js
new file mode 100644
index 0000000..5ea04d7
--- /dev/null
+++ b/rulesets/test/collections/sps-unreasonable-query-parameters-limit.test.js
@@ -0,0 +1,235 @@
+ const { SpectralTestHarness } = require("../harness/spectral-test-harness.js");
+
+describe.skip("sps-unreasonable-query-parameters-limit", () => {
+ let spectral = null;
+ const ruleName = "sps-unreasonable-query-parameters-limit";
+ const ruleset = "src/collections.ruleset.yml";
+
+ beforeEach(async () => {
+ spectral = new SpectralTestHarness(ruleset);
+ });
+
+ test("valid - endpoint has 1 path parameter and zero query parameters", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users/{id}:
+ get:
+ summary: Get a specific user
+ parameters:
+ - name: id
+ in: path
+ required: true
+ responses:
+ '200':
+ description: A single user
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - endpoint has 13 query parameters and 1 path parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users/{id}:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: id
+ in: path
+ - name: query_param_1
+ in: query
+ - name: query_param_2
+ in: query
+ - name: query_param_3
+ in: query
+ - name: query_param_4
+ in: query
+ - name: query_param_5
+ in: query
+ - name: query_param_6
+ in: query
+ - name: query_param_7
+ in: query
+ - name: query_param_8
+ in: query
+ - name: query_param_9
+ in: query
+ - name: query_param_10
+ in: query
+ - name: query_param_11
+ in: query
+ - name: query_param_12
+ in: query
+ - name: query_param_13
+ in: query
+ `;
+
+ await spectral.validateFailure(spec, ruleName, "Warning");
+ });
+
+ test("valid - endpoint has 12 query parameters", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: query_param_1
+ in: query
+ - name: query_param_2
+ in: query
+ - name: query_param_3
+ in: query
+ - name: query_param_4
+ in: query
+ - name: query_param_5
+ in: query
+ - name: query_param_6
+ in: query
+ - name: query_param_7
+ in: query
+ - name: query_param_8
+ in: query
+ - name: query_param_9
+ in: query
+ - name: query_param_10
+ in: query
+ - name: query_param_11
+ in: query
+ - name: query_param_12
+ in: query
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("invalid - endpoint has 13 query parameters", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: query_param_1
+ in: query
+ - name: query_param_2
+ in: query
+ - name: query_param_3
+ in: query
+ - name: query_param_4
+ in: query
+ - name: query_param_5
+ in: query
+ - name: query_param_6
+ in: query
+ - name: query_param_7
+ in: query
+ - name: query_param_8
+ in: query
+ - name: query_param_9
+ in: query
+ - name: query_param_10
+ in: query
+ - name: query_param_11
+ in: query
+ - name: query_param_12
+ in: query
+ - name: query_param_13
+ in: query
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ // I understand this probably breaks some other rule within our standards but its testing the json schema
+ test("valid - endpoint has 13 path parameters and 1 query parameter", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /users/{path_param_1}/{path_param_2}/{path_param_3}/{path_param_4}/{path_param_5}/{path_param_6}/{path_param_7}/{path_param_8}/{path_param_9}/{path_param_10}/{path_param_11}/{path_param_12}/{path_param_13}:
+ get:
+ summary: Get a list of users
+ parameters:
+ - name: query_param_1
+ in: query
+ - name: path_param_1
+ in: path
+ required: true
+ - name: path_param_2
+ in: path
+ required: true
+ - name: path_param_3
+ in: path
+ required: true
+ - name: path_param_4
+ in: path
+ required: true
+ - name: path_param_5
+ in: path
+ required: true
+ - name: path_param_6
+ in: path
+ required: true
+ - name: path_param_7
+ in: path
+ required: true
+ - name: path_param_8
+ in: path
+ required: true
+ - name: path_param_9
+ in: path
+ required: true
+ - name: path_param_10
+ in: path
+ required: true
+ - name: path_param_11
+ in: path
+ required: true
+ - name: path_param_12
+ in: path
+ required: true
+ - name: path_param_13
+ in: path
+ required: true
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+
+ test("valid - endpoint has no parameters array in spec", async () => {
+ const spec = `
+ openapi: 3.0.0
+ info:
+ title: Sample API
+ version: 1.0.0
+ paths:
+ /up:
+ get:
+ summary: Get a 200 status code back
+ responses:
+ '200':
+ description: returns 200 status code
+ `;
+
+ await spectral.validateSuccess(spec, ruleName);
+ });
+});
diff --git a/rulesets/test/root.openapi.yml b/rulesets/test/root.openapi.yml
index 9b96222..8096275 100644
--- a/rulesets/test/root.openapi.yml
+++ b/rulesets/test/root.openapi.yml
@@ -61,7 +61,13 @@ paths:
content:
application/json:
schema:
- $ref: "#/components/schemas/UserList"
+ properties:
+ results:
+ type: array
+ items:
+ $ref: "#/components/schemas/User"
+ paging:
+ $ref: "#/components/schemas/PagingOffset"
"400":
description: Invalid Data
content:
diff --git a/standards/collections.md b/standards/collections.md
index 867fe02..1daf3c9 100644
--- a/standards/collections.md
+++ b/standards/collections.md
@@ -4,11 +4,11 @@
Collection usage and manipulation with HTTP REST APIs specifically require its own set of standardization that provide helpful considerations and must use styles for consistency.
-- All collection-based API responses **MUST** offer paging capability for consistency and evolution, regardless of the collection size or the static nature of the data. Even if the resource is returning a collection of static "types" representing 2 items, pagination should be supported in its simplest form.
+- All collection-based API responses **SHOULD** offer paging capability for consistency and evolution, regardless of the collection size or the static nature of the data. Even if the resource is returning a collection of static "types" representing 2 items, pagination should be supported in its simplest form.
## Root Element
-- All collection-based endpoint responses **MUST** include the collection under the `results` root element.
+- All collection-based endpoint responses **MUST** include the collection under the `results` root element.
```
// CORRECT
@@ -30,7 +30,7 @@ Collection usage and manipulation with HTTP REST APIs specifically require its o
### General
-- All collection-based endpoints `GET` request parameters **MUST** be specified as query parameters with the outlined schema below.
+- All collection-based endpoints `GET` request parameters **SHOULD** be specified as query parameters with the outlined schema below.
```
// CORRECT
@@ -44,7 +44,7 @@ GET https://api.spscommerce.com/v1/books
```
- All collection-based endpoints **SHOULD** use `GET` requests for pagination and not `POST` requests unless it is necessary for the action.
-- All collection-based endpoints `POST` request parameters **MUST** be specified as parameters in the body of the request within the `paging` element and the outlined schema below.
+- All collection-based endpoints `POST` request parameters **MUST** be specified as parameters in the body of the request within the `paging` element and the outlined schema below.
```
// CORRECT
@@ -401,12 +401,12 @@ RESPONSE
To limit or narrow down the results of a collection endpoint you may provide filtering capabilities, where a filter is part of the query parameters.
- Filtering is not a requirement on all collection-based endpoints.
-- Filtering query parameters **MUST** always be optionally applied as indicated by URL Structures that all query parameters are always optional.
-- The resource identifier in a collection **SHOULD NOT** be used to filter collection results, resource identifier should be in the URI.
+- Filtering query parameters **MUST** always be optionally applied as indicated by URL Structures that all query parameters are always optional.
+- The resource identifier in a collection **SHOULD NOT** be used to filter collection results, resource identifier should be in the URI.
- Filtering **SHOULD** only occur on endpoints that are collections using the schema described above.
- Filtering attribute names may represent nested objects and **MUST** use a period to represent each segment of the object path: `grandparent.parent.child`.
- Limit filter references to three levels of object hierarchy in accordance with `GET-based` HTTP Methods ([Request Response](request-response.md)).
-- Filtering **MUST** only be implemented on `GET-based` HTTP Methods via query parameters.
+- Filtering **MUST** only be implemented on `GET-based` HTTP Methods via query parameters.
- Filtering using `GET-based` requests with query parameters **SHOULD** be avoided if expected use cases or allowed usage resolves URL lengths beyond a reasonable size for the developer experience or approaching limits defined in [URL Structure](url-structure.md).
- Overly verbose filtering that contains dozens or hundreds of parameters **SHOULD** consider if their API design is appropriate.
- Overly verbose filtering that contains an undesirable number of parameters that cannot be redesigned **SHOULD** consider using a non-REST style `POST` endpoint as described under [Actions in URL Structure](url-structure.md).
@@ -516,7 +516,7 @@ RSQL is based on FIQL and is considered a superset of it, making it and FIQL usa
- Using "simple" or "advanced" filtering without the hybrid approach **SHOULD** be the preferred choice. Hybrid filtering is not desirable but may be necessary based on the constraints of your implementation and requirements (including performance).
- Hybrid filtering is intended to support scenarios where API producers are unable to provide advanced filtering capability on all aspects of the payload response attributes and want to provide scope clarity in the attribute filter name.
- Hybrid filtering attribute values **MUST** be valid advanced filtering expressions (FIQL/RSQL).
- - Hybrid filtering **MAY** be offered on multiple attributes, but **MUST** never exist if a root "filter" query parameter is available.
+ - Hybrid filtering **MAY** be offered on multiple attributes, but **MUST** never exist if a root "filter" query parameter is available.
- Hybrid filtering with multiple attribute filters **MUST** logically "AND" the results of both filters together (unless the attribute name is repeated, in which case repeated attributes names are "OR" for the results as described in simple filtering).
- Hybrid filtering **MAY** be combined with additional simple filtering query parameters, provided they do not have a suffix of `Filter`.
@@ -600,12 +600,12 @@ The dynamic nature of filters means that all fields cannot be listed in Open API
Sorting on collection endpoints should be done by specifying the attributes that should be sorted or ordered by using an ordering query parameter.
- Sorting is not a requirement on all collection-based endpoints.
-- Sorting query parameters **MUST** always be optionally applied as indicated by URL Structures that all query parameters are always optional.
+- Sorting query parameters **MUST** always be optionally applied as indicated by URL Structures that all query parameters are always optional.
- Default sort order **SHOULD** be considered as `undefined` and non-deterministic from the API consumer's perspective when no sorting query parameters are provided.
- A default sort order **MUST** be applied internally for implementation purposes to provide consistently paged responses.
- A default sort order modification is not considered an API breaking change unless the behavior is documented as such.
- Sorting **SHOULD** only occur on endpoints that are collections using the schema described above.
-- Sorting **MUST** only be implemented on `GET-based` HTTP Methods via query parameters.
+- Sorting **MUST** only be implemented on `GET-based` HTTP Methods via query parameters.
- Sorting query parameters **MUST** be included as part of the pagination next/previous URLs, similar to how `limit` is included as an additional query parameter.
- If an explicit sort order is desired, the query parameter `ordering` **MUST** be used to specify attribute names.
- Specifying an attribute name in the ordering parameter **MUST** sort by that attribute ascending (ASC). Specifying a `-` sign in front modifies to descending (DESC) on the same attribute.