зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
f9a63c1481
Коммит
cd5a8640d9
|
@ -16,10 +16,12 @@ var {
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["Schemas"];
|
this.EXPORTED_SYMBOLS = ["Schemas"];
|
||||||
|
|
||||||
/* globals Schemas */
|
/* globals Schemas, URL */
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||||
|
|
||||||
|
Cu.importGlobalProperties(["URL"]);
|
||||||
|
|
||||||
function readJSON(uri) {
|
function readJSON(uri) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
NetUtil.asyncFetch({uri, loadUsingSystemPrincipal: true}, (inputStream, status) => {
|
||||||
|
@ -74,6 +76,45 @@ function getValueBaseType(value) {
|
||||||
return t;
|
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,
|
// Schema files contain namespaces, and each namespace contains types,
|
||||||
// properties, functions, and events. An Entry is a base class for
|
// properties, functions, and events. An Entry is a base class for
|
||||||
// types, properties, functions, and events.
|
// types, properties, functions, and events.
|
||||||
|
@ -136,9 +177,9 @@ class ChoiceType extends Type {
|
||||||
this.choices = choices;
|
this.choices = choices;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize(value) {
|
normalize(value, context) {
|
||||||
for (let choice of this.choices) {
|
for (let choice of this.choices) {
|
||||||
let r = choice.normalize(value);
|
let r = choice.normalize(value, context);
|
||||||
if (!r.error) {
|
if (!r.error) {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
@ -162,13 +203,13 @@ class RefType extends Type {
|
||||||
this.reference = reference;
|
this.reference = reference;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize(value) {
|
normalize(value, context) {
|
||||||
let ns = Schemas.namespaces.get(this.namespaceName);
|
let ns = Schemas.namespaces.get(this.namespaceName);
|
||||||
let type = ns.get(this.reference);
|
let type = ns.get(this.reference);
|
||||||
if (!type) {
|
if (!type) {
|
||||||
throw new Error(`Internal error: Type ${this.reference} not found`);
|
throw new Error(`Internal error: Type ${this.reference} not found`);
|
||||||
}
|
}
|
||||||
return type.normalize(value);
|
return type.normalize(value, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkBaseType(baseType) {
|
checkBaseType(baseType) {
|
||||||
|
@ -182,15 +223,16 @@ class RefType extends Type {
|
||||||
}
|
}
|
||||||
|
|
||||||
class StringType extends Type {
|
class StringType extends Type {
|
||||||
constructor(enumeration, minLength, maxLength, pattern) {
|
constructor(enumeration, minLength, maxLength, pattern, format) {
|
||||||
super();
|
super();
|
||||||
this.enumeration = enumeration;
|
this.enumeration = enumeration;
|
||||||
this.minLength = minLength;
|
this.minLength = minLength;
|
||||||
this.maxLength = maxLength;
|
this.maxLength = maxLength;
|
||||||
this.pattern = pattern;
|
this.pattern = pattern;
|
||||||
|
this.format = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize(value) {
|
normalize(value, context) {
|
||||||
let r = this.normalizeBase("string", value);
|
let r = this.normalizeBase("string", value);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
return r;
|
return r;
|
||||||
|
@ -214,6 +256,14 @@ class StringType extends Type {
|
||||||
return {error: `String ${JSON.stringify(value)} must match ${this.pattern}`};
|
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;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +295,7 @@ class ObjectType extends Type {
|
||||||
return baseType == "object";
|
return baseType == "object";
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize(value) {
|
normalize(value, context) {
|
||||||
let v = this.normalizeBase("object", value);
|
let v = this.normalizeBase("object", value);
|
||||||
if (v.error) {
|
if (v.error) {
|
||||||
return v;
|
return v;
|
||||||
|
@ -310,7 +360,7 @@ class ObjectType extends Type {
|
||||||
if (optional && (properties[prop] === null || properties[prop] === undefined)) {
|
if (optional && (properties[prop] === null || properties[prop] === undefined)) {
|
||||||
result[prop] = null;
|
result[prop] = null;
|
||||||
} else {
|
} else {
|
||||||
let r = type.normalize(properties[prop]);
|
let r = type.normalize(properties[prop], context);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
@ -347,7 +397,7 @@ class ObjectType extends Type {
|
||||||
if (this.additionalProperties) {
|
if (this.additionalProperties) {
|
||||||
for (let prop of remainingProps) {
|
for (let prop of remainingProps) {
|
||||||
let type = this.additionalProperties;
|
let type = this.additionalProperties;
|
||||||
let r = type.normalize(properties[prop]);
|
let r = type.normalize(properties[prop], context);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
@ -433,7 +483,7 @@ class ArrayType extends Type {
|
||||||
this.maxItems = maxItems;
|
this.maxItems = maxItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize(value) {
|
normalize(value, context) {
|
||||||
let v = this.normalizeBase("array", value);
|
let v = this.normalizeBase("array", value);
|
||||||
if (v.error) {
|
if (v.error) {
|
||||||
return v;
|
return v;
|
||||||
|
@ -441,7 +491,7 @@ class ArrayType extends Type {
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
for (let element of value) {
|
for (let element of value) {
|
||||||
element = this.itemType.normalize(element);
|
element = this.itemType.normalize(element, context);
|
||||||
if (element.error) {
|
if (element.error) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
@ -559,7 +609,7 @@ class CallEntry extends Entry {
|
||||||
throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
|
throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkParameters(args, global) {
|
checkParameters(args, global, context) {
|
||||||
let fixedArgs = [];
|
let fixedArgs = [];
|
||||||
|
|
||||||
// First we create a new array, fixedArgs, that is the same as
|
// First we create a new array, fixedArgs, that is the same as
|
||||||
|
@ -617,7 +667,7 @@ class CallEntry extends Entry {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
let parameter = this.parameters[parameterIndex];
|
let parameter = this.parameters[parameterIndex];
|
||||||
let r = parameter.type.normalize(arg);
|
let r = parameter.type.normalize(arg, context);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
this.throwError(global, `Type error for parameter ${parameter.name} (${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 stub = (...args) => {
|
||||||
let actuals = this.checkParameters(args, dest);
|
let actuals = this.checkParameters(args, dest, wrapperFuncs);
|
||||||
return wrapperFuncs.callFunction(this.namespaceName, name, actuals);
|
return wrapperFuncs.callFunction(this.namespaceName, name, actuals);
|
||||||
};
|
};
|
||||||
Cu.exportFunction(stub, dest, {defineAs: name});
|
Cu.exportFunction(stub, dest, {defineAs: name});
|
||||||
|
@ -743,7 +793,7 @@ this.Schemas = {
|
||||||
|
|
||||||
// Otherwise it's a normal type...
|
// Otherwise it's a normal type...
|
||||||
if (type.type == "string") {
|
if (type.type == "string") {
|
||||||
checkTypeProperties("enum", "minLength", "maxLength", "pattern");
|
checkTypeProperties("enum", "minLength", "maxLength", "pattern", "format");
|
||||||
|
|
||||||
let enumeration = type.enum || null;
|
let enumeration = type.enum || null;
|
||||||
if (enumeration) {
|
if (enumeration) {
|
||||||
|
@ -758,6 +808,7 @@ this.Schemas = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let pattern = null;
|
let pattern = null;
|
||||||
if (type.pattern) {
|
if (type.pattern) {
|
||||||
try {
|
try {
|
||||||
|
@ -766,10 +817,19 @@ this.Schemas = {
|
||||||
throw new Error(`Internal error: Invalid pattern ${JSON.stringify(type.pattern)}`);
|
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,
|
return new StringType(enumeration,
|
||||||
type.minLength || 0,
|
type.minLength || 0,
|
||||||
type.maxLength || Infinity,
|
type.maxLength || Infinity,
|
||||||
pattern);
|
pattern,
|
||||||
|
format);
|
||||||
} else if (type.type == "object") {
|
} else if (type.type == "object") {
|
||||||
let parseProperty = (type, extraProps = []) => {
|
let parseProperty = (type, extraProps = []) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -150,6 +150,22 @@ let json = [
|
||||||
{name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"},
|
{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: [
|
events: [
|
||||||
|
@ -182,6 +198,15 @@ function verify(...args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let wrapper = {
|
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) {
|
callFunction(ns, name, args) {
|
||||||
tally("call", ns, name, args);
|
tally("call", ns, name, args);
|
||||||
},
|
},
|
||||||
|
@ -353,6 +378,31 @@ add_task(function* () {
|
||||||
/String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
|
/String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
|
||||||
"should throw for non-match");
|
"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);
|
root.testing.onFoo.addListener(f);
|
||||||
do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"]));
|
do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["addListener", "testing", "onFoo"]));
|
||||||
do_check_eq(tallied[3][0], f);
|
do_check_eq(tallied[3][0], f);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче