mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 22:01:30 +00:00
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:
parent
abf38239b4
commit
9188b197f6
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -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)."
|
||||
}
|
||||
|
@ -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()");
|
||||
});
|
@ -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]
|
||||
|
@ -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"],
|
||||
|
Loading…
Reference in New Issue
Block a user