Skip to content

Commit

Permalink
chore: simplify logic by looking at parental examples
Browse files Browse the repository at this point in the history
Signed-off-by: Dustin Popp <[email protected]>
  • Loading branch information
dpopp07 committed Dec 31, 2024
1 parent adeaf1e commit 2ab9d95
Showing 1 changed file with 57 additions and 148 deletions.
205 changes: 57 additions & 148 deletions packages/ruleset/src/functions/use-date-based-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,49 @@ function checkForDateBasedFormat(s, p, apidef) {
const schemaExamples = getExamplesForSchema(schema);
logger.debug(`${ruleId}: ${schemaExamples.length} examples found`);

// Examples have already been stored on the parent - as we go through the
// schemas, check for the presence of an example value for the current
// property within the parent's example.
const parentalExamples =
// If the logical path is empty, there are no parents, but there may be
// indirect examples to add in the ":" branch.
logicalPath.length > 0
? // Look at the example values for the logical parent schema, if any.
// For successive, nested properties, this will end up propagating
// examples through the recursive descent so that example values
// separated from a property by multiple degrees of nesting will
// still be preserved.
examples[logicalPath.slice(0, -1).join('.')]
.map(e => {
// Check the parental example values for
// the presence of the current property.
const prop = logicalPath.at(-1);

// Check for sentinel indicating an array.
if (prop === '[]' && Array.isArray(e)) {
return e[0];
}

// Check for sentinel indicating a dictionary.
if (prop === '*' && isObject(e)) {
return Object.values(e)[0];
}

// Standard model path.
return e[prop];
})

// We are not guaranteed to find a value - filter
// out any values that are not defined.
.filter(e => e !== undefined)
: // Add indirect examples to the map - note that they will necessarily
// be indexed with the empty string key, like primary schemas.
indirectExamples;

logger.debug(
`${ruleId}: ${parentalExamples.length} examples found in logical parent`
);

// Index the examples with the stringified logical path.
// Note that the unconditional assignment is intentional - there may be
// existing entries for this same logical path (e.g. for the same
Expand All @@ -99,15 +142,9 @@ function checkForDateBasedFormat(s, p, apidef) {
// order the schemas are checked in (the logic may look at more examples
// for one instance of a property than another, arbitrarily) and we don't
// make a guarantee that order will be stable in `validateNestedSchemas`.
examples[logicalPath.join('.')] = schemaExamples;

return performValidation(
schema,
path,
logicalPath,
examples,
indirectExamples
);
examples[logicalPath.join('.')] = [...schemaExamples, ...parentalExamples];

return performValidation(schema, path, logicalPath, examples);
});
}

Expand All @@ -117,13 +154,7 @@ function checkForDateBasedFormat(s, p, apidef) {
// this function implements the checks: 1) see if the name of a property
// indicates that it is a date-based schema and 2) see if the example value for
// a schema indicates that it is a date-based schema.
function performValidation(
schema,
path,
logicalPath,
examples,
indirectExamples
) {
function performValidation(schema, path, logicalPath, examples) {
// If this is already a date or date-time schema, no need to check if it should be.
if (isDateSchema(schema) || isDateTimeSchema(schema)) {
logger.debug(
Expand Down Expand Up @@ -166,15 +197,12 @@ function performValidation(

// Check example values for string schemas.
if (isStringSchema(schema)) {
// Search the list of examples for an example object that contains a value
// for this property.
const exampleValue = findExample(logicalPath, examples, indirectExamples);
// Use the first value example value found for this logical path.
const exampleValue = examples[logicalPath.join('.')].find(
e => e !== undefined
);
if (exampleValue !== undefined) {
logger.debug(
`${ruleId}: example value found for string schema at location ${path.join(
'.'
)}: ${exampleValue}`
);
logger.debug(`${ruleId}: example value found: ${exampleValue}`);

if (isDateBasedValue(exampleValue)) {
return [
Expand All @@ -185,138 +213,19 @@ function performValidation(
},
];
}
} else {
logger.debug(`${ruleId}: no example value found`);
}
}

return [];
}

// This function checks all of the examples we've gathered while processing
// schemas to check if once of them defines a value for the specific property
// or string schema that we are looking at. It returns the first value found.
function findExample(logicalPath, examples, indirectExamples) {
// First, look at the indirect examples (those included through OpenAPI
// fields) as they are given priority by OpenAPI over native schema examples.
if (indirectExamples.length) {
logger.debug(`${ruleId}: Looking for value within indirect examples.`);
const value = findValueInExamples(
indirectExamples,
logicalPath,
// Indirect examples, like primary schemas, need an empty path array.
[]
);

// If we find a value, go ahead and return.
if (value !== undefined) {
logger.debug(
`${ruleId}: example value found in indirect example: ${value}`
);

return value;
}
}

// Look for examples at different paths up the logical path.
const examplePath = [...logicalPath];

// Look at each level of hierarchy of the example
// path and check for an example value.
do {
logger.debug(
`${ruleId}: Looking at example at logical path ${logicalPathForLogger(
examplePath
)}`
);
const value = findValueInExamples(
examples[examplePath.join('.')],
logicalPath,
examplePath
);

// If we find a value, go ahead and return.
if (value !== undefined) {
logger.debug(
`${ruleId}: example value found in schema example at logical path: ${logicalPathForLogger(
examplePath
)}: ${value}`
);

return value;
}
} while (examplePath.pop() !== undefined); // Pop the last element to keep looking up the path.

// This will return `undefined` if we never find a value;
logger.debug(
`${ruleId}: no example value found for schema at logical path: ${logicalPathForLogger(
logicalPath
)}`
);
}

function findValueInExamples(examples, logicalPath, examplePath) {
if (examples && examples.length) {
// First thing is to find the relevant segment of the logical path relative
// to the example path, which should be the first element where they differ.
const index = logicalPath.findIndex((prop, i) => prop !== examplePath[i]);

// This means that the logical paths are the same, i.e. the examples are
// primitives defined directly on the schema.
if (index === -1) {
// Return the first defined example.
return examples.find(e => e !== undefined);
}

const relativeLogicalPath = logicalPath.slice(index);
for (const example of examples) {
const value = getObjectValueAtPath(example, relativeLogicalPath);

// If we find a value, go ahead and return it.
if (value !== undefined) {
return value;
}
}
}

return undefined;
}

// This function takes an object, as well as a path to a specific value, and
// recursively parses the object looking for the value at that path. If it
// finds one, the value will be returned. If not, the function will return
// `undefined`.
// parses the object, looking for the value at that path. If it finds one,
// the value will be returned. If not, the function will return `undefined`.
function getObjectValueAtPath(obj, pathToValue) {
// If obj is undefined, there is nothing to process.
if (obj === undefined) {
return;
}

// If we've exhausted the whole path, we've found the desired value!
if (!pathToValue.length) {
return obj;
}

// Make a modifiable copy.
pathToValue = [...pathToValue];

const p = pathToValue.shift();

// Check for sentinel indicating an array.
if (p === '[]' && Array.isArray(obj) && obj.length) {
return getObjectValueAtPath(obj[0], pathToValue);
}

// Check for sentinel indicating a dictionary.
if (p === '*' && isObject(obj) && Object.values(obj).length) {
return getObjectValueAtPath(Object.values(obj)[0], pathToValue);
}

// Standard model path.
if (obj[p]) {
return getObjectValueAtPath(obj[p], pathToValue);
}

// Return undefined if we don't find anything.
return;
return pathToValue.reduce((value, field) => value[field], obj);
}

// "Indirect" examples are those coming from request bodies, response bodies, and parameters.
Expand Down

0 comments on commit 2ab9d95

Please sign in to comment.