Bug 1225715: Part 2 - Add string format support to schemas. r=billm

--HG--
extra : commitid : ArSuR3hloWW
extra : rebase_source : 64f075729beee1a0b3e411c7f3c7084839e793df
This commit is contained in:
Kris Maglione 2016-01-29 19:48:57 -08:00
Родитель f9a63c1481
Коммит cd5a8640d9
2 изменённых файлов: 127 добавлений и 17 удалений

Просмотреть файл

@ -16,10 +16,12 @@ var {
this.EXPORTED_SYMBOLS = ["Schemas"];
/* globals Schemas */
/* globals Schemas, URL */
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.importGlobalProperties(["URL"]);
function readJSON(uri) {
return new Promise((resolve, reject) => {
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
@ -74,6 +76,45 @@ function getValueBaseType(value) {
return t;
}
/**
* The methods in this singleton represent the "format" specifier for
* JSON Schema string types.
*
* Each method either returns a normalized version of the original
* value, or throws an error if the value is not valid for the given
* format.
*/
const FORMATS = {
url(string, context) {
let url = new URL(string).href;
context.checkLoadURL(url);
return url;
},
relativeUrl(string, context) {
let url = new URL(string, context.url).href;
context.checkLoadURL(url);
return url;
},
strictRelativeUrl(string, context) {
// Do not accept a string which resolves as an absolute URL, or any
// protocol-relative URL.
if (!string.startsWith("//")) {
try {
new URL(string);
} catch (e) {
return FORMATS.relativeUrl(string, context);
}
}
throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`);
},
};
// Schema files contain namespaces, and each namespace contains types,
// properties, functions, and events. An Entry is a base class for
// types, properties, functions, and events.
@ -136,9 +177,9 @@ class ChoiceType extends Type {
this.choices = choices;
}
normalize(value) {
normalize(value, context) {
for (let choice of this.choices) {
let r = choice.normalize(value);
let r = choice.normalize(value, context);
if (!r.error) {
return r;
}
@ -162,13 +203,13 @@ class RefType extends Type {
this.reference = reference;
}
normalize(value) {
normalize(value, context) {
let ns = Schemas.namespaces.get(this.namespaceName);
let type = ns.get(this.reference);
if (!type) {
throw new Error(`Internal error: Type ${this.reference} not found`);
}
return type.normalize(value);
return type.normalize(value, context);
}
checkBaseType(baseType) {
@ -182,15 +223,16 @@ class RefType extends Type {
}
class StringType extends Type {
constructor(enumeration, minLength, maxLength, pattern) {
constructor(enumeration, minLength, maxLength, pattern, format) {
super();
this.enumeration = enumeration;
this.minLength = minLength;
this.maxLength = maxLength;
this.pattern = pattern;
this.format = format;
}
normalize(value) {
normalize(value, context) {
let r = this.normalizeBase("string", value);
if (r.error) {
return r;
@ -214,6 +256,14 @@ class StringType extends Type {
return {error: `String ${JSON.stringify(value)} must match ${this.pattern}`};
}
if (this.format) {
try {
r.value = this.format(r.value, context);
} catch (e) {
return {error: String(e)};
}
}
return r;
}
@ -245,7 +295,7 @@ class ObjectType extends Type {
return baseType == "object";
}
normalize(value) {
normalize(value, context) {
let v = this.normalizeBase("object", value);
if (v.error) {
return v;
@ -310,7 +360,7 @@ class ObjectType extends Type {
if (optional && (properties[prop] === null || properties[prop] === undefined)) {
result[prop] = null;
} else {
let r = type.normalize(properties[prop]);
let r = type.normalize(properties[prop], context);
if (r.error) {
return r;
}
@ -347,7 +397,7 @@ class ObjectType extends Type {
if (this.additionalProperties) {
for (let prop of remainingProps) {
let type = this.additionalProperties;
let r = type.normalize(properties[prop]);
let r = type.normalize(properties[prop], context);
if (r.error) {
return r;
}
@ -433,7 +483,7 @@ class ArrayType extends Type {
this.maxItems = maxItems;
}
normalize(value) {
normalize(value, context) {
let v = this.normalizeBase("array", value);
if (v.error) {
return v;
@ -441,7 +491,7 @@ class ArrayType extends Type {
let result = [];
for (let element of value) {
element = this.itemType.normalize(element);
element = this.itemType.normalize(element, context);
if (element.error) {
return element;
}
@ -559,7 +609,7 @@ class CallEntry extends Entry {
throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
}
checkParameters(args, global) {
checkParameters(args, global, context) {
let fixedArgs = [];
// First we create a new array, fixedArgs, that is the same as
@ -617,7 +667,7 @@ class CallEntry extends Entry {
return null;
} else {
let parameter = this.parameters[parameterIndex];
let r = parameter.type.normalize(arg);
let r = parameter.type.normalize(arg, context);
if (r.error) {
this.throwError(global, `Type error for parameter ${parameter.name} (${r.error})`);
}
@ -642,7 +692,7 @@ class FunctionEntry extends CallEntry {
}
let stub = (...args) => {
let actuals = this.checkParameters(args, dest);
let actuals = this.checkParameters(args, dest, wrapperFuncs);
return wrapperFuncs.callFunction(this.namespaceName, name, actuals);
};
Cu.exportFunction(stub, dest, {defineAs: name});
@ -743,7 +793,7 @@ this.Schemas = {
// Otherwise it's a normal type...
if (type.type == "string") {
checkTypeProperties("enum", "minLength", "maxLength", "pattern");
checkTypeProperties("enum", "minLength", "maxLength", "pattern", "format");
let enumeration = type.enum || null;
if (enumeration) {
@ -758,6 +808,7 @@ this.Schemas = {
}
});
}
let pattern = null;
if (type.pattern) {
try {
@ -766,10 +817,19 @@ this.Schemas = {
throw new Error(`Internal error: Invalid pattern ${JSON.stringify(type.pattern)}`);
}
}
let format = null;
if (type.format) {
if (!(type.format in FORMATS)) {
throw new Error(`Internal error: Invalid string format ${type.format}`);
}
format = FORMATS[type.format];
}
return new StringType(enumeration,
type.minLength || 0,
type.maxLength || Infinity,
pattern);
pattern,
format);
} else if (type.type == "object") {
let parseProperty = (type, extraProps = []) => {
return {

Просмотреть файл

@ -150,6 +150,22 @@ let json = [
{name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"},
],
},
{
name: "format",
type: "function",
parameters: [
{
name: "arg",
type: "object",
properties: {
url: {type: "string", "format": "url", "optional": true},
relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true},
},
},
],
},
],
events: [
@ -182,6 +198,15 @@ function verify(...args) {
}
let wrapper = {
url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
checkLoadURL(url) {
if (url.startsWith("chrome:")) {
throw new Error("Access denied");
}
return url;
},
callFunction(ns, name, args) {
tally("call", ns, name, args);
},
@ -353,6 +378,31 @@ add_task(function* () {
/String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
"should throw for non-match");
root.testing.format({url: "http://foo/bar",
relativeUrl: "http://foo/bar"});
verify("call", "testing", "format", [{url: "http://foo/bar",
relativeUrl: "http://foo/bar",
strictRelativeUrl: null}]);
tallied = null;
root.testing.format({relativeUrl: "foo.html", strictRelativeUrl: "foo.html"});
verify("call", "testing", "format", [{url: null,
relativeUrl: `${wrapper.url}foo.html`,
strictRelativeUrl: `${wrapper.url}foo.html`}]);
tallied = null;
for (let format of ["url", "relativeUrl"]) {
Assert.throws(() => root.testing.format({[format]: "chrome://foo/content/"}),
/Access denied/,
"should throw for access denied");
}
for (let url of ["//foo.html", "http://foo/bar.html"]) {
Assert.throws(() => root.testing.format({strictRelativeUrl: url}),
/must be a relative URL/,
"should throw for non-relative URL");
}
root.testing.onFoo.addListener(f);
do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"]));
do_check_eq(tallied[3][0], f);