Skip to content
Draft
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
6 changes: 4 additions & 2 deletions aep/0132.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ aliases:
functionsDir: ../functions
functions:
- skipSingletonsSchema
- get-no-request-body

rules:
aep-132-http-body:
description: A list operation must not accept a request body.
message: '{{error}}'
severity: error
formats: ['oas3']
given:
- '#ListOperation.requestBody'
- $.paths[*]
then:
function: falsy
function: get-no-request-body

aep-132-operation-id:
description: The operation ID should be List{resource-singular}.
Expand Down
21 changes: 11 additions & 10 deletions aep/0135.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ aliases:
# first condition excludes custom methods and second condition matches paths ending in a path parameter
- $.paths[?([email protected](/:[^/]*$/) && @property.match(/\}$/))].delete

functionsDir: ../functions
functions:
- delete-no-request-body
- delete-response-204

rules:
aep-135-http-body:
description: A delete operation must not accept a request body.
message: '{{error}}'
severity: error
formats: ['oas3']
given:
- '#DeleteOperation.requestBody'
- $.paths[*]
then:
function: falsy
function: delete-no-request-body

aep-135-operation-id:
description: The operation ID should be Delete{resource-singular}.
Expand All @@ -36,14 +42,9 @@ rules:

aep-135-response-204:
description: A delete operation should have a 204 response.
message: A delete operation should have a `204` response.
message: '{{error}}'
severity: warn
formats: ['oas2', 'oas3']
given: '#DeleteOperation.responses'
given: $.paths[*]
then:
function: schema
functionOptions:
schema:
oneOf:
- required: ['204']
- required: ['202']
function: delete-response-204
35 changes: 35 additions & 0 deletions functions/delete-no-request-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Check that DELETE operations on resources with x-aep-resource do not have a request body

const { shouldLintOperation } = require('./resource-utils');

module.exports = (pathItem, _opts, context, otherValues) => {
if (!pathItem || typeof pathItem !== 'object') {
return [];
}

const errors = [];
const pathArray = context.path || context.target || [];

// Extract the path string from the context
const pathString = pathArray.length >= 2 ? pathArray[1] : '';

// Get the full document from otherValues
const document = otherValues?.documentInventory?.resolved || otherValues?.document;

if (!document) {
return [];
}

// Check if DELETE method exists and has a requestBody
if (pathItem.delete && pathItem.delete.requestBody) {
// Only lint if this operation is on a resource with x-aep-resource
if (shouldLintOperation(pathItem.delete, 'delete', pathString, pathItem, document)) {
errors.push({
message: 'A delete operation must not accept a request body.',
path: [...pathArray, 'delete', 'requestBody'],
});
}
}

return errors;
};
39 changes: 39 additions & 0 deletions functions/delete-response-204.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Check that DELETE operations on resources with x-aep-resource have a 204 or 202 response

const { shouldLintOperation } = require('./resource-utils');

module.exports = (pathItem, _opts, context, otherValues) => {
if (!pathItem || typeof pathItem !== 'object') {
return [];
}

const errors = [];
const pathArray = context.path || context.target || [];

// Extract the path string from the context
const pathString = pathArray.length >= 2 ? pathArray[1] : '';

// Get the full document from otherValues
const document = otherValues?.documentInventory?.resolved || otherValues?.document;

if (!document) {
return [];
}

// Check if DELETE method exists
if (pathItem.delete && pathItem.delete.responses) {
// Only lint if this operation is on a resource with x-aep-resource
if (shouldLintOperation(pathItem.delete, 'delete', pathString, pathItem, document)) {
const responses = pathItem.delete.responses;
// Check if 204 or 202 response exists
if (!responses['204'] && !responses['202']) {
errors.push({
message: 'A delete operation should have a `204` response.',
path: [...pathArray, 'delete', 'responses'],
});
}
}
}

return errors;
};
37 changes: 37 additions & 0 deletions functions/get-no-request-body.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Check that GET operations on resources with x-aep-resource do not have a request body

const { shouldLintOperation } = require('./resource-utils');

module.exports = (pathItem, _opts, context, otherValues) => {
if (!pathItem || typeof pathItem !== 'object') {
return [];
}

const errors = [];
const pathArray = context.path || context.target || [];

// Extract the path string from the context
// The path array looks like: ['paths', '/users/{id}', ...]
const pathString = pathArray.length >= 2 ? pathArray[1] : '';

// Get the full document from otherValues
const document = otherValues?.documentInventory?.resolved || otherValues?.document;

if (!document) {
// If we can't get the document, we can't check x-aep-resource, so skip
return [];
}

// Check if GET method exists and has a requestBody
if (pathItem.get && pathItem.get.requestBody) {
// Only lint if this operation is on a resource with x-aep-resource
if (shouldLintOperation(pathItem.get, 'get', pathString, pathItem, document)) {
errors.push({
message: 'A list operation must not accept a request body.',
path: [...pathArray, 'get', 'requestBody'],
});
}
}

return errors;
};
103 changes: 75 additions & 28 deletions functions/parameter-names-unique.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Check that the parameters of an operation -- including those specified on the path -- are
// are case-insensitive unique regardless of "in".

const { shouldLintOperation } = require('./resource-utils');

// Return the "canonical" casing for a string.
// Currently just lowercase but should be extended to convert kebab/camel/snake/Pascal.
function canonical(name) {
Expand All @@ -22,57 +24,102 @@ function dupIgnoreCase(arr) {
// targetVal should be a
// [path item object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#pathItemObject).
// The code assumes it is running on a resolved doc
module.exports = (pathItem, _opts, paths) => {
module.exports = (pathItem, _opts, context, otherValues) => {
if (pathItem === null || typeof pathItem !== 'object') {
return [];
}
const path = paths.path || paths.target || [];

const pathArray = context.path || context.target || [];
const pathString = pathArray.length >= 2 ? pathArray[1] : '';

// Get the full document from otherValues
const document = otherValues?.documentInventory?.resolved || otherValues?.document;

if (!document) {
return [];
}

const errors = [];

const pathParams = pathItem.parameters ? pathItem.parameters.map((p) => p.name) : [];
const pathParams = pathItem.parameters
? pathItem.parameters.map((p) => p.name)
: [];

// Only check parameters if at least one method on this path should be linted
let shouldLintAnyMethod = false;
['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].forEach((method) => {
if (pathItem[method]) {
if (shouldLintOperation(pathItem[method], method, pathString, pathItem, document)) {
shouldLintAnyMethod = true;
}
}
});

if (!shouldLintAnyMethod) {
return [];
}

// Check path params for dups
const pathDups = dupIgnoreCase(pathParams);

// Report all dups
pathDups.forEach((dup) => {
// get the index of all names that match dup
const dupKeys = [...pathParams.keys()].filter((k) => canonical(pathParams[k]) === dup);
const dupKeys = [...pathParams.keys()].filter(
(k) => canonical(pathParams[k]) === dup
);
// Report errors for all the others
dupKeys.slice(1).forEach((key) => {
errors.push({
message: `Duplicate parameter name (ignoring case): ${dup}.`,
path: [...path, 'parameters', key, 'name'],
path: [...pathArray, 'parameters', key, 'name'],
});
});
});

['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].forEach((method) => {
// If this method exists and it has parameters, check them
if (pathItem[method] && Array.isArray(pathItem[method].parameters)) {
const allParams = [...pathParams, ...pathItem[method].parameters.map((p) => p.name)];

// Check method params for dups -- including path params
const dups = dupIgnoreCase(allParams);

// Report all dups
dups.forEach((dup) => {
// get the index of all names that match dup
const dupKeys = [...allParams.keys()].filter((k) => canonical(allParams[k]) === dup);
// Report errors for any others that are method parameters
dupKeys
.slice(1)
.filter((k) => k >= pathParams.length)
.forEach((key) => {
errors.push({
message: `Duplicate parameter name (ignoring case): ${dup}.`,
path: [...path, method, 'parameters', key - pathParams.length, 'name'],
['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].forEach(
(method) => {
// If this method exists and it has parameters, check them
if (pathItem[method] && Array.isArray(pathItem[method].parameters)) {
// Only check if this specific method should be linted
if (!shouldLintOperation(pathItem[method], method, pathString, pathItem, document)) {
return;
}

const allParams = [
...pathParams,
...pathItem[method].parameters.map((p) => p.name),
];

// Check method params for dups -- including path params
const dups = dupIgnoreCase(allParams);

// Report all dups
dups.forEach((dup) => {
// get the index of all names that match dup
const dupKeys = [...allParams.keys()].filter(
(k) => canonical(allParams[k]) === dup
);
// Report errors for any others that are method parameters
dupKeys
.slice(1)
.filter((k) => k >= pathParams.length)
.forEach((key) => {
errors.push({
message: `Duplicate parameter name (ignoring case): ${dup}.`,
path: [
...pathArray,
method,
'parameters',
key - pathParams.length,
'name',
],
});
});
});
});
});
}
}
});
);

return errors;
};
40 changes: 40 additions & 0 deletions functions/request-body-required.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Check that POST/PUT/PATCH operations on resources with x-aep-resource have required request bodies

const { shouldLintOperation } = require('./resource-utils');

module.exports = (pathItem, _opts, context, otherValues) => {
if (!pathItem || typeof pathItem !== 'object') {
return [];
}

const errors = [];
const pathArray = context.path || context.target || [];

// Extract the path string from the context
const pathString = pathArray.length >= 2 ? pathArray[1] : '';

// Get the full document from otherValues
const document = otherValues?.documentInventory?.resolved || otherValues?.document;

if (!document) {
return [];
}

// Check PUT, POST, and PATCH methods
['put', 'post', 'patch'].forEach((method) => {
if (pathItem[method] && pathItem[method].requestBody) {
// Only lint if this operation is on a resource with x-aep-resource
if (shouldLintOperation(pathItem[method], method, pathString, pathItem, document)) {
const requestBody = pathItem[method].requestBody;
if (!requestBody.required) {
errors.push({
message: 'The body parameter is not marked as required.',
path: [...pathArray, method, 'requestBody'],
});
}
}
}
});

return errors;
};
Loading
Loading