Skip to content

Commit

Permalink
Merge pull request #25 from dtolb/DT_update-functions
Browse files Browse the repository at this point in the history
Updates to Rulesets, Tests, & Functions
  • Loading branch information
coliu19 authored Aug 27, 2024
2 parents 7bdad66 + a3cf9b8 commit 66b940c
Show file tree
Hide file tree
Showing 53 changed files with 2,562 additions and 632 deletions.
File renamed without changes.
4 changes: 2 additions & 2 deletions contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export default {
},
},
'general-schema-definition': {
'description': 'Some of the defined schema use object as a final field when describing their object structure.',
'description': 'Some of the defined schema use object as a final field when describing their object structure',
'message': '{{description}}; {{error}}',
'severity': 'error',
'given': [
Expand Down Expand Up @@ -100,7 +100,7 @@ export default {
},
},
'oas2-missing-returned-representation': {
'description': '2XX (except 204) responses must have a response schema defined',
'description': '2XX (except 204) responses must have a response schema defined.',
'message': '{{description}}; {{error}}',
'severity': 'error',
'formats': [oas2],
Expand Down
52 changes: 48 additions & 4 deletions documentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import { oas } from '@stoplight/spectral-rulesets';
import { xor } from '@stoplight/spectral-functions/dist';
import ensureExamples from './functions/ensureExamples.js';
import ensureField from './functions/ensureField.js';
import ensureUniqueErrorDescriptions from './functions/ensureUniqueErrorDescriptions.js';
import ensureErrorConsistency from './functions/ensureErrorConsistency.js';
import ensureTagConsistency from './functions/ensureTagConsistency.js';
import ensureOperationIdConsistency from './functions/ensureOperationIdConsistency.js';

export default {
'extends': [
[
Expand All @@ -32,7 +37,7 @@ export default {
'info-description': 'warn',
'info-license': 'warn',
'license-url': {
'description': 'License object must include "url" or "identifier"',
'description': 'License object must include "url" or "identifier".',
'message': '{{description}}; {{error}}',
'severity': 'warn',
'given': [
Expand All @@ -49,8 +54,8 @@ export default {
},
},
'description-for-every-attribute': {
'description': 'DEA - Descriptions for Every Attribute',
'message': 'For every attribute that is present in the OAS document, if a description is proposed as optional to complement that attribute, then yes it must be present; {{error}}',
'description': 'Every attribute in the OpenAPI document must have a description. Description fields that are marked as optional must be filled.',
'message': '{{description}}; {{error}}',
'severity': 'error',
'given': [
'$.info',
Expand All @@ -74,7 +79,7 @@ export default {
},
},
'examples-for-every-schema': {
'description': 'For every schema provided in the OAS document, at least one example must be present',
'description': 'For every schema provided in the OAS document, at least one example must be present.',
'message': '{{description}}; {{error}}',
'severity': 'warn',
'given': [
Expand All @@ -87,5 +92,44 @@ export default {
'function': ensureExamples,
},
},
'error-description-unique-for-method': {
'description': 'For each Error status-code defined, the description must be unique.',
'message': '{{description}}; {{error}}',
'severity': 'warn',
'given': '$.paths',
'then': {
'function': ensureUniqueErrorDescriptions,
},
},
'error-code-description-consistent': {
'description': 'For each error code, the description should be consistent across the API.',
'message': '{{description}}; {{error}}',
'severity': 'warn',
'given': '$',
'then': {
'function': ensureErrorConsistency,
},
},
'tag-name-case-consistent': {
'description': 'Tags should have consistent casing for the same tag.',
'message': '{{description}}; {{error}}',
'severity': 'warn',
'given': '$',
'then': {
'function': ensureTagConsistency,
},
},
'operationId-name-case-consistent': {
'description': 'OperationIds should have consistent casing.',
'message': '{{description}}; {{error}}',
'severity': 'warn',
'given': '$',
'then': {
'function': ensureOperationIdConsistency,
},
},
'oas3-valid-media-example': 'error',
'oas2-valid-media-example': 'error',
'oas3-valid-schema-example': 'error',
},
};
63 changes: 63 additions & 0 deletions functions/checkCasingConsistency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright 2022 Cisco Systems, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';

import commonAbbreviations from './commonAbbreviations.js';
import identifyCasingType from './identifyCasingType.js';

const removeCommonAbbreviations = (string) => {
const regex = new RegExp(`\\b(${ commonAbbreviations.join('|') })\\b`, 'g');

return string.replace(regex, '').trim();
};

const checkConsistency = (strings) => {
const cleanedStrings = strings.map(removeCommonAbbreviations).filter(Boolean);

const casingCounts = cleanedStrings.reduce((acc, str) => {
const type = identifyCasingType(str);

acc[type] = (acc[type] || 0) + 1;

return acc;
}, {});

const relevantTypes = Object.keys(casingCounts).filter((type) => type !== 'lowercase' && type !== 'unknown');

const isConsistent = relevantTypes.length <= 1;

if (!isConsistent) {
const foundTypes = relevantTypes.length > 0 ? relevantTypes : ['lowercase', 'unknown'].filter((type) => casingCounts[type]);
const typeDescriptions = foundTypes.map((type) => ({
'camelCase': 'camel',
'PascalCase': 'Pascal',
'kebab-case': 'kebab',
'snake_case': 'snake',
'unknown': 'unknown',
}[type] || type)).join(', ');

return {
message: `Inconsistent casing types detected. Found: ${ typeDescriptions }.`,
};
}

return null; // Consistency found or acceptable ambiguity with 'lowercase'
};

export default checkConsistency;
48 changes: 48 additions & 0 deletions functions/collectTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';
/**
* Copyright 2022 Cisco Systems, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

const isObject = (value) => value !== null && typeof value === 'object';

export const collectTags = (targetVal) => {
if (!isObject(targetVal)) {
return new Map();
}

const globalTags = targetVal.tags
? targetVal.tags.map((tagObj, index) => [tagObj.name, ['tags', index.toString()]])
: [];

const operationTags = Object.entries(targetVal.paths || {}).flatMap(([pathKey, operations]) => Object.entries(operations).flatMap(([method, operation]) => operation.tags
? operation.tags.map((tag, index) => [tag, ['paths', pathKey, method, 'tags', index.toString()]])
: [],
),
);

const allTags = [...globalTags, ...operationTags].reduce((acc, [tag, path]) => {
if (!acc.has(tag)) {
acc.set(tag, new Set());
}

acc.get(tag).add(path.join('.'));

return acc;
}, new Map());

return allTags;
};
4 changes: 2 additions & 2 deletions functions/completedSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* @param {string} targetVal The string to lint
* @param {Options} opts String requirements given by the linter ruleset
*/
module.exports = function (targetVal) {
export default function (targetVal) {
if (typeof targetVal !== 'object') {
return;
}
Expand All @@ -46,4 +46,4 @@ module.exports = function (targetVal) {
message: 'properties missing for object schema',
},
];
};
}
2 changes: 1 addition & 1 deletion functions/completedSchema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

const completedSchema = require('./completedSchema');
import completedSchema from './completedSchema';

describe('completedSchema', () => {

Expand Down
66 changes: 66 additions & 0 deletions functions/ensureErrorConsistency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright 2022 Cisco Systems, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

'use strict';

export default function ensureErrorConsistency(targetVal, opts, paths, otherValues) {
const errors = [];
const descriptionsMap = {};

// Traverse the API document to collect descriptions and their paths
Object.entries(targetVal.paths || {}).forEach(([path, methods]) => {
Object.entries(methods).forEach(([method, details]) => {
if (details.responses) {
Object.entries(details.responses).forEach(([statusCode, response]) => {
if (statusCode.startsWith('4') || statusCode.startsWith('5')) {
const description = response.description;

if (!descriptionsMap[statusCode]) {
descriptionsMap[statusCode] = {};
}

if (!descriptionsMap[statusCode][description]) {
descriptionsMap[statusCode][description] = [];
}

descriptionsMap[statusCode][description].push(`paths.${ path }.${ method }.responses.${ statusCode }`);
}
});
}
});
});

// Generate errors for inconsistencies
Object.entries(descriptionsMap).forEach(([statusCode, descriptions]) => {
if (Object.keys(descriptions).length > 1) { // More than one unique description for this status code
let allPaths = [];
const allDescriptions = Object.keys(descriptions);

Object.values(descriptions).forEach((paths) => {
allPaths = allPaths.concat(paths);
});

errors.push({
message: `Inconsistent descriptions for status code ${ statusCode }. Found '${ allDescriptions.join("', '") }'.`,
// path: allPaths[0].split('.')
});
}
});

return errors;
}
26 changes: 26 additions & 0 deletions functions/ensureErrorConsistency.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright 2022 Cisco Systems, Inc. and its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/


import ensureErrorConsistency from './ensureErrorConsistency';

describe('ensureErrorConsistency', () => {
it('should import the function without error', () => {
expect(ensureErrorConsistency).toBeDefined();
});
});
4 changes: 2 additions & 2 deletions functions/ensureExamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* @param {string} targetVal The string to lint
* @param {Options} opts String requirements given by the linter ruleset
*/
module.exports = function (targetVal) {
export default function (targetVal) {
if (typeof targetVal !== 'object') {
return;
}
Expand All @@ -45,7 +45,7 @@ module.exports = function (targetVal) {
message: 'example or examples is missing in the object',
},
];
};
}

function hasExample(target) {
if (target == null || target.examples || target.example) {
Expand Down
4 changes: 2 additions & 2 deletions functions/ensureField.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* @param {string} targetVal The string to lint
* @param {Options} opts String requirements given by the linter ruleset
*/
module.exports = function (targetVal, opts) {
export default function (targetVal, opts) {
if (typeof targetVal !== 'object') {
return;
}
Expand All @@ -45,4 +45,4 @@ module.exports = function (targetVal, opts) {
message: `${ opts.field } is missing in the object`,
},
];
};
}
2 changes: 1 addition & 1 deletion functions/ensureField.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

const ensureField = require('./ensureField');
import ensureField from './ensureField.js';

describe('ensureField', () => {

Expand Down
Loading

0 comments on commit 66b940c

Please sign in to comment.