132 lines
3.7 KiB
JavaScript
132 lines
3.7 KiB
JavaScript
const { is, descriptors } = require('./utils');
|
|
const { type, required, requires, format, defaultValue } = descriptors;
|
|
const {
|
|
InvalidSpecError,
|
|
MissingRequiredError,
|
|
TypeMismatchError,
|
|
FormatMismatchError,
|
|
VersionMalformedError,
|
|
VersionNotFoundError,
|
|
VersionMismatchError } = require('./utils').errors;
|
|
|
|
function isRequiresSatisfied(spec, config) {
|
|
try {
|
|
if (!spec.hasOwnProperty(requires) || spec[requires](config) === true) {
|
|
return true;
|
|
}
|
|
} catch (e) { }
|
|
return false;
|
|
}
|
|
|
|
function getConfigType(spec, config) {
|
|
const specTypes = is.array(spec[type]) ? spec[type] : [spec[type]];
|
|
for (let specType of specTypes) {
|
|
if (is[specType](config)) {
|
|
return specType;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function hasFormat(spec, config) {
|
|
if (!spec.hasOwnProperty(format)) {
|
|
return true;
|
|
}
|
|
return spec[format].test(config);
|
|
}
|
|
|
|
function validate(spec, config, parentConfig, path) {
|
|
if (!is.spec(spec)) {
|
|
throw new InvalidSpecError(spec, path);
|
|
}
|
|
if (!isRequiresSatisfied(spec, parentConfig)) {
|
|
return;
|
|
}
|
|
if (is.undefined(config) || is.null(config)) {
|
|
if (spec[required] === true) {
|
|
throw new MissingRequiredError(spec, path);
|
|
}
|
|
return;
|
|
}
|
|
const type = getConfigType(spec, config);
|
|
if (type === null) {
|
|
throw new TypeMismatchError(spec, path, config);
|
|
}
|
|
if (type === 'string') {
|
|
if (!hasFormat(spec, config)) {
|
|
throw new FormatMismatchError(spec, path, config);
|
|
}
|
|
} else if (type === 'array' && spec.hasOwnProperty('*')) {
|
|
config.forEach((child, i) => validate(spec['*'], child, config, path.concat(`[${i}]`)));
|
|
} else if (type === 'object') {
|
|
for (let key in spec) {
|
|
if (key === '*') {
|
|
Object.keys(config).forEach(k => validate(spec['*'], config[k], config, path.concat(k)));
|
|
} else {
|
|
validate(spec[key], config[key], config, path.concat(key));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatVersion(ver) {
|
|
const m = /^(\d)+\.(\d)+\.(\d)+(?:-([0-9A-Za-z-]+))*$/.exec(ver);
|
|
if (m === null) {
|
|
throw new VersionMalformedError(ver);
|
|
}
|
|
return {
|
|
major: m[1],
|
|
minor: m[2],
|
|
patch: m[3],
|
|
identifier: m.length > 4 ? m[4] : null
|
|
};
|
|
}
|
|
|
|
function compareVersion(ver1, ver2) {
|
|
for (let section of ['major', 'minor', 'patch']) {
|
|
if (ver1[section] !== ver2[section]) {
|
|
return Math.sign(ver1[section] - ver2[section]);
|
|
}
|
|
}
|
|
const id1 = ver1.hasOwnProperty('identifier') ? ver1.identifier : null;
|
|
const id2 = ver2.hasOwnProperty('identifier') ? ver2.identifier : null;
|
|
if (id1 === id2) {
|
|
return 0;
|
|
}
|
|
if (id1 === null) {
|
|
return 1;
|
|
}
|
|
if (id2 === null) {
|
|
return -1;
|
|
}
|
|
return id1.localeCompare(id2);
|
|
}
|
|
|
|
function isBreakingChange(base, ver) {
|
|
return base.major !== ver.major || base.minor !== ver.minor;
|
|
}
|
|
|
|
|
|
function checkVersion(spec, config) {
|
|
if (!config.hasOwnProperty('version')) {
|
|
throw new VersionNotFoundError();
|
|
}
|
|
const configVersion = formatVersion(config.version);
|
|
const specVersion = formatVersion(spec.version[defaultValue]);
|
|
if (isBreakingChange(specVersion, configVersion)) {
|
|
throw new VersionMismatchError(spec.version[defaultValue], config.version, compareVersion(specVersion, configVersion) > 0);
|
|
}
|
|
}
|
|
|
|
class ConfigValidator {
|
|
constructor(spec) {
|
|
this.spec = spec;
|
|
}
|
|
|
|
validate(config) {
|
|
checkVersion(this.spec, config);
|
|
validate(this.spec, config, null, []);
|
|
}
|
|
}
|
|
|
|
module.exports = ConfigValidator; |