Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 77 additions & 7 deletions __tests__/page-parameter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("Rule page-parameter", () => {
spectral = createWithRules(["page-parameter"]);
});

it("page uses deepObject but invalid member types", async () => {
it("invalid: single page parameter must use deepObject", async () => {
await expectRuleErrors(
spectral,
"page-parameter",
Expand All @@ -22,11 +22,11 @@ describe("Rule page-parameter", () => {
{
name: "page",
in: "query",
style: "deepObject",
style: "form",
schema: {
type: "object",
properties: {
limit: { type: "string" },
limit: { type: "integer", format: "int32" },
},
},
},
Expand All @@ -39,7 +39,43 @@ describe("Rule page-parameter", () => {
[
{
message:
"page must be a deepObject query parameter that matches the pagination schema.",
"page query parameters must use either deepObject page or page[...] query parameters.",
path: ["paths", "/articles", "get", "parameters", "0", "style"],
severity: DiagnosticSeverity.Error,
},
],
);
});

it("invalid: deepObject page parameter set but no defined properties lay within", async () => {
await expectRuleErrors(
spectral,
"page-parameter",
{
...openApiBase,
paths: {
"/articles": {
get: {
parameters: [
{
name: "page",
in: "query",
style: "deepObject",
schema: {
type: "object",
properties: {},
},
},
],
responses: { "200": { description: "ok" } },
},
},
},
},
[
{
message:
"page query parameters must use either deepObject page or page[...] query parameters.",
path: [
"paths",
"/articles",
Expand All @@ -48,16 +84,14 @@ describe("Rule page-parameter", () => {
"0",
"schema",
"properties",
"limit",
"type",
],
severity: DiagnosticSeverity.Error,
},
],
);
});

it("cursor pagination shape passes", async () => {
it("valid: deepObject page parameter passes when it has properties", async () => {
await expectRuleErrors(
spectral,
"page-parameter",
Expand Down Expand Up @@ -88,4 +122,40 @@ describe("Rule page-parameter", () => {
[],
);
});

it("valid: any page[] parameters pass", async () => {
await expectRuleErrors(
spectral,
"page-parameter",
{
...openApiBase,
paths: {
"/articles": {
get: {
parameters: [
{
name: "page[size]",
in: "query",
required: false,
schema: {
type: "integer",
},
},
{
name: "page[anything]",
in: "query",
required: false,
schema: {
type: "integer",
},
},
],
responses: { "200": { description: "ok" } },
},
},
},
},
[],
);
});
});
206 changes: 106 additions & 100 deletions src/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1903,137 +1903,143 @@ Related specification information can be found [here](https://jsonapi.org/format
],
},
"page-parameter": {
description: `\`page\` query param **MUST** follow schema
description: `\`page\` query parameters **MUST** follow schema

**Schema Rules:**
- **MUST** be type \`object\`
- **MUST** be style \`deepObject\`
- contents depend on strategy:
- Either define a single \`page\` query parameter using \`deepObject\`
- Or define multiple query parameters using bracket notation like \`page[size]\`
- Either way, the page[foo] parameters will vary by pagination strategy:
- cursor: \`string\` \`cursor\` and \`int32\` \`limit\`
- offset: \`int32\` \`offset\` and \`int32\` \`limit\`

**Valid Examples:**
\`\`\`yaml
name: page
description: Paging parameter, cursor based.
in: query
schema:
type: object
required: ["cursor","limit"]
properties:
cursor:
type: string
limit:
type: integer
format: int32
style: deepObject

name: page
description: Paging parameter, offset based.
in: query
schema:
type: object
required: ["offset","limit"]
properties:
cursor:
type: integer
format: int32
limit:
type: integer
format: int32
style: deepObject
- name: page
description: Paging parameter, cursor based.
in: query
style: deepObject
schema:
type: object
required: ["cursor", "limit"]
properties:
cursor:
type: string
limit:
type: integer
format: int32

- name: page
description: Paging parameter, offset based.
in: query
style: deepObject
schema:
type: object
required: ["offset","limit"]
properties:
cursor:
type: integer
format: int32
limit:
type: integer
format: int32

# Single parameters using bracket notation

- name: page[size]
in: query
required: false
schema:
type: integer
- name: page[limit]
in: query
required: false
schema:
type: integer
\`\`\`
Example query string:
- \`/myResources?page[cursor]=fdsJ34lkjSfjsdfk&page[limit]=10\`
- \`/myResources?page[offset]=2&page[limit]=10\`
- \`/myResources?page[size]=10&page[number]=2\`

Related specification information can be found [here](https://jsonapi.org/format/1.1/#fetching-pagination).`,
documentationUrl: "https://jsonapi.org/format/1.1/#fetching-pagination",
message:
"page must be a deepObject query parameter that matches the pagination schema.",
"page query parameters must use either deepObject page or page[...] query parameters.",
severity: DiagnosticSeverity.Error,
given: "$.paths..parameters[*][?(@property === 'name' && @ === 'page')]^",
then: [
{
field: "in",
function: enumeration,
functionOptions: {
values: ["query"],
},
},
{
field: "style",
function: truthy,
},
{
field: "style",
function: enumeration,
functionOptions: {
values: ["deepObject"],
},
},
{
field: "schema",
function: schema,
functionOptions: {
dialect: "draft2020-12",
schema: {
type: "object",
given: [
"$.paths..parameters[*][?(@property === 'name' && @ === 'page')]^",
"$.paths..parameters[*][?(@property === 'name' && @.match(/^page\\[[^\\]]+\\]$/))]^",
],
then: {
function: schema,
functionOptions: {
dialect: "draft2020-12",
schema: {
type: "object",
required: ["name", "in", "schema"],
if: {
properties: {
type: {
name: {
type: "string",
enum: ["object"],
const: "page",
},
properties: {
},
},
then: {
required: ["style"],
properties: {
name: {
type: "string",
const: "page",
},
in: {
type: "string",
enum: ["query"],
},
style: {
type: "string",
enum: ["deepObject"],
},
schema: {
type: "object",
additionalProperties: false,
properties: {
cursor: {
type: "object",
properties: {
type: {
type: "string",
enum: ["string"],
},
},
type: {
type: "string",
enum: ["object"],
},
offset: {
properties: {
type: "object",
properties: {
type: {
type: "string",
enum: ["integer"],
},
format: {
type: "string",
enum: ["int32"],
},
minimum: {
type: "integer",
minimum: 0,
},
},
minProperties: 1,
},
limit: {
type: "object",
properties: {
type: {
type: "string",
enum: ["integer"],
},
format: {
type: "string",
enum: ["int32"],
},
},
},
},
},
},
else: {
properties: {
name: {
type: "string",
pattern: "^page\\[[^\\]]+\\]$",
},
in: {
type: "string",
enum: ["query"],
},
schema: {
type: "object",
required: ["type"],
properties: {
type: {
type: "string",
enum: ["string", "integer", "number"],
},
},
},
},
},
},
},
],
},
},
"post-requests-single-object": {
description: `POST requests **MUST** only contain a single resource object
Expand Down