Bug 1225715: Part 1 - Add support for patterned strings and properties in schemas. r?billm
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -40,16 +40,27 @@ function readJSON(uri) {
resolve(JSON.parse(text));
} catch (e) {
reject(e);
}
});
});
}
+// Parses a regular expression, with support for the Python extended
+// syntax that allows setting flags by including the string (?im)
+function parsePattern(pattern) {
+ let flags = "";
+ let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
+ if (match) {
+ [, flags, pattern] = match;
+ }
+ return new RegExp(pattern, flags);
+}
+
function getValueBaseType(value) {
let t = typeof(value);
if (t == "object") {
if (value === null) {
return "null";
} else if (Array.isArray(value)) {
return "array";
} else if (Object.prototype.toString.call(value) == "[object ArrayBuffer]") {
@@ -166,21 +177,22 @@ class RefType extends Type {
if (!type) {
throw new Error(`Internal error: Type ${this.reference} not found`);
}
return type.checkBaseType(baseType);
}
}
class StringType extends Type {
- constructor(enumeration, minLength, maxLength) {
+ constructor(enumeration, minLength, maxLength, pattern) {
super();
this.enumeration = enumeration;
this.minLength = minLength;
this.maxLength = maxLength;
+ this.pattern = pattern;
}
normalize(value) {
let r = this.normalizeBase("string", value);
if (r.error) {
return r;
}
@@ -193,16 +205,20 @@ class StringType extends Type {
if (value.length < this.minLength) {
return {error: `String ${JSON.stringify(value)} is too short (must be ${this.minLength})`};
}
if (value.length > this.maxLength) {
return {error: `String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`};
}
+ if (this.pattern && !this.pattern.test(value)) {
+ return {error: `String ${JSON.stringify(value)} must match ${this.pattern}`};
+ }
+
return r;
}
checkBaseType(baseType) {
return baseType == "string";
}
inject(name, dest, wrapperFuncs) {
@@ -212,35 +228,37 @@ class StringType extends Type {
let key = e.toUpperCase();
obj[key] = e;
}
}
}
}
class ObjectType extends Type {
- constructor(properties, additionalProperties, isInstanceOf) {
+ constructor(properties, additionalProperties, patternProperties, isInstanceOf) {
super();
this.properties = properties;
this.additionalProperties = additionalProperties;
+ this.patternProperties = patternProperties;
this.isInstanceOf = isInstanceOf;
}
checkBaseType(baseType) {
return baseType == "object";
}
normalize(value) {
let v = this.normalizeBase("object", value);
if (v.error) {
return v;
}
if (this.isInstanceOf) {
if (Object.keys(this.properties).length ||
+ this.patternProperties.length ||
!(this.additionalProperties instanceof AnyType)) {
throw new Error("InternalError: isInstanceOf can only be used with objects that are otherwise unrestricted");
}
if (!instanceOf(value, this.isInstanceOf)) {
return {error: `Object must be an instance of ${this.isInstanceOf}`};
}
@@ -276,18 +294,18 @@ class ObjectType extends Type {
// Chrome ignores non-enumerable properties.
continue;
}
properties[prop] = Cu.unwaiveXrays(desc.value);
}
}
let result = {};
- for (let prop of Object.keys(this.properties)) {
- let {type, optional, unsupported} = this.properties[prop];
+ let checkProperty = (prop, propType) => {
+ let {type, optional, unsupported} = propType;
if (unsupported) {
if (prop in properties) {
return {error: `Property "${prop}" is unsupported by Firefox`};
}
} else if (prop in properties) {
if (optional && (properties[prop] === null || properties[prop] === undefined)) {
result[prop] = null;
} else {
@@ -297,30 +315,51 @@ class ObjectType extends Type {
}
result[prop] = r.value;
}
} else if (!optional) {
return {error: `Property "${prop}" is required`};
} else {
result[prop] = null;
}
+ };
+
+ for (let prop of Object.keys(this.properties)) {
+ let error = checkProperty(prop, this.properties[prop]);
+ if (error) {
+ return error;
+ }
}
+ outer:
for (let prop of Object.keys(properties)) {
- if (!(prop in this.properties)) {
- if (this.additionalProperties) {
- let r = this.additionalProperties.normalize(properties[prop]);
- if (r.error) {
- return r;
+ if (prop in this.properties) {
+ continue;
+ }
+
+ for (let {pattern, type} of this.patternProperties) {
+ if (pattern.test(prop)) {
+ let error = checkProperty(prop, type);
+ if (error) {
+ return error;
}
- result[prop] = r.value;
- } else {
- return {error: `Unexpected property "${prop}"`};
+
+ continue outer;
}
}
+
+ if (this.additionalProperties) {
+ let r = this.additionalProperties.normalize(properties[prop]);
+ if (r.error) {
+ return r;
+ }
+ result[prop] = r.value;
+ } else {
+ return {error: `Unexpected property "${prop}"`};
+ }
}
return {value: result};
}
}
class NumberType extends Type {
normalize(value) {
@@ -658,53 +697,80 @@ this.Schemas = {
if (!("type" in type)) {
throw new Error(`Unexpected value for type: ${JSON.stringify(type)}`);
}
allowedProperties.add("type");
// Otherwise it's a normal type...
if (type.type == "string") {
- checkTypeProperties("enum", "minLength", "maxLength");
+ checkTypeProperties("enum", "minLength", "maxLength", "pattern");
let enumeration = type.enum || null;
if (enumeration) {
// The "enum" property is either a list of strings that are
// valid values or else a list of {name, description} objects,
// where the .name values are the valid values.
enumeration = enumeration.map(e => {
if (typeof(e) == "object") {
return e.name;
} else {
return e;
}
});
}
+ let pattern = null;
+ if (type.pattern) {
+ try {
+ pattern = parsePattern(type.pattern);
+ } catch (e) {
+ throw new Error(`Internal error: Invalid pattern ${JSON.stringify(type.pattern)}`);
+ }
+ }
return new StringType(enumeration,
type.minLength || 0,
- type.maxLength || Infinity);
+ type.maxLength || Infinity,
+ pattern);
} else if (type.type == "object") {
- let properties = {};
+ let parseProperty = type => {
+ return {
+ type: this.parseType(namespaceName, type,
+ ["optional", "unsupported", "deprecated"]),
+ optional: type.optional || false,
+ unsupported: type.unsupported || false,
+ };
+ };
+
+ let properties = Object.create(null);
for (let propName of Object.keys(type.properties || {})) {
- let propType = this.parseType(namespaceName, type.properties[propName],
- ["optional", "unsupported", "deprecated"]);
- properties[propName] = {
- type: propType,
- optional: type.properties[propName].optional || false,
- unsupported: type.properties[propName].unsupported || false,
- };
+ properties[propName] = parseProperty(type.properties[propName]);
+ }
+
+ let patternProperties = [];
+ for (let propName of Object.keys(type.patternProperties || {})) {
+ let pattern;
+ try {
+ pattern = parsePattern(propName);
+ } catch (e) {
+ throw new Error(`Internal error: Invalid property pattern ${JSON.stringify(propName)}`);
+ }
+
+ patternProperties.push({
+ pattern,
+ type: parseProperty(type.patternProperties[propName]),
+ });
}
let additionalProperties = null;
if ("additionalProperties" in type) {
additionalProperties = this.parseType(namespaceName, type.additionalProperties);
}
- checkTypeProperties("properties", "additionalProperties", "isInstanceOf");
- return new ObjectType(properties, additionalProperties, type.isInstanceOf || null);
+ checkTypeProperties("properties", "additionalProperties", "patternProperties", "isInstanceOf");
+ return new ObjectType(properties, additionalProperties, patternProperties, type.isInstanceOf || null);
} else if (type.type == "array") {
checkTypeProperties("items", "minItems", "maxItems");
return new ArrayType(this.parseType(namespaceName, type.items),
type.minItems || 0, type.maxItems || Infinity);
} else if (type.type == "number") {
checkTypeProperties();
return new NumberType();
} else if (type.type == "integer") {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -121,16 +121,40 @@ let json = [
{
name: "quosimodo",
type: "function",
parameters: [
{name: "xyz", type: "object", additionalProperties: {type: "any"}},
],
},
+
+ {
+ name: "patternprop",
+ type: "function",
+ parameters: [
+ {
+ name: "obj",
+ type: "object",
+ properties: {"prop1": {type: "integer"}},
+ patternProperties: {
+ "(?i)^prop\\d+$": {type: "string"},
+ "^foo\\d+$": {type: "string", optional: true},
+ },
+ },
+ ],
+ },
+
+ {
+ name: "pattern",
+ type: "function",
+ parameters: [
+ {name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"},
+ ],
+ },
],
events: [
{
name: "onFoo",
type: "function",
},
@@ -278,21 +302,63 @@ add_task(function* () {
root.testing.quasar({func: f});
do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["call", "testing", "quasar"]));
do_check_eq(tallied[3][0].func, f);
tallied = null;
root.testing.quosimodo({a: 10, b: 20, c: 30});
verify("call", "testing", "quosimodo", [{a: 10, b: 20, c: 30}]);
+ tallied = null;
Assert.throws(() => root.testing.quosimodo(10),
/Incorrect argument types/,
"should throw for wrong type");
+ root.testing.patternprop({prop1: 12, prop2: "42", Prop3: "43", foo1: "x"});
+ verify("call", "testing", "patternprop", [{prop1: 12, prop2: "42", Prop3: "43", foo1: "x"}]);
+ tallied = null;
+
+ root.testing.patternprop({prop1: 12});
+ verify("call", "testing", "patternprop", [{prop1: 12}]);
+ tallied = null;
+
+ root.testing.patternprop({prop1: 12, foo1: null});
+ verify("call", "testing", "patternprop", [{prop1: 12, foo1: null}]);
+ tallied = null;
+
+
+ Assert.throws(() => root.testing.patternprop({prop1: "12", prop2: "42"}),
+ /Expected integer instead of "12"/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: 12, prop2: 42}),
+ /Expected string instead of 42/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: 12, prop2: null}),
+ /Expected string instead of null/,
+ "should throw for wrong property type");
+
+ Assert.throws(() => root.testing.patternprop({prop1: 12, propx: "42"}),
+ /Unexpected property "propx"/,
+ "should throw for unexpected property");
+
+ Assert.throws(() => root.testing.patternprop({prop1: 12, Foo1: "x"}),
+ /Unexpected property "Foo1"/,
+ "should throw for unexpected property");
+
+ root.testing.pattern("DEADbeef");
+ verify("call", "testing", "pattern", ["DEADbeef"]);
+ tallied = null;
+
+ Assert.throws(() => root.testing.pattern("DEADcow"),
+ /String "DEADcow" must match \/\^\[0-9a-f\]\+\$\/i/,
+ "should throw for non-match");
+
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);
do_check_eq(JSON.stringify(tallied[3][1]), JSON.stringify([]));
tallied = null;
root.testing.onFoo.removeListener(f);
do_check_eq(JSON.stringify(tallied.slice(0, -1)), JSON.stringify(["removeListener", "testing", "onFoo"]));