codekie/openapi-examples-validator

View on GitHub
src/impl/v3/index.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * Contains validation-logic that is specific to V3 of the OpenAPI-spec
 */

const cloneDeep = require('lodash.clonedeep'),
    { ApplicationError, ErrorType } = require('../../application-error'),
    { setAllPropertiesRequired } = require('../service/all-properties-required'),
    { setNoAdditionalProperties } = require('../service/no-additional-properties');

// CONSTANTS

const RESPONSES = '$..responses..content[?(@property.match(/[\/+]json/))]';
const REQUEST = '$..requestBody.content[?(@property.match(/[\/+]json/))]';
const SINGLE_EXAMPLE = '.example';
const MANY_EXAMPLES = '.examples.*.value';

const PATH__EXAMPLE = `${RESPONSES}${SINGLE_EXAMPLE}`,
    PATH__EXAMPLES = `${RESPONSES}${MANY_EXAMPLES}`,
    PATH__EXAMPLE__PARAMETER = '$..parameters..example',
    PATH__EXAMPLES__PARAMETER = '$..parameters..examples.*.value',
    PATH__EXAMPLE__REQUEST_BODY = `${REQUEST}${SINGLE_EXAMPLE}`,
    PATH__EXAMPLES__REQUEST_BODY = `${REQUEST}${MANY_EXAMPLES}`,
    PROP__SCHEMA = 'schema',
    PROP__EXAMPLE = 'example',
    PROP__EXAMPLES = 'examples';

const ExampleType = {
    single: 'single',
    multi: 'multi'
};

// PUBLIC API

module.exports = {
    buildValidationMap,
    getJsonPathsToExamples,
    prepare
};

// IMPLEMENTATION DETAILS

/**
 * Get the JSONPaths to the examples
 * @returns {Array.<String>}    JSONPaths to the examples
 */
function getJsonPathsToExamples() {
    return [
        PATH__EXAMPLE,
        PATH__EXAMPLES,
        PATH__EXAMPLE__PARAMETER,
        PATH__EXAMPLES__PARAMETER,
        PATH__EXAMPLE__REQUEST_BODY,
        PATH__EXAMPLES__REQUEST_BODY
    ];
}

/**
 * Builds a map with the json-pointers to the response-schema as key and the json-pointers to the examples, as value.
 * The pointer of the schema is derived from the pointer to the example and doesn't necessarily mean
 * that the schema actually exists.
 * @param {Array.<String>}  pathsExamples   Paths to the examples
 * @returns {Object.<String, String>} Map with schema-pointers as key and example-pointers as value
 * @private
 */
function buildValidationMap(pathsExamples) {
    const exampleTypesOfSchemas = new Map();
    return pathsExamples.reduce((validationMap, pathExample) => {
        const { pathSchemaAsArray, exampleType } = _getSchemaPointerOfExample(pathExample),
            pathSchema = pathSchemaAsArray.join('/'),
            exampleTypeOfSchema = exampleTypesOfSchemas.get(pathSchema);
        if (exampleTypeOfSchema) {
            exampleTypeOfSchema !== exampleType && _throwMutuallyExclusiveError(pathSchemaAsArray);
        }
        exampleTypesOfSchemas.set(pathSchema, exampleType);
        validationMap[pathSchema] = (validationMap[pathSchema] || new Set())
            .add(pathExample);
        return validationMap;
    }, {});
}

/**
 * Pre-processes the OpenAPI-spec, for further use.
 * The passed spec won't be modified. If a modification happens, a modified copy will be returned.
 * @param {Object}  openapiSpec     The OpenAPI-spec as JSON-schema
 * @param {boolean} [noAdditionalProperties=false]  Don't allow properties that are not defined in the schema
 * @param {boolean} [allPropertiesRequired=false]   Make all properties required
 * @return {Object} The prepared OpenAPI-spec
 */
function prepare(openapiSpec, { noAdditionalProperties, allPropertiesRequired } = {}) {
    const openapiSpecCopy = cloneDeep(openapiSpec);
    noAdditionalProperties && setNoAdditionalProperties(openapiSpecCopy, getJsonPathsToExamples());
    allPropertiesRequired && setAllPropertiesRequired(openapiSpecCopy, getJsonPathsToExamples());
    return openapiSpecCopy;
}

/**
 * Gets a JSON-pointer to the corresponding response-schema, based on a JSON-pointer to an example.
 *
 * It is assumed that the JSON-pointer to the example is valid and existing.
 * @param {String}  examplePointer JSON-pointer to example
 * @returns {{
 *     exampleType: ExampleType,
 *     pathSchema: String
 * }} JSON-path to the corresponding response-schema
 * @private
 */
function _getSchemaPointerOfExample(examplePointer) {
    const pathSegs = examplePointer.split('/'),
        idxExample = pathSegs.lastIndexOf(PROP__EXAMPLE),
        /** @type ExampleType */
        exampleType = idxExample > -1
            ? ExampleType.single
            : ExampleType.multi,
        idxExamples = exampleType === ExampleType.single
            ? idxExample
            : pathSegs.lastIndexOf(PROP__EXAMPLES);
    pathSegs.splice(idxExamples, pathSegs.length - idxExamples, PROP__SCHEMA);
    return {
        exampleType,
        pathSchemaAsArray: pathSegs
    };
}


/**
 * Checks if only `example` or `examples` is set for the schema, as they are mutually exclusive by OpenAPI-spec.
 * @param {Array.<String>}  pathSchemaAsArray   JSON-path to the Schema, as JSON-path-array
 * @throws ApplicationError if both are set
 * @private
 */
function _throwMutuallyExclusiveError(pathSchemaAsArray) {
    const pathContextAsArray = pathSchemaAsArray.slice(0, pathSchemaAsArray.length - 1);    // Strip `schema` away
    throw ApplicationError.create({
        type: ErrorType.errorAndErrorsMutuallyExclusive,
        message: 'Properties "error" and "errors" are mutually exclusive',
        params: {
            pathContext: pathContextAsArray.join('/')
        }
    });
}