gecko-dev/toolkit/components/utils/JsonSchemaValidator.jsm

195 строки
5.4 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* This file implements a not-quite standard JSON schema validator. It differs
* from the spec in a few ways:
*
* - the spec doesn't allow custom types to be defined, but this validator
* defines "URL", "URLorEmpty", "origin" etc.
* - Strings are automatically converted to nsIURIs for the appropriate types.
* - It doesn't support "pattern" when matching strings.
* - The boolean type accepts (and casts) 0 and 1 as valid values.
*/
"use strict";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "log", () => {
let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm", {});
return new ConsoleAPI({
prefix: "JsonSchemaValidator.jsm",
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
// messages during development. See LOG_LEVELS in Console.jsm for details.
maxLogLevel: "error",
});
});
var EXPORTED_SYMBOLS = ["JsonSchemaValidator"];
var JsonSchemaValidator = {
validateAndParseParameters(param, properties) {
return validateAndParseParamRecursive(param, properties);
}
};
function validateAndParseParamRecursive(param, properties) {
if (properties.enum) {
if (properties.enum.includes(param)) {
return [true, param];
}
return [false, null];
}
log.debug(`checking @${param}@ for type ${properties.type}`);
if (Array.isArray(properties.type)) {
log.debug("type is an array");
// For an array of types, the value is valid if it matches any of the listed
// types. To check this, make versions of the object definition that include
// only one type at a time, and check the value against each one.
for (const type of properties.type) {
let typeProperties = Object.assign({}, properties, {type});
log.debug(`checking subtype ${type}`);
let [valid, data] = validateAndParseParamRecursive(param, typeProperties);
if (valid) {
return [true, data];
}
}
// None of the types matched
return [false, null];
}
switch (properties.type) {
case "boolean":
case "number":
case "integer":
case "string":
case "URL":
case "URLorEmpty":
case "origin":
return validateAndParseSimpleParam(param, properties.type);
case "array":
if (!Array.isArray(param)) {
log.error("Array expected but not received");
return [false, null];
}
let parsedArray = [];
for (let item of param) {
log.debug(`in array, checking @${item}@ for type ${properties.items.type}`);
let [valid, parsedValue] = validateAndParseParamRecursive(item, properties.items);
if (!valid) {
return [false, null];
}
parsedArray.push(parsedValue);
}
return [true, parsedArray];
case "object": {
if (typeof(param) != "object") {
log.error("Object expected but not received");
return [false, null];
}
let parsedObj = {};
for (let property of Object.keys(properties.properties)) {
log.debug(`in object, checking\n property: ${property}\n value: ${param[property]}\n expected type: ${properties.properties[property].type}`);
if (!param.hasOwnProperty(property)) {
if (properties.required && properties.required.includes(property)) {
log.error(`Object is missing required property ${property}`);
return [false, null];
}
continue;
}
let [valid, parsedValue] = validateAndParseParamRecursive(param[property], properties.properties[property]);
if (!valid) {
return [false, null];
}
parsedObj[property] = parsedValue;
}
return [true, parsedObj];
}
}
return [false, null];
}
function validateAndParseSimpleParam(param, type) {
let valid = false;
let parsedParam = param;
switch (type) {
case "boolean":
if (typeof(param) == "boolean") {
valid = true;
} else if (typeof(param) == "number" &&
(param == 0 || param == 1)) {
valid = true;
parsedParam = !!param;
}
break;
case "number":
case "string":
valid = (typeof(param) == type);
break;
// integer is an alias to "number" that some JSON schema tools use
case "integer":
valid = (typeof(param) == "number");
break;
case "origin":
if (typeof(param) != "string") {
break;
}
try {
parsedParam = Services.io.newURI(param);
let pathQueryRef = parsedParam.pathQueryRef;
// Make sure that "origin" types won't accept full URLs.
if (pathQueryRef != "/" && pathQueryRef != "") {
valid = false;
} else {
valid = true;
}
} catch (ex) {
valid = false;
}
break;
case "URL":
case "URLorEmpty":
if (typeof(param) != "string") {
break;
}
if (type == "URLorEmpty" && param === "") {
valid = true;
break;
}
try {
parsedParam = Services.io.newURI(param);
valid = true;
} catch (ex) {
valid = false;
}
break;
}
return [valid, parsedParam];
}