UNPKG

ibm-openapi-validator

Version:

Configurable and extensible validator/linter for OpenAPI documents

273 lines (233 loc) 7.42 kB
/** * Copyright 2017 - 2024 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ const path = require('path'); const { getFileExtension, supportedFileExtension, } = require('./file-extension-validator'); const { LoggerFactory } = require('@ibm-cloud/openapi-ruleset/src/utils'); const validateSchema = require('./validate-schema'); const createCLIOptions = require('./cli-options'); const readYaml = require('./read-yaml'); // Lazy initializer for the logger. let logger; function getLogger() { if (!logger) { logger = LoggerFactory.getInstance().getLogger('root'); } return logger; } // Our default config object. const defaultConfig = { colorizeOutput: true, errorsOnly: false, files: [ // 'my-api.yaml' ], limits: { warnings: -1, }, ignoreFiles: [ // '/full/path/to/file/ignoreMe.json' ], logLevels: { // 'root': 'info' // 'ibm-schema-description-exists': 'debug' }, outputFormat: 'text', ruleset: null, summaryOnly: false, produceImpactScore: false, markdownReport: false, }; const supportedConfigFileTypes = ['json', 'yaml', 'yml', 'js']; /** * Returns the default validator configuration. * @returns default config */ function getDefaultConfig() { // Make a copy that can be modified by the caller. return JSON.parse(JSON.stringify(defaultConfig)); } /** * Loads the specified file as a validator config object. * @param {*} filename the name of the file to load * @returns the loaded config object or the default config in case of an error */ async function loadConfig(filename) { let userConfig = null; try { // Get fully-qualified filename. const configFile = path.resolve(filename); if (supportedFileExtension(configFile, supportedConfigFileTypes)) { const extension = getFileExtension(configFile); try { switch (extension) { case 'json': case 'js': { userConfig = require(configFile); break; } case 'yaml': case 'yml': { userConfig = await readYaml(configFile); break; } } } catch (err) { throw new Error(`Unable to load config file '${configFile}': ${err}`); } } else { throw new Error( `Unsupported config file type: '${configFile}'; supported extensions are: ${supportedConfigFileTypes.join( ', ' )}` ); } // If we loaded the user's config object above, then we need to validate it // against our schema. if (userConfig) { const schema = await getConfigFileSchema(); const results = validateSchema(userConfig, schema); if (results.length) { let msg = `Invalid configuration file '${configFile}' detected:`; results.forEach(result => { msg += '\n ' + result; }); throw new Error(msg); } } } catch (err) { // Rather than propagate any exceptions up to the caller, we'll just log the error messages, // and then return the default configuration object. getLogger().error(err.message); getLogger().error( `The validator will use a default config due to the previous error(s).` ); userConfig = null; } // Return a fully-populated config object by overlaying the user config on top // of our default configuration. const configObj = Object.assign( {}, defaultConfig, userConfig ? userConfig : {} ); return configObj; } /** * Process the specified command-line arguments ('args') to produce * a validator context. * @param {*} args an array of command-line arguments * @param {*} cliParseOptions options for parsing the CLI args * @returns an object with fields "context" and "command" */ async function processArgs(args, cliParseOptions) { // 'command' will be a "Command" instance that describes our CLI options. const command = createCLIOptions(); command.parse(args, cliParseOptions); // Set default loglevel of the root logger to be 'warn'. // The user can change this via options. const loggerFactory = LoggerFactory.getInstance(); loggerFactory.addLoggerSetting('root', 'warn'); logger = loggerFactory.getLogger('root'); // "context" will serve as a container for the validator's configuration // and state information. const context = { logger, }; // Retrieve the options that were set by the user on the command-line. const opts = command.opts(); // Load the user's config file (if -c/--config specified on command-line), // or use the default config. const configObj = opts.config ? await loadConfig(opts.config) : getDefaultConfig(); // Save the config object in our context. context.config = configObj; // Command-line options should take precedence over options contained in the config file, // so overlay CLI options onto our config object. // Filenames specified on the command-line will be in the "args" field. const cliFiles = command.args || []; if (cliFiles.length) { configObj.files = cliFiles; } // Process each loglevel entry supplied on the command line. // During this first pass, we just want to parse the CLI -l/--logLevel options // and build an object ("cliLogLevels") that maps logger name -> logging level // (e.g. {'root': 'debug'}). const cliLogLevels = {}; const logLevels = opts.logLevel || []; for (const entry of logLevels) { let [loggerName, logLevel] = entry.split('='); // No logLevel was parsed (e.g. -l info); assume root logger. if (!logLevel) { logLevel = loggerName; loggerName = 'root'; } cliLogLevels[loggerName] = logLevel; } // If we in fact received log level options via the CLI, then overlay them // onto the config object. if (Object.keys(cliLogLevels).length) { configObj.logLevels = cliLogLevels; } // Now we just need to process the log level settings within the config object, // and update the LoggerFactory instance. for (const [loggerName, logLevel] of Object.entries(configObj.logLevels)) { loggerFactory.addLoggerSetting(loggerName, logLevel); } // // Retrieve the rest of the command-line options and apply them to "configObj". // if ('colors' in opts) { configObj.colorizeOutput = !!opts.colors; } if ('errorsOnly' in opts) { configObj.errorsOnly = !!opts.errorsOnly; } const ignoreFiles = opts.ignore || []; if (ignoreFiles.length) { configObj.ignoreFiles = ignoreFiles; } if ('json' in opts) { configObj.outputFormat = 'json'; } if ('ruleset' in opts) { configObj.ruleset = opts.ruleset; } if ('summaryOnly' in opts) { configObj.summaryOnly = !!opts.summaryOnly; } if ('warningsLimit' in opts) { configObj.limits.warnings = opts.warningsLimit; } if ('impactScore' in opts) { configObj.produceImpactScore = true; } if ('markdownReport' in opts) { configObj.markdownReport = true; } return { context, command }; } /** * Loads the configuration file schema from 'config-file.yaml'. * @returns the config file schema */ let configFileSchema; async function getConfigFileSchema() { if (!configFileSchema) { configFileSchema = await readYaml( path.join(__dirname, '../../schemas/config-file.yaml') ); } return configFileSchema; } module.exports = { getConfigFileSchema, getDefaultConfig, loadConfig, processArgs, };