ibm-openapi-validator
Version:
Configurable and extensible validator/linter for OpenAPI documents
176 lines (146 loc) • 5.54 kB
JavaScript
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/
const { getCategories, getCategoryCoefficient } = require('./categories');
const rubric = require('./rubric');
/**
* Uses the validator results to calculate categorized demerits for each rule
* violation, which are then used to compute categorized impact scores for the API.
*
* @param object result - the validator results
* @param Metrics metrics - the metric tracking class instance for scaling rule violations
* @param object logger - the root logger from the validator context
* @returns void
*/
function scoreResults(result, metrics, logger) {
// The `talliedResults` variable contains an object where each key is the name
// of a validator rule and each value is an object that contains a "count" field
// representing how many times the validator rule was violated by the given API.
const talliedResults = tally(result, logger);
// Initialize the categorized demerit sums.
const demeritSumsByCategory = {};
getCategories().forEach(c => (demeritSumsByCategory[c] = 0.0));
// Initialize the list of scoring data entries.
const scoringData = [];
Object.keys(talliedResults).forEach(rule => {
// Add the "func", which is a string representation of the formula
// used to compute the demerit value, as well as the demerit value
// itself, for each rule present in the validator results (each
// rule, not each instance of each rule).
const { count } = talliedResults[rule];
const data = {
rule,
count,
func: getFunc(rubric[rule], count),
demerits: computeCategorizedScores(rubric[rule], count, metrics),
};
// Count up category totals as we go.
for (const [category, demerit] of Object.entries(data.demerits)) {
demeritSumsByCategory[category] += demerit;
}
// After adding category totals, we can calculate the total impact for the given rule.
data.demerits.total = includeDecimals(
Object.values(data.demerits).reduce((sum, num) => sum + num, 0.0),
2
);
scoringData.push(data);
});
// Compute the categorized summary object.
const categorizedSummary = {};
getCategories().forEach(c => {
categorizedSummary[c] = computeOverallScore(demeritSumsByCategory[c]);
});
// Average the categorized scores to produce the overall score.
// They are already weighted through their coefficients.
categorizedSummary.overall = includeDecimals(
getCategories()
.map(c => categorizedSummary[c])
.reduce((total, score) => total + score) / getCategories().length,
0
);
return {
categorizedSummary,
scoringData,
};
}
// Calculate the numerical demerit severity for each rule.
function compute(rule, count, metrics) {
let denom = 1;
let coef = 1;
if (rule.denominator !== undefined) {
denom = metrics.get(rule.denominator);
}
if (rule.coefficient !== undefined) {
coef = rule.coefficient;
}
return (coef * count) / denom;
}
function computeCategorizedScores(rule, count, metrics) {
const baseScore = compute(rule, count, metrics);
// Initialize categories to zero to see "0" values in table.
const result = {};
// Base the categories on those defined for the given rule in the rubric.
rule.categories.forEach(c => {
result[c] = includeDecimals(baseScore * getCategoryCoefficient(c), 2);
});
return result;
}
// Show the user the formula used to calculate the demerit, as a string.
function getFunc(rule, count) {
let func = String(count);
if (rule.coefficient !== undefined) {
func += '×' + String(rule.coefficient);
}
if (rule.denominator !== undefined) {
func += '÷' + String(rule.denominator);
}
return func;
}
// Tally up the number of occurances for each rule present in the results.
// Returns an object where the keys are rule names and the values are objects
// with the total stored under the field name "count".
function tally(result, logger) {
const results = [...result.error.results, ...result.warning.results];
return results.reduce((tallies, result) => {
// Do not include rules that are not present in the rubric. This includes
// rules that have no actual API impact, are deprecated, etc.
if (!rubric[result.rule]) {
logger.debug(`Rule ${result.rule} is not present in the rubric`);
return tallies;
}
if (tallies[result.rule] === undefined) {
return { ...tallies, [result.rule]: { count: 1 } };
}
return {
...tallies,
[result.rule]: { count: tallies[result.rule].count + 1 },
};
}, {});
}
// Do some magic to compute the overall score :)
// From @hudlow: the idea is to use a function that's not TOO far from a
// linear slope for 0 < x < 100 but leveled out with an asymptote so
// that it couldn't go below zero. The thing that's kind of arbitrary
// is the "/40", which is just scaling the input subjectively — it's
// a number we will keep tuning alongside tuning the demerit coefficients.
function computeOverallScore(demeritSum) {
return includeDecimals(
100 - (100 * Math.atan(demeritSum / 40)) / Math.asin(1),
0
);
}
// In order to display numbers with meaningful precision,
// only use the first "number" of decimal places.
function includeDecimals(value, number) {
return parseFloat(value.toFixed(number));
}
module.exports = {
scoreResults,
// The following functions are only exported for testing purposes.
compute,
computeCategorizedScores,
getFunc,
tally,
computeOverallScore,
};