Bug 1323845: Part 1 - Support multiple schema root namespaces. r=aswan

MozReview-Commit-ID: DfOjHGzLJro

--HG--
extra : rebase_source : 9dcd2ff1c93e41eb9771068e65aad350d295ba18
extra : absorb_source : d43bc1bff19576fe07a4cd33efa3b6df63954570
extra : histedit_source : 4f1d31ab7b09a36b4e5d6220d71185becef47ea8%2Cad4b2eaa4d1535a67e41e6e379d13893f56d1eaf
This commit is contained in:
Kris Maglione 2017-12-16 15:05:13 -06:00
parent abf38239b4
commit 9188b197f6
5 changed files with 478 additions and 133 deletions

View File

@ -34,7 +34,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
XPCOMUtils.defineLazyGetter(this, "StartupCache", () => ExtensionParent.StartupCache);
this.EXPORTED_SYMBOLS = ["Schemas"];
this.EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"];
const {DEBUG} = AppConstants;
@ -1116,6 +1116,8 @@ class Type extends Entry {
* Parses the given schema object and returns an instance of this
* class which corresponds to its properties.
*
* @param {SchemaRoot} root
* The root schema for this type.
* @param {object} schema
* A JSON schema object which corresponds to a definition of
* this type.
@ -1132,7 +1134,7 @@ class Type extends Entry {
* schema object.
* @static
*/
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
return new this(schema);
@ -1226,10 +1228,10 @@ class ChoiceType extends Type {
return ["choices", ...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let choices = schema.choices.map(t => Schemas.parseSchema(t, path));
let choices = schema.choices.map(t => root.parseSchema(t, path));
return new this(schema, choices);
}
@ -1291,27 +1293,28 @@ class RefType extends Type {
return ["$ref", ...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let ref = schema.$ref;
let ns = path[0];
let ns = path.join(".");
if (ref.includes(".")) {
[ns, ref] = ref.split(".");
[, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
}
return new this(schema, ns, ref);
return new this(root, schema, ns, ref);
}
// For a reference to a type named T declared in namespace NS,
// namespaceName will be NS and reference will be T.
constructor(schema, namespaceName, reference) {
constructor(root, schema, namespaceName, reference) {
super(schema);
this.root = root;
this.namespaceName = namespaceName;
this.reference = reference;
}
get targetType() {
let ns = Schemas.getNamespace(this.namespaceName);
let ns = this.root.getNamespace(this.namespaceName);
let type = ns.get(this.reference);
if (!type) {
throw new Error(`Internal error: Type ${this.reference} not found`);
@ -1335,7 +1338,7 @@ class StringType extends Type {
...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let enumeration = schema.enum || null;
@ -1467,9 +1470,9 @@ class ObjectType extends Type {
return ["properties", "patternProperties", "$import", ...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
if ("functions" in schema) {
return SubModuleType.parseSchema(schema, path, extraProperties);
return SubModuleType.parseSchema(root, schema, path, extraProperties);
}
if (DEBUG && !("$extend" in schema)) {
@ -1491,8 +1494,8 @@ class ObjectType extends Type {
let parseProperty = (schema, extraProps = []) => {
return {
type: Schemas.parseSchema(schema, path,
DEBUG && ["unsupported", "onError", "permissions", "default", ...extraProps]),
type: root.parseSchema(schema, path,
DEBUG && ["unsupported", "onError", "permissions", "default", ...extraProps]),
optional: schema.optional || false,
unsupported: schema.unsupported || false,
onError: schema.onError || null,
@ -1530,7 +1533,7 @@ class ObjectType extends Type {
type = {"type": "any"};
}
additionalProperties = Schemas.parseSchema(type, path);
additionalProperties = root.parseSchema(type, path);
}
return new this(schema, properties, additionalProperties, patternProperties, schema.isInstanceOf || null, imported);
@ -1728,19 +1731,19 @@ SubModuleType = class SubModuleType extends Type {
return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
// The path we pass in here is only used for error messages.
path = [...path, schema.id];
let functions = schema.functions.filter(fun => !fun.unsupported)
.map(fun => FunctionEntry.parseSchema(fun, path));
.map(fun => FunctionEntry.parseSchema(root, fun, path));
let events = [];
if (schema.events) {
events = schema.events.filter(event => !event.unsupported)
.map(event => Event.parseSchema(event, path));
.map(event => Event.parseSchema(root, event, path));
}
return new this(functions, events);
@ -1778,7 +1781,7 @@ class IntegerType extends Type {
return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
return new this(schema, schema.minimum || -Infinity, schema.maximum || Infinity);
@ -1835,10 +1838,10 @@ class ArrayType extends Type {
return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let items = Schemas.parseSchema(schema.items, path, ["onError"]);
let items = root.parseSchema(schema.items, path, ["onError"]);
return new this(schema, items, schema.minItems || 0, schema.maxItems || Infinity);
}
@ -1896,7 +1899,7 @@ class FunctionType extends Type {
...super.EXTRA_PROPERTIES];
}
static parseSchema(schema, path, extraProperties = []) {
static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let isAsync = !!schema.async;
@ -1913,7 +1916,7 @@ class FunctionType extends Type {
}
parameters.push({
type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
type: root.parseSchema(param, path, ["name", "optional", "default"]),
name: param.name,
optional: param.optional == null ? isCallback : param.optional,
default: param.default == undefined ? null : param.default,
@ -2047,8 +2050,9 @@ class SubModuleProperty extends Entry {
// namespaceName: Namespace in which the property lives.
// reference: Name of the type defining the functions to add to the property.
// properties: Additional properties to add to the module (unsupported).
constructor(schema, path, name, reference, properties, permissions) {
constructor(root, schema, path, name, reference, properties, permissions) {
super(schema);
this.root = root;
this.name = name;
this.path = path;
this.namespaceName = path.join(".");
@ -2060,11 +2064,11 @@ class SubModuleProperty extends Entry {
getDescriptor(path, context) {
let obj = Cu.createObjectIn(context.cloneScope);
let ns = Schemas.getNamespace(this.namespaceName);
let ns = this.root.getNamespace(this.namespaceName);
let type = ns.get(this.reference);
if (!type && this.reference.includes(".")) {
let [namespaceName, ref] = this.reference.split(".");
ns = Schemas.getNamespace(namespaceName);
ns = this.root.getNamespace(namespaceName);
type = ns.get(ref);
}
@ -2196,19 +2200,19 @@ class CallEntry extends Entry {
// Represents a "function" defined in a schema namespace.
FunctionEntry = class FunctionEntry extends CallEntry {
static parseSchema(schema, path) {
static parseSchema(root, schema, path) {
// When not in DEBUG mode, we just need to know *if* this returns.
let returns = !!schema.returns;
if (DEBUG && "returns" in schema) {
returns = {
type: Schemas.parseSchema(schema.returns, path, ["optional", "name"]),
type: root.parseSchema(schema.returns, path, ["optional", "name"]),
optional: schema.returns.optional || false,
name: "result",
};
}
return new this(schema, path, schema.name,
Schemas.parseSchema(
root.parseSchema(
schema, path,
["name", "unsupported", "returns",
"permissions",
@ -2321,9 +2325,9 @@ FunctionEntry = class FunctionEntry extends CallEntry {
// TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
// once Bug 1369722 has been fixed.
Event = class Event extends CallEntry { // eslint-disable-line no-native-reassign
static parseSchema(event, path) {
static parseSchema(root, event, path) {
let extraParameters = Array.from(event.extraParameters || [], param => ({
type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
type: root.parseSchema(param, path, ["name", "optional", "default"]),
name: param.name,
optional: param.optional || false,
default: param.default == undefined ? null : param.default,
@ -2334,7 +2338,7 @@ Event = class Event extends CallEntry { // eslint-disable-line no-native-reassig
"returns", "filters"];
return new this(event, path, event.name,
Schemas.parseSchema(event, path, extraProperties),
root.parseSchema(event, path, extraProperties),
extraParameters,
event.unsupported || false,
event.permissions || null);
@ -2415,9 +2419,11 @@ const LOADERS = {
};
class Namespace extends Map {
constructor(name, path) {
constructor(root, name, path) {
super();
this.root = root;
this._lazySchemas = [];
this.initialized = false;
@ -2449,7 +2455,7 @@ class Namespace extends Map {
}
if (schema.$import) {
this.superNamespace = Schemas.getNamespace(schema.$import);
this.superNamespace = this.root.getNamespace(schema.$import);
}
}
@ -2544,7 +2550,7 @@ class Namespace extends Map {
if ("$extend" in type) {
return this.extendType(type);
}
return Schemas.parseSchema(type, this.path, ["id"]);
return this.root.parseSchema(type, this.path, ["id"]);
}
extendType(type) {
@ -2561,7 +2567,7 @@ class Namespace extends Map {
}
}
let parsed = Schemas.parseSchema(type, this.path, ["$extend"]);
let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
if (DEBUG && parsed.constructor !== targetType.constructor) {
throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
@ -2575,7 +2581,8 @@ class Namespace extends Map {
loadProperty(name, prop) {
if ("$ref" in prop) {
if (!prop.unsupported) {
return new SubModuleProperty(prop, this.path, name,
return new SubModuleProperty(this.root,
prop, this.path, name,
prop.$ref, prop.properties || {},
prop.permissions || null);
}
@ -2584,18 +2591,18 @@ class Namespace extends Map {
} else {
// We ignore the "optional" attribute on properties since we
// don't inject anything here anyway.
let type = Schemas.parseSchema(prop, [this.name], ["optional", "permissions", "writable"]);
let type = this.root.parseSchema(prop, [this.name], ["optional", "permissions", "writable"]);
return new TypeProperty(prop, this.path, name, type, prop.writable || false,
prop.permissions || null);
}
}
loadFunction(name, fun) {
return FunctionEntry.parseSchema(fun, this.path);
return FunctionEntry.parseSchema(this.root, fun, this.path);
}
loadEvent(name, event) {
return Event.parseSchema(event, this.path);
return Event.parseSchema(this.root, event, this.path);
}
/**
@ -2662,10 +2669,13 @@ class Namespace extends Map {
*
* @param {string} name
* The name of the sub-namespace to retrieve.
* @param {boolean} [create = true]
* If true, create any intermediate namespaces which don't
* exist.
*
* @returns {Namespace}
*/
getNamespace(name) {
getNamespace(name, create = true) {
let subName;
let idx = name.indexOf(".");
@ -2676,7 +2686,10 @@ class Namespace extends Map {
let ns = super.get(name);
if (!ns) {
ns = new Namespace(name, this.path);
if (!create) {
return null;
}
ns = new Namespace(this.root, name, this.path);
this.set(name, ns);
}
@ -2686,40 +2699,75 @@ class Namespace extends Map {
return ns;
}
getOwnNamespace(name) {
return this.getNamespace(name);
}
has(key) {
this.init();
return super.has(key);
}
}
this.Schemas = {
initialized: false,
REVOKE: Symbol("@@revoke"),
/**
* A root schema namespace containing schema data which is isolated from data in
* other schema roots. May extend a base namespace, in which case schemas in
* this root may refer to types in a base, but not vice versa.
*
* @param {SchemaRoot|Array<SchemaRoot>|null} base
* A base schema root (or roots) from which to derive, or null.
* @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
* A map of schema URLs and corresponding JSON blobs from which to
* populate this root namespace.
*/
class SchemaRoot extends Namespace {
constructor(base, schemaJSON) {
super(null, "", []);
// Maps a schema URL to the JSON contained in that schema file. This
// is useful for sending the JSON across processes.
schemaJSON: new Map(),
this.root = this;
this.base = base;
this.schemaJSON = schemaJSON;
}
// A separate map of schema JSON which should be available in all
// content processes.
contentSchemaJSON: new Map(),
/**
* Returns the sub-namespace with the given name. If the given namespace
* doesn't already exist, attempts to find it in the base SchemaRoot before
* creating a new empty namespace.
*
* @param {string} name
* The namespace to retrieve.
* @param {boolean} [create = true]
* If true, an empty namespace should be created if one does not
* already exist.
* @returns {Namespace|null}
*/
getNamespace(name, create = true) {
let res = this.base && this.base.getNamespace(name, false);
if (res) {
return res;
}
return super.getNamespace(name, create);
}
// Map[<schema-name> -> Map[<symbol-name> -> Entry]]
// This keeps track of all the schemas that have been loaded so far.
rootNamespace: new Namespace("", []),
getNamespace(name) {
return this.rootNamespace.getNamespace(name);
},
/**
* Like getNamespace, but does not take the base SchemaRoot into account.
*
* @param {string} name
* The namespace to retrieve.
* @returns {Namespace}
*/
getOwnNamespace(name) {
return super.getNamespace(name);
}
parseSchema(schema, path, extraProperties = []) {
let allowedProperties = DEBUG && new Set(extraProperties);
if ("choices" in schema) {
return ChoiceType.parseSchema(schema, path, allowedProperties);
return ChoiceType.parseSchema(this, schema, path, allowedProperties);
} else if ("$ref" in schema) {
return RefType.parseSchema(schema, path, allowedProperties);
return RefType.parseSchema(this, schema, path, allowedProperties);
}
let type = TYPES[schema.type];
@ -2736,7 +2784,123 @@ this.Schemas = {
}
}
return type.parseSchema(schema, path, allowedProperties);
return type.parseSchema(this, schema, path, allowedProperties);
}
parseSchemas() {
for (let [key, schema] of this.schemaJSON.entries()) {
try {
if (typeof schema.deserialize === "function") {
schema = schema.deserialize(global);
// If we're in the parent process, we need to keep the
// StructuredCloneHolder blob around in order to send to future child
// processes. If we're in a child, we have no further use for it, so
// just store the deserialized schema data in its place.
if (!isParentProcess) {
this.schemaJSON.set(key, schema);
}
}
this.loadSchema(schema);
} catch (e) {
Cu.reportError(e);
}
}
}
loadSchema(json) {
for (let namespace of json) {
this.getOwnNamespace(namespace.namespace)
.addSchema(namespace);
}
}
/**
* Checks whether a given object has the necessary permissions to
* expose the given namespace.
*
* @param {string} namespace
* The top-level namespace to check permissions for.
* @param {object} wrapperFuncs
* Wrapper functions for the given context.
* @param {function} wrapperFuncs.hasPermission
* A function which, when given a string argument, returns true
* if the context has the given permission.
* @returns {boolean}
* True if the context has permission for the given namespace.
*/
checkPermissions(namespace, wrapperFuncs) {
let ns = this.getNamespace(namespace);
if (ns && ns.permissions) {
return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
}
return true;
}
/**
* Inject registered extension APIs into `dest`.
*
* @param {object} dest The root namespace for the APIs.
* This object is usually exposed to extensions as "chrome" or "browser".
* @param {object} wrapperFuncs An implementation of the InjectionContext
* interface, which runs the actual functionality of the generated API.
*/
inject(dest, wrapperFuncs) {
let context = new InjectionContext(wrapperFuncs);
if (this.base) {
this.base.injectInto(dest, context);
}
this.injectInto(dest, context);
}
/**
* Normalize `obj` according to the loaded schema for `typeName`.
*
* @param {object} obj The object to normalize against the schema.
* @param {string} typeName The name in the format namespace.propertyname
* @param {object} context An implementation of Context. Any validation errors
* are reported to the given context.
* @returns {object} The normalized object.
*/
normalize(obj, typeName, context) {
let [namespaceName, prop] = typeName.split(".");
let ns = this.getNamespace(namespaceName);
let type = ns.get(prop);
let result = type.normalize(obj, new Context(context));
if (result.error) {
return {error: forceString(result.error)};
}
return result;
}
}
this.Schemas = {
initialized: false,
REVOKE: Symbol("@@revoke"),
// Maps a schema URL to the JSON contained in that schema file. This
// is useful for sending the JSON across processes.
schemaJSON: new Map(),
// A separate map of schema JSON which should be available in all
// content processes.
contentSchemaJSON: new Map(),
_rootSchema: null,
get rootSchema() {
if (!this.initialized) {
this.init();
}
return this._rootSchema;
},
getNamespace(name) {
return this.rootSchema.getNamespace(name);
},
init() {
@ -2787,47 +2951,13 @@ this.Schemas = {
flushSchemas() {
if (this._needFlush) {
this._needFlush = false;
XPCOMUtils.defineLazyGetter(this, "rootNamespace",
() => this.parseSchemas());
}
},
XPCOMUtils.defineLazyGetter(this, "_rootSchema", () => {
this._needFlush = true;
parseSchemas() {
this._needFlush = true;
Object.defineProperty(this, "rootNamespace", {
enumerable: true,
configurable: true,
value: new Namespace("", []),
});
for (let [key, schema] of this.schemaJSON.entries()) {
try {
if (typeof schema.deserialize === "function") {
schema = schema.deserialize(global);
// If we're in the parent process, we need to keep the
// StructuredCloneHolder blob around in order to send to future child
// processes. If we're in a child, we have no further use for it, so
// just store the deserialized schema data in its place.
if (!isParentProcess) {
this.schemaJSON.set(key, schema);
}
}
this.loadSchema(schema);
} catch (e) {
Cu.reportError(e);
}
}
return this.rootNamespace;
},
loadSchema(json) {
for (let namespace of json) {
this.getNamespace(namespace.namespace)
.addSchema(namespace);
let rootSchema = new SchemaRoot(null, this.schemaJSON);
rootSchema.parseSchemas();
return rootSchema;
});
}
},
@ -2900,15 +3030,7 @@ this.Schemas = {
* True if the context has permission for the given namespace.
*/
checkPermissions(namespace, wrapperFuncs) {
if (!this.initialized) {
this.init();
}
let ns = this.getNamespace(namespace);
if (ns && ns.permissions) {
return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
}
return true;
return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
},
exportLazyGetter,
@ -2922,13 +3044,7 @@ this.Schemas = {
* interface, which runs the actual functionality of the generated API.
*/
inject(dest, wrapperFuncs) {
if (!this.initialized) {
this.init();
}
let context = new InjectionContext(wrapperFuncs);
this.rootNamespace.injectInto(dest, context);
this.rootSchema.inject(dest, wrapperFuncs);
},
/**
@ -2941,18 +3057,6 @@ this.Schemas = {
* @returns {object} The normalized object.
*/
normalize(obj, typeName, context) {
if (!this.initialized) {
this.init();
}
let [namespaceName, prop] = typeName.split(".");
let ns = this.getNamespace(namespaceName);
let type = ns.get(prop);
let result = type.normalize(obj, new Context(context));
if (result.error) {
return {error: forceString(result.error)};
}
return result;
return this.rootSchema.normalize(obj, typeName, context);
},
};

View File

@ -85,7 +85,7 @@
"type": "any"
},
"scope": {
"$ref": "SettingScope",
"$ref": "types.SettingScope",
"optional": true,
"description": "Where to set the setting (default: regular)."
}
@ -112,7 +112,7 @@
"description": "Which setting to clear.",
"properties": {
"scope": {
"$ref": "SettingScope",
"$ref": "types.SettingScope",
"optional": true,
"description": "Where to clear the setting (default: regular)."
}

View File

@ -0,0 +1,239 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
Components.utils.import("resource://gre/modules/Schemas.jsm");
Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
let {SchemaAPIInterface} = ExtensionCommon;
const global = this;
let baseSchemaJSON = [
{
namespace: "base",
properties: {
PROP1: {value: 42},
},
types: [
{
id: "type1",
type: "string",
"enum": ["value1", "value2", "value3"],
},
],
functions: [
{
name: "foo",
type: "function",
parameters: [
{name: "arg1", $ref: "type1"},
],
},
],
},
];
let experimentFooJSON = [
{
namespace: "experiments.foo",
types: [
{
id: "typeFoo",
type: "string",
"enum": ["foo1", "foo2", "foo3"],
},
],
functions: [
{
name: "foo",
type: "function",
parameters: [
{name: "arg1", $ref: "typeFoo"},
{name: "arg2", $ref: "base.type1"},
],
},
],
},
];
let experimentBarJSON = [
{
namespace: "experiments.bar",
types: [
{
id: "typeBar",
type: "string",
"enum": ["bar1", "bar2", "bar3"],
},
],
functions: [
{
name: "bar",
type: "function",
parameters: [
{name: "arg1", $ref: "typeBar"},
{name: "arg2", $ref: "base.type1"},
],
},
],
},
];
let tallied = null;
function tally(kind, ns, name, args) {
tallied = [kind, ns, name, args];
}
function verify(...args) {
equal(JSON.stringify(tallied), JSON.stringify(args));
tallied = null;
}
let talliedErrors = [];
let permissions = new Set();
class TallyingAPIImplementation extends SchemaAPIInterface {
constructor(namespace, name) {
super();
this.namespace = namespace;
this.name = name;
}
callFunction(args) {
tally("call", this.namespace, this.name, args);
if (this.name === "sub_foo") {
return 13;
}
}
callFunctionNoReturn(args) {
tally("call", this.namespace, this.name, args);
}
getProperty() {
tally("get", this.namespace, this.name);
}
setProperty(value) {
tally("set", this.namespace, this.name, value);
}
addListener(listener, args) {
tally("addListener", this.namespace, this.name, [listener, args]);
}
removeListener(listener) {
tally("removeListener", this.namespace, this.name, [listener]);
}
hasListener(listener) {
tally("hasListener", this.namespace, this.name, [listener]);
}
}
let wrapper = {
url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
cloneScope: global,
checkLoadURL(url) {
return !url.startsWith("chrome:");
},
preprocessors: {
localize(value, context) {
return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
},
},
logError(message) {
talliedErrors.push(message);
},
hasPermission(permission) {
return permissions.has(permission);
},
shouldInject(ns, name) {
return name != "do-not-inject";
},
getImplementation(namespace, name) {
return new TallyingAPIImplementation(namespace, name);
},
};
add_task(async function() {
let baseSchemas = new Map([
["resource://schemas/base.json", baseSchemaJSON],
]);
let experimentSchemas = new Map([
["resource://experiment-foo/schema.json", experimentFooJSON],
["resource://experiment-bar/schema.json", experimentBarJSON],
]);
let baseSchema = new SchemaRoot(null, baseSchemas);
let schema = new SchemaRoot(baseSchema, experimentSchemas);
baseSchema.parseSchemas();
schema.parseSchemas();
let root = {};
let base = {};
tallied = null;
baseSchema.inject(base, wrapper);
schema.inject(root, wrapper);
equal(typeof base.base, "object", "base.base exists");
equal(typeof root.base, "object", "root.base exists");
equal(typeof base.experiments, "undefined", "base.experiments exists not");
equal(typeof root.experiments, "object", "root.experiments exists");
equal(typeof root.experiments.foo, "object", "root.experiments.foo exists");
equal(typeof root.experiments.bar, "object", "root.experiments.bar exists");
equal(tallied, null);
equal(root.base.PROP1, 42, "root.base.PROP1");
equal(base.base.PROP1, 42, "root.base.PROP1");
root.base.foo("value2");
verify("call", "base", "foo", ["value2"]);
base.base.foo("value3");
verify("call", "base", "foo", ["value3"]);
root.experiments.foo.foo("foo2", "value1");
verify("call", "experiments.foo", "foo", ["foo2", "value1"]);
root.experiments.bar.bar("bar2", "value1");
verify("call", "experiments.bar", "bar", ["bar2", "value1"]);
Assert.throws(() => root.base.foo("Meh."),
/Type error for parameter arg1/,
"root.base.foo()");
Assert.throws(() => base.base.foo("Meh."),
/Type error for parameter arg1/,
"base.base.foo()");
Assert.throws(() => root.experiments.foo.foo("Meh."),
/Incorrect argument types/,
"root.experiments.foo.foo()");
Assert.throws(() => root.experiments.bar.bar("Meh."),
/Incorrect argument types/,
"root.experiments.bar.bar()");
});

View File

@ -40,6 +40,7 @@ tags = webextensions in-process-webextensions
[test_ext_manifest_minimum_opera_version.js]
[test_ext_manifest_themes.js]
[test_ext_schemas.js]
[test_ext_schemas_roots.js]
[test_ext_schemas_async.js]
[test_ext_schemas_allowed_contexts.js]
[test_ext_schemas_interactive.js]

View File

@ -175,6 +175,7 @@
"rotaryengine.js": ["RotaryEngine", "RotaryRecord", "RotaryStore", "RotaryTracker"],
"require.js": ["require"],
"RTCStatsReport.jsm": ["convertToRTCStatsReport"],
"Schemas.jsm": ["SchemaRoot", "Schemas"],
"scratchpad-manager.jsm": ["ScratchpadManager"],
"server.js": ["server"],
"service.js": ["Service"],