mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-31 22:25:30 +00:00
Bug 1225715: Part 1 - Add support for patterned strings and properties in schemas. r=billm
--HG-- extra : commitid : DXiMnqrHsn0 extra : rebase_source : e8457775d1c9510e20e3f987eb8deccbcfbb50fb
This commit is contained in:
parent
985ff5df04
commit
f9a63c1481
@ -45,6 +45,17 @@ function readJSON(uri) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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") {
|
||||
@ -171,11 +182,12 @@ class RefType extends Type {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -198,6 +210,10 @@ class StringType extends Type {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -217,10 +233,11 @@ class StringType extends Type {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -236,6 +253,7 @@ class ObjectType extends Type {
|
||||
|
||||
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");
|
||||
}
|
||||
@ -280,9 +298,10 @@ class ObjectType extends Type {
|
||||
}
|
||||
}
|
||||
|
||||
let result = {};
|
||||
for (let prop of Object.keys(this.properties)) {
|
||||
let {type, optional, unsupported} = this.properties[prop];
|
||||
let remainingProps = new Set(Object.keys(properties));
|
||||
|
||||
let checkProperty = (prop, propType, result) => {
|
||||
let {type, optional, unsupported} = propType;
|
||||
if (unsupported) {
|
||||
if (prop in properties) {
|
||||
return {error: `Property "${prop}" is unsupported by Firefox`};
|
||||
@ -296,28 +315,50 @@ class ObjectType extends Type {
|
||||
return r;
|
||||
}
|
||||
result[prop] = r.value;
|
||||
properties[prop] = r.value;
|
||||
}
|
||||
remainingProps.delete(prop);
|
||||
} else if (!optional) {
|
||||
return {error: `Property "${prop}" is required`};
|
||||
} else {
|
||||
result[prop] = null;
|
||||
}
|
||||
};
|
||||
|
||||
let result = {};
|
||||
for (let prop of Object.keys(this.properties)) {
|
||||
let error = checkProperty(prop, this.properties[prop], result);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
for (let {pattern, type} of this.patternProperties) {
|
||||
if (pattern.test(prop)) {
|
||||
let error = checkProperty(prop, type, result);
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
result[prop] = r.value;
|
||||
} else {
|
||||
return {error: `Unexpected property "${prop}"`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.additionalProperties) {
|
||||
for (let prop of remainingProps) {
|
||||
let type = this.additionalProperties;
|
||||
let r = type.normalize(properties[prop]);
|
||||
if (r.error) {
|
||||
return r;
|
||||
}
|
||||
result[prop] = r.value;
|
||||
}
|
||||
} else if (remainingProps.size == 1) {
|
||||
return {error: `Unexpected property "${[...remainingProps]}"`};
|
||||
} else if (remainingProps.size) {
|
||||
return {error: `Unexpected properties: ${[...remainingProps]}`};
|
||||
}
|
||||
|
||||
return {value: result};
|
||||
}
|
||||
}
|
||||
@ -702,7 +743,7 @@ this.Schemas = {
|
||||
|
||||
// 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) {
|
||||
@ -717,19 +758,46 @@ this.Schemas = {
|
||||
}
|
||||
});
|
||||
}
|
||||
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 = {};
|
||||
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,
|
||||
let parseProperty = (type, extraProps = []) => {
|
||||
return {
|
||||
type: this.parseType(namespaceName, type,
|
||||
["unsupported", "deprecated", ...extraProps]),
|
||||
optional: type.optional || false,
|
||||
unsupported: type.unsupported || false,
|
||||
};
|
||||
};
|
||||
|
||||
let properties = Object.create(null);
|
||||
for (let propName of Object.keys(type.properties || {})) {
|
||||
properties[propName] = parseProperty(type.properties[propName], ["optional"]);
|
||||
}
|
||||
|
||||
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;
|
||||
@ -737,8 +805,8 @@ this.Schemas = {
|
||||
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),
|
||||
|
@ -126,6 +126,30 @@ let json = [
|
||||
{name: "xyz", type: "object", additionalProperties: {type: "any"}},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "patternprop",
|
||||
type: "function",
|
||||
parameters: [
|
||||
{
|
||||
name: "obj",
|
||||
type: "object",
|
||||
properties: {"prop1": {type: "string", pattern: "^\\d+$"}},
|
||||
patternProperties: {
|
||||
"(?i)^prop\\d+$": {type: "string"},
|
||||
"^foo\\d+$": {type: "string"},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "pattern",
|
||||
type: "function",
|
||||
parameters: [
|
||||
{name: "arg", type: "string", pattern: "(?i)^[0-9a-f]+$"},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
events: [
|
||||
@ -283,11 +307,52 @@ add_task(function* () {
|
||||
|
||||
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;
|
||||
|
||||
Assert.throws(() => root.testing.patternprop({prop1: "12", foo1: null}),
|
||||
/Expected string instead of null/,
|
||||
"should throw for wrong property type");
|
||||
|
||||
Assert.throws(() => root.testing.patternprop({prop1: "xx", prop2: "yy"}),
|
||||
/String "xx" must match \/\^\\d\+\$\//,
|
||||
"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);
|
||||
|
Loading…
Reference in New Issue
Block a user