UNPKG

@ibm-cloud/openapi-ruleset

Version:

Spectral ruleset for validating IBM Cloud services

202 lines (182 loc) 8.22 kB
/** * Copyright 2017 - 2023 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const { each, merge, pickBy, reduce } = require('lodash'); const { operationMethods } = require('../utils'); module.exports = function (rootDocument) { return operationIdNamingConvention(rootDocument); }; function operationIdNamingConvention(resolvedSpec) { const operations = reduce( resolvedSpec.paths, (arr, path, pathKey) => { const pathOps = pickBy(path, (obj, k) => { return operationMethods.indexOf(k) > -1; }); const allPathOperations = Object.keys(pathOps); each(pathOps, (op, opKey) => arr.push( merge( { pathKey: `${pathKey}`, opKey: `${opKey}`, path: ['paths', `${pathKey}`, `${opKey}`], allPathOperations, }, op ) ) ); return arr; }, [] ); const errors = []; operations.forEach(op => { if (op.operationId) { const pathEndsWithParam = op.pathKey.endsWith('}'); const numParamRefs = (op.pathKey.match(/\{[A-Za-z0-9-_]+\}/g) || []) .length; // We'll use a heuristic to decide if this path is part of a resource oriented API. // If the path ends in a path param reference, look for the corresponding path // without the trailing path param reference. // Conversely, if path doesn't end with a path param reference, look for the corresponding // path with the trailing path param reference. const isResourceOriented = pathEndsWithParam ? Object.keys(resolvedSpec.paths).includes( op.pathKey.replace('/\\{[A-Za-z0-9-_]+\\}$', '') ) : Object.keys(resolvedSpec.paths).some(p => p.startsWith(op.pathKey + '/{') ); const { checkPassed, verbs } = operationIdPassedConventionCheck( isResourceOriented, op['opKey'], op.operationId, pathEndsWithParam, numParamRefs ); if (checkPassed === false) { errors.push({ message: `operationIds should follow naming convention: operationId verb should be ${verbs.join( ' or ' )}`, path: [...op.path, 'operationId'], }); } } }); return errors; } /** * This function checks to make sure that the operation's operationId starts with * an acceptable verb (e.g. 'create', 'list', etc.) depending on various * characteristics of the operation's path and http method. * * This function and the table below should be kept in sync with the API Handbook guidance in this area, * located here: https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-operations * The following table attempts to capture these rules so that the logic below is a little * easier to understand: * * HTTP | | Resource | Does path end | # of param | Acceptable | Operation ID * Method | Path Example | Oriented? | w/ param ref? | refs in path | Verbs | Examples * -------+------------------------------+-----------+---------------+---------------+----------------+--------------------------------- * get | /bands | Y | N | 0+ | list | list_bands * | /bands/{id} | Y | Y | 1 | get | get_band * | /bands/{id}/albums/{album_id}| Y | Y | 2+ | get, check | get_band_album, check_band_album * | dc | N | dc | dc | dc | <no check> * | | | | | | * post | /bands | Y | N | dc | create | create_band * | dc | N | dc | dc | dc | <no check> * | | | | | | * patch | /bands/{id} | dc | dc | dc | update | update_band * | | | | | | * put | /bands | Y | N | 0 | replace | replace_bands * | /bands/{id} | Y | Y | 1 | replace | replace_band * | /bands/{id}/albums | N | N | 1+ | replace, set | replace_band_albums, set_band_albums * | /bands/{id}/albums/{album_id}| Y | Y | 2+ | replace, add | replace_band_album, add_band_album * | | | | | | * delete | /bands | dc | N | 0+ | delete | delete_bands * | /bands/{id} | dc | Y | 1 | delete | delete_band * | /bands/{id}/albums/{album_id}| Y | Y | 2+ | delete, remove | delete_band_album, remove_band_album * | /bands/{id}/albums | Y | N | 0+ | delete | delete_band_albums * | /bands/{id}/albums | N | N | 1+ | delete, unset | delete_band_albums, unset_band_albums * | | | | | | * * Legend: * - "dc" = don't care * - resource-oriented implies a particular path is part of a pair of paths that differ only in that one has a trailing * path parameter reference and one does not (e.g. '/albums' and '/albums/{album_id}'). * * Please try to keep this table in sync with the API Handbook guidance and the logic in the function below. * * @param {boolean} isResourceOriented a flag that indicates whether the operation is resource-oriented or not * @param {string} httpMethod the http method (get, post, etc.) associated with the operation * @param {string} operationId the operation's operationId * @param {boolean} pathEndsWithParam a flag that indicates whether or not the path ends with a path parameter reference * @param {number} numParamRefs the number of path parameter references in the path * @returns */ function operationIdPassedConventionCheck( isResourceOriented, httpMethod, operationId, pathEndsWithParam, numParamRefs ) { // Useful for debugging. // console.log(`Debug: ${httpMethod} ${isResourceOriented} ${pathEndsWithParam} ${numParamRefs} ${operationId}`); const verbs = []; switch (httpMethod) { case 'get': if (isResourceOriented) { if (!pathEndsWithParam) { verbs.push('list'); } else { verbs.push('get'); } if (numParamRefs >= 2) { verbs.push('check'); } } break; case 'post': if (isResourceOriented) { verbs.push('create'); } break; case 'patch': verbs.push('update'); break; case 'put': verbs.push('replace'); if (isResourceOriented) { if (pathEndsWithParam && numParamRefs >= 2) { verbs.push('add'); } } else if (!pathEndsWithParam && numParamRefs >= 1) { verbs.push('set'); } break; case 'delete': verbs.push('delete'); if (isResourceOriented) { if (pathEndsWithParam && numParamRefs >= 2) { verbs.push('remove'); } } else if (!pathEndsWithParam && numParamRefs >= 1) { verbs.push('unset'); } break; } // If we have a non-empty list of acceptable verbs, then make sure // that the operationId starts with one of them. if (verbs.length > 0) { const checkPassed = verbs .map(verb => operationId.startsWith(verb)) .some(v => v); return { checkPassed, verbs }; } return { checkPassed: true }; }