зеркало из 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"];
|
||||
|
||||
/* 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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче