Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import testRule from './__helpers__/testRule';
import { DiagnosticSeverity } from '@stoplight/types';

testRule('xgen-IPA-123-enum-values-must-be-upper-snake-case', [
{
name: 'valid schema - components.schemas',
document: {
components: {
schemas: {
SchemaName: {
properties: {
exampleProperty: {
enum: ['EXAMPLE_A', 'EXAMPLE_B'],
type: 'string',
},
},
},
},
},
},
errors: [],
},
{
name: 'invalid schema with exception - components.schemas',
document: {
components: {
schemas: {
SchemaName: {
'x-xgen-IPA-exception': {
'xgen-IPA-123-enum-values-must-be-upper-snake-case': 'reason',
},
properties: {
exampleProperty: {
enum: ['exampleA', 'exampleB'],
type: 'string',
},
},
},
},
},
},
errors: [],
},
{
name: 'invalid schema - components.schemas',
document: {
components: {
schemas: {
SchemaName: {
properties: {
exampleProperty: {
enum: ['exampleA', 'exampleB'],
type: 'string',
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
message: 'exampleA enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
path: ['components', 'schemas', 'SchemaName', 'properties', 'exampleProperty', 'enum', '0'],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
message: 'exampleB enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
path: ['components', 'schemas', 'SchemaName', 'properties', 'exampleProperty', 'enum', '1'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'valid schema - paths.*',
document: {
paths: {
'/a/{exampleId}': {
get: {
parameters: [
{
schema: {
type: 'string',
enum: ['EXAMPLE_A', 'EXAMPLE_B'],
},
},
],
},
},
},
},
errors: [],
},
{
name: 'invalid schema with exception - paths.*',
document: {
paths: {
'/a/{exampleId}': {
get: {
parameters: [
{
schema: {
'x-xgen-IPA-exception': {
'xgen-IPA-123-enum-values-must-be-upper-snake-case': 'reason',
},
type: 'string',
enum: ['exampleA', 'exampleB'],
},
},
],
},
},
},
},
errors: [],
},
{
name: 'invalid schema - paths.*',
document: {
paths: {
'/a/{exampleId}': {
get: {
parameters: [
{
schema: {
type: 'string',
enum: ['exampleA', 'exampleB'],
},
},
],
},
},
},
},
errors: [
{
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
message: 'exampleA enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
path: ['paths', '/a/{exampleId}', 'get', 'parameters', '0', 'schema', 'enum', '0'],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
message: 'exampleB enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
path: ['paths', '/a/{exampleId}', 'get', 'parameters', '0', 'schema', 'enum', '1'],
severity: DiagnosticSeverity.Warning,
},
],
},
]);
1 change: 1 addition & 0 deletions tools/spectral/ipa/ipa-spectral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ extends:
- ./rulesets/IPA-102.yaml
- ./rulesets/IPA-104.yaml
- ./rulesets/IPA-109.yaml
- ./rulesets/IPA-123.yaml
14 changes: 14 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-123.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# IPA-123: Enums
# http://go/ipa/123

functions:
- eachEnumValueMustBeUpperSnakeCase

rules:
xgen-IPA-123-enum-values-must-be-upper-snake-case:
description: 'Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123'
message: '{{error}} http://go/ipa/123'
severity: warn
given: '$..enum'
then:
function: 'eachEnumValueMustBeUpperSnakeCase'
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { hasException } from './utils/exceptions.js';
import { getSchemaPath, resolveObject } from './utils/componentUtils.js';
import { casing } from '@stoplight/spectral-functions';

const RULE_NAME = 'xgen-IPA-123-enum-values-must-be-upper-snake-case';
const ERROR_MESSAGE = 'enum value must be UPPER_SNAKE_CASE.';

export default (input, _, { path, documentInventory }) => {
const oas = documentInventory.resolved;
const schemaPath = getSchemaPath(path);
const schemaObject = resolveObject(oas, schemaPath);
if (hasException(schemaObject, RULE_NAME)) {
return;
}

const errors = [];
input.forEach((enumValue, index) => {
const isUpperSnakeCase = casing(enumValue, { type: 'macro' });

if (isUpperSnakeCase) {
errors.push({
path: [...path, index],
message: `${enumValue} ${ERROR_MESSAGE} `,
});
}
});

return errors;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPathParam } from './utils/pathUtils.js';
import { isPathParam } from './utils/componentUtils.js';
import { hasException } from './utils/exceptions.js';

const RULE_NAME = 'xgen-IPA-102-path-alternate-resource-name-path-param';
Expand Down
75 changes: 75 additions & 0 deletions tools/spectral/ipa/rulesets/functions/utils/componentUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Checks if a string belongs to a path parameter or a path parameter with a custom method.
*
* A path parameter has the format: `{paramName}`
* A path parameter with a custom method has the format: `{paramName}:customMethod`
*
* @param {string} str - A string extracted from a path split by slashes.
* @returns {boolean} True if the string matches the expected formats, false otherwise.
*/
export function isPathParam(str) {
const pathParamRegEx = new RegExp(`^{[a-z][a-zA-Z0-9]*}$`);
const pathParamWithCustomMethodRegEx = new RegExp(`^{[a-z][a-zA-Z0-9]*}:[a-z][a-zA-Z0-9]*$`);
return pathParamRegEx.test(str) || pathParamWithCustomMethodRegEx.test(str);
}

/**
* Extracts the schema path from the given JSONPath array.
*
* This function is designed to handle two types of paths commonly encountered in OpenAPI definitions:
*
* 1. **Component Schema Paths**:
* - Represented as: `components.schemas.schemaName.*.enum`
* - This path indicates that the enum is defined within a schema under `components.schemas`.
* - The function returns the first three elements (`["components", "schemas", "schemaName"]`).
*
* 2. **Parameter Schema Paths**:
* - Represented as: `paths.*.method.parameters[*].schema.enum`
* - This path indicates that the enum is part of a parameter's schema in an operation.
* - The function identifies the location of `schema` in the path and returns everything up to (and including) it.
*
* @param {string[]} path - An array representing the JSONPath structure of the OpenAPI definition.
* @returns {string[]} The truncated path pointing to the schema object.
*/
export function getSchemaPath(path) {
if (path.includes('components')) {
return path.slice(0, 3);
} else if (path.includes('paths')) {
const index = path.findIndex((item) => item === 'schema');
return path.slice(0, index + 1);
}
}

/**
* Resolves the value of a nested property within an OpenAPI structure using a given path.
*
* This function traverses an OpenAPI object based on a specified path (array of keys)
* and retrieves the value at the end of the path. If any key in the path is not found,
* or the value is undefined at any point, the function will return `undefined`.
*
* @param {Object} oas - The entire OpenAPI Specification object.
* @param {string[]} objectPath - An array of strings representing the path to the desired value.
* For example, `['components', 'schemas', 'MySchema', 'properties']`.
* @returns {*} The value at the specified path within the OpenAPI object, or `undefined` if the path is invalid.
*
* @example
* const oas = {
* components: {
* schemas: {
* MySchema: {
* properties: {
* fieldName: { type: 'string' }
* }
* }
* }
* }
* };
*
* const result = resolveObject(oas, ['components', 'schemas', 'MySchema', 'properties']);
* console.log(result); // Output: { fieldName: { type: 'string' } }
*/
export function resolveObject(oas, objectPath) {
return objectPath.reduce((current, key) => {
return current && current[key] ? current[key] : undefined;
}, oas);
}
14 changes: 0 additions & 14 deletions tools/spectral/ipa/rulesets/functions/utils/pathUtils.js

This file was deleted.

Loading