functions/naming-convention.js (117 lines of code) (raw):

// Check naming convention. // options: // type: 'boolean' | 'date-time' // match: RegExp // notMatch: RegExp /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ const { pattern } = require('@stoplight/spectral-functions'); function isBooleanSchema(schema) { return schema.type === 'boolean'; } function isDateTimeSchema(schema) { return schema.type === 'string' && schema.format === 'date-time'; } function isSchemaType(type) { switch (type) { case 'boolean': return isBooleanSchema; case 'date-time': return isDateTimeSchema; default: return (_) => false; } } // Check all property names in the schema comply with the naming convention. function propertyNamingConvention(schema, options, path) { const errors = []; const { type, ...patternOpts } = options; const isType = isSchemaType(type); // Check property names for (const name of schema.properties ? Object.keys(schema.properties) : []) { if (isType(schema.properties[name]) && pattern(name, patternOpts)) { errors.push({ message: `property "${name}" does not follow ${options.type} naming convention`, path: [...path, 'properties', name], }); } } if (schema.items) { errors.push( ...propertyNamingConvention(schema.items, options, [...path, 'items']), ); } for (const applicator of ['allOf', 'anyOf', 'oneOf']) { if (schema[applicator] && Array.isArray(schema[applicator])) { for (const [index, value] of schema[applicator].entries()) { errors.push( ...propertyNamingConvention(value, options, [...path, applicator, index]), ); } } } return errors; } // input is ignored -- we take the whole document as input // Rule is run on resolved doc. module.exports = (input, options, _context) => { const oasDoc = input; const oas2 = oasDoc.swagger === '2.0'; const oas3 = oasDoc.openapi?.startsWith('3.') || false; const { type, ...patternOpts } = options; const isType = isSchemaType(type); const errors = []; // Check all property names in the schema comply with the naming convention. for (const pathKey of Object.keys(oasDoc.paths)) { const pathItem = oasDoc.paths[pathKey]; for (const opMethod of ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']) { if (pathItem[opMethod]) { const op = pathItem[opMethod]; // Processing for oas2 documents if (oas2) { // Check the oas2 parameters for (let i = 0; i < op.parameters?.length || 0; i += 1) { const param = op.parameters[i]; if (param.in !== 'body' && isType(param) && pattern(param.name, patternOpts)) { errors.push({ message: `parameter "${param.name}" does not follow ${options.type} naming convention`, path: ['paths', pathKey, opMethod, 'parameters', i, 'name'], }); } } // Check the oas2 body parameter const bodyParam = op.parameters?.find((p) => p.in === 'body'); if (bodyParam) { const bodyIndex = op.parameters.indexOf(bodyParam); errors.push( ...propertyNamingConvention(bodyParam.schema, options, ['paths', pathKey, opMethod, 'parameters', bodyIndex, 'schema']), ); } // Check the oas2 responses for (const [responseKey, response] of Object.entries(op.responses)) { if (response.schema) { errors.push( ...propertyNamingConvention(response.schema, options, ['paths', pathKey, opMethod, 'responses', responseKey, 'schema']), ); } } } // Processing for oas3 documents if (oas3) { // Check the oas3 parameters for (let i = 0; i < op.parameters?.length || 0; i += 1) { const param = op.parameters[i]; if (param.schema && isType(param.schema) && pattern(param.name, patternOpts)) { errors.push({ message: `parameter "${param.name}" does not follow ${options.type} naming convention`, path: ['paths', pathKey, opMethod, 'parameters', i, 'name'], }); } } // Check the oas3 requestBody if (op.requestBody?.content) { for (const [contentTypeKey, contentType] of Object.entries(op.requestBody.content)) { if (contentType.schema) { errors.push( ...propertyNamingConvention(contentType.schema, options, ['paths', pathKey, opMethod, 'requestBody', 'content', contentTypeKey, 'schema']), ); } } } // Check the oas3 responses if (op.responses) { for (const [responseKey, response] of Object.entries(op.responses)) { if (response.content) { for (const [contentTypeKey, contentType] of Object.entries(response.content)) { if (contentType.schema) { errors.push( ...propertyNamingConvention(contentType.schema, options, ['paths', pathKey, opMethod, 'responses', responseKey, 'content', contentTypeKey, 'schema']), ); } } } } } } } } } return errors; };