Skip to content

Commit

Permalink
New: Add config validator (refs #2179)
Browse files Browse the repository at this point in the history
  • Loading branch information
btmills committed May 13, 2015
1 parent f20fd41 commit 4b1ec7e
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 0 deletions.
99 changes: 99 additions & 0 deletions lib/config-validator.js
@@ -0,0 +1,99 @@
/**
* @fileoverview Validates configs.
* @author Brandon Mills
* @copyright 2015 Brandon Mills
*/

"use strict";

var rules = require("./rules"),
schemaValidator = require("is-my-json-valid");

var optionsValidators = Object.create(null);

/**
* Converts a rule's exported, abbreviated schema into a full schema.
* @param {object} options Exported schema from a rule.
* @returns {object} Full schema ready for validation.
*/
function makeRuleOptionsSchema(options) {

// If no schema, only validate warning level, and permit anything after
if (!options) {
return {
"type": "array",
"items": [
{
"enum": [0, 1, 2]
}
],
"minItems": 1
};
}

// Given a tuple of schemas, insert warning level at the beginning
if (Array.isArray(options)) {
return {
"type": "array",
"items": [
{
"enum": [0, 1, 2]
}
].concat(options),
"minItems": 1,
"maxItems": options.length + 1
};
}

// Given a full schema, leave it alone
return options;
}

/**
* Gets an options schema for a rule.
* @param {string} id The rule's unique name.
* @returns {object} vJSON Schema for the rule's options.
*/
function getRuleOptionsSchema(id) {
var rule = rules.get(id);
return makeRuleOptionsSchema(rule && rule.schema);
}

/**
* Validates a rule's options against its schema.
* @param {string} id The rule's unique name.
* @param {object} options The given options for the rule.
* @param {string} source The name of the configuration source.
* @returns {void}
*/
module.exports.validateRuleOptions = function (id, options, source) {
var validate = optionsValidators[id],
message;

if (!validate) {
validate = schemaValidator(getRuleOptionsSchema(id), { verbose: true });
optionsValidators[id] = validate;
}

if (typeof options === "number") {
options = [options];
}

validate(options);

if (validate.errors) {
message = [
source, ":\n",
"\tConfiguration for rule \"", id, "\" is invalid:\n"
];
validate.errors.forEach(function (error) {
message.push(
"\tValue \"", error.value, "\" ", error.message, ".\n"
);
});

throw new Error(message.join(""));
}
};

module.exports.getRuleOptionsSchema = optionsValidators;
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -45,6 +45,7 @@
"estraverse-fb": "^1.3.1",
"globals": "^6.1.0",
"inquirer": "^0.8.2",
"is-my-json-valid": "^2.10.0",
"js-yaml": "^3.2.5",
"minimatch": "^2.0.1",
"mkdirp": "^0.5.0",
Expand Down
78 changes: 78 additions & 0 deletions tests/lib/config-validator.js
@@ -0,0 +1,78 @@
/**
* @fileoverview Tests for config validator.
* @author Brandon Mills
* @copyright 2015 Brandon Mills
*/

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

var assert = require("chai").assert,
eslint = require("../../lib/eslint"),
validator = require("../../lib/config-validator");

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

function mockRule(context) {
return {
"Program": function(node) {
context.report(node, "Expected a validation error.");
}
};
}

mockRule.schema = [
{
"enum": ["single", "double", "backtick"]
},
{
"enum": ["avoid-escape"]
}
];

describe("Validator", function() {

describe("validateRuleOptions", function() {

beforeEach(function() {
eslint.defineRule("mock-rule", mockRule);
});

it("should throw for incorrect warning level", function() {
var fn = validator.validateRuleOptions.bind(null, "mock-rule", 3, "tests");

assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"3\" must be an enum value.\n");
});

it("should only check warning level for nonexistent rules", function() {
var fn = validator.validateRuleOptions.bind(null, "non-existent-rule", [3, "foobar"], "tests");

assert.throws(fn, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tValue \"3\" must be an enum value.\n");
});

it("should only check warning level for plugin rules", function() {
var fn = validator.validateRuleOptions.bind(null, "plugin/rule", 3, "tests");

assert.throws(fn, "tests:\n\tConfiguration for rule \"plugin/rule\" is invalid:\n\tValue \"3\" must be an enum value.\n");
});

it("should throw for incorrect configuration values", function() {
var fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "doulbe", "avoidEscape"], "tests");

assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"doulbe\" must be an enum value.\n\tValue \"avoidEscape\" must be an enum value.\n");
});

it("should throw for too many configuration values", function() {
var fn = validator.validateRuleOptions.bind(null, "mock-rule", [2, "single", "avoid-escape", "extra"], "tests");

assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"2,single,avoid-escape,extra\" has more items than allowed.\n");
});

});

});

0 comments on commit 4b1ec7e

Please sign in to comment.