Bug 1338409: Part 1 - Lazily parse the schema data for each namespace as it is needed. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 09 Feb 2017 15:46:52 -0800
changeset 481592 504e8335ebcfba6bef87aae6a6bbed79f4eb8702
parent 480279 dde8ef8cf38ed5affd0a4073bf1b3e897f334e96
child 481593 080d30c8e3b113fa800436782bd1b4f3c3c11617
push id44862
push usermaglione.k@gmail.com
push dateFri, 10 Feb 2017 03:56:08 +0000
reviewersaswan
bugs1338409
milestone54.0a1
Bug 1338409: Part 1 - Lazily parse the schema data for each namespace as it is needed. r?aswan MozReview-Commit-ID: 73q9ba9sLtb
browser/components/extensions/schemas/devtools.json
toolkit/components/extensions/Schemas.jsm
--- a/browser/components/extensions/schemas/devtools.json
+++ b/browser/components/extensions/schemas/devtools.json
@@ -7,10 +7,15 @@
         "properties": {
           "devtools_page": {
             "$ref": "ExtensionURL",
             "optional": true
           }
         }
       }
     ]
+  },
+  {
+    "namespace": "devtools",
+    "allowedContexts": ["devtools", "devtools_only"],
+    "defaultContexts": ["devtools", "devtools_only"]
   }
 ]
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -8,42 +8,35 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const global = this;
 
 Cu.importGlobalProperties(["URL"]);
 
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   DefaultMap,
   instanceOf,
 } = ExtensionUtils;
 
-class DeepMap extends DefaultMap {
-  constructor() {
-    super(() => new DeepMap());
-  }
-
-  getPath(...keys) {
-    return keys.reduce((map, prop) => map.get(prop), this);
-  }
-}
-
 XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
                                    "@mozilla.org/addons/content-policy;1",
                                    "nsIAddonContentPolicy");
 
 this.EXPORTED_SYMBOLS = ["Schemas"];
 
+const {DEBUG} = AppConstants;
+
 /* globals Schemas, URL */
 
 function readJSON(url) {
   return new Promise((resolve, reject) => {
     NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
       if (!Components.isSuccessCode(status)) {
         // Convert status code to a string
         let e = Components.Exception("", status);
@@ -639,21 +632,20 @@ class Entry {
 
   /**
    * Injects JS values for the entry into the extension API
    * namespace. The default implementation is to do nothing.
    * `context` is used to call the actual implementation
    * of a given function or event.
    *
    * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
-   * @param {string} name The method name, e.g. "get".
    * @param {object} dest The object where `path`.`name` should be stored.
    * @param {InjectionContext} context
    */
-  inject(path, name, dest, context) {
+  inject(path, dest, context) {
   }
 }
 
 // Corresponds either to a type declared in the "types" section of the
 // schema or else to any type object used throughout the schema.
 class Type extends Entry {
   /**
    * @property {Array<string>} EXTRA_PROPERTIES
@@ -704,21 +696,25 @@ class Type extends Entry {
    * @param {Array<string>} [extra]
    *        An array of extra property names which are valid for this
    *        schema in the current context.
    * @throws {Error}
    *        An error describing the first invalid property found in the
    *        schema object.
    */
   static checkSchemaProperties(schema, path, extra = []) {
-    let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
+    if (DEBUG) {
+      let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
 
-    for (let prop of Object.keys(schema)) {
-      if (!allowedSet.has(prop)) {
-        throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${schema.id || JSON.stringify(schema)}"`);
+      for (let prop of Object.keys(schema)) {
+        if (!allowedSet.has(prop)) {
+          throw new Error(`Internal error: Namespace ${path.join(".")} has ` +
+                          `invalid type property "${prop}" ` +
+                          `in type "${schema.id || JSON.stringify(schema)}"`);
+        }
       }
     }
   }
 
   // Takes a value, checks that it has the correct type, and returns a
   // "normalized" version of the value. The normalized version will
   // include "nulls" in place of omitted optional properties. The
   // result of this function is either {error: "Some type error"} or
@@ -848,17 +844,17 @@ class RefType extends Type {
   // namespaceName will be NS and reference will be T.
   constructor(schema, namespaceName, reference) {
     super(schema);
     this.namespaceName = namespaceName;
     this.reference = reference;
   }
 
   get targetType() {
-    let ns = Schemas.namespaces.get(this.namespaceName);
+    let ns = Schemas.getNamespace(this.namespaceName);
     let type = ns.get(this.reference);
     if (!type) {
       throw new Error(`Internal error: Type ${this.reference} not found`);
     }
     return type;
   }
 
   normalize(value, context) {
@@ -904,25 +900,28 @@ class StringType extends Type {
 
     let format = null;
     if (schema.format) {
       if (!(schema.format in FORMATS)) {
         throw new Error(`Internal error: Invalid string format ${schema.format}`);
       }
       format = FORMATS[schema.format];
     }
-    return new this(schema, enumeration,
+    return new this(schema,
+                    schema.id,
+                    enumeration,
                     schema.minLength || 0,
                     schema.maxLength || Infinity,
                     pattern,
                     format);
   }
 
-  constructor(schema, enumeration, minLength, maxLength, pattern, format) {
+  constructor(schema, name, enumeration, minLength, maxLength, pattern, format) {
     super(schema);
+    this.name = name;
     this.enumeration = enumeration;
     this.minLength = minLength;
     this.maxLength = maxLength;
     this.pattern = pattern;
     this.format = format;
   }
 
   normalize(value, context) {
@@ -967,50 +966,52 @@ class StringType extends Type {
 
     return r;
   }
 
   checkBaseType(baseType) {
     return baseType == "string";
   }
 
-  inject(path, name, dest, context) {
+  inject(path, dest, context) {
     if (this.enumeration) {
-      exportLazyGetter(dest, name, () => {
+      exportLazyGetter(dest, this.name, () => {
         let obj = Cu.createObjectIn(dest);
         for (let e of this.enumeration) {
           obj[e.toUpperCase()] = e;
         }
         return obj;
       });
     }
   }
 }
 
+let FunctionEntry;
 let SubModuleType;
+
 class ObjectType extends Type {
   static get EXTRA_PROPERTIES() {
     return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES];
   }
 
   static parseSchema(schema, path, extraProperties = []) {
     if ("functions" in schema) {
       return SubModuleType.parseSchema(schema, path, extraProperties);
     }
 
-    if (!("$extend" in schema)) {
+    if (DEBUG && !("$extend" in schema)) {
       // Only allow extending "properties" and "patternProperties".
       extraProperties = ["additionalProperties", "isInstanceOf", ...extraProperties];
     }
     this.checkSchemaProperties(schema, path, extraProperties);
 
     let parseProperty = (schema, extraProps = []) => {
       return {
         type: Schemas.parseSchema(schema, path,
-                                  ["unsupported", "onError", "permissions", ...extraProps]),
+                                  DEBUG && ["unsupported", "onError", "permissions", ...extraProps]),
         optional: schema.optional || false,
         unsupported: schema.unsupported || false,
         onError: schema.onError || null,
       };
     };
 
     // Parse explicit "properties" object.
     let properties = Object.create(null);
@@ -1161,20 +1162,23 @@ class ObjectType extends Type {
     try {
       let v = this.normalizeBase("object", value, context);
       if (v.error) {
         return v;
       }
       value = v.value;
 
       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 (DEBUG) {
+          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 context.error(`Object must be an instance of ${this.isInstanceOf}`,
                                `be an instance of ${this.isInstanceOf}`);
         }
 
         // This is kind of a hack, but we can't normalize things that
@@ -1235,17 +1239,17 @@ SubModuleType = class SubModuleType exte
     return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
   }
 
   static parseSchema(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.map(fun => Schemas.parseFunction(path, fun));
+    let functions = schema.functions.map(fun => FunctionEntry.parseSchema(fun, path));
 
     return new this(functions);
   }
 
   constructor(functions) {
     super();
     this.functions = functions;
   }
@@ -1406,34 +1410,40 @@ class FunctionType extends Type {
         parameters.push({
           type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
           name: param.name,
           optional: param.optional == null ? isCallback : param.optional,
           default: param.default == undefined ? null : param.default,
         });
       }
     }
-    if (isExpectingCallback) {
-      throw new Error(`Internal error: Expected a callback parameter with name ${schema.async}`);
-    }
-
     let hasAsyncCallback = false;
     if (isAsync) {
       hasAsyncCallback = (parameters &&
                           parameters.length &&
                           parameters[parameters.length - 1].name == schema.async);
+    }
 
-      if (schema.returns) {
-        throw new Error("Internal error: Async functions must not have return values.");
+    if (DEBUG) {
+      if (isExpectingCallback) {
+        throw new Error(`Internal error: Expected a callback parameter ` +
+                        `with name ${schema.async}`);
       }
-      if (schema.allowAmbiguousOptionalArguments && !hasAsyncCallback) {
-        throw new Error("Internal error: Async functions with ambiguous arguments must declare the callback as the last parameter");
+
+      if (isAsync && schema.returns) {
+        throw new Error("Internal error: Async functions must not " +
+                        "have return values.");
+      }
+      if (isAsync && schema.allowAmbiguousOptionalArguments && !hasAsyncCallback) {
+        throw new Error("Internal error: Async functions with ambiguous " +
+                        "arguments must declare the callback as the last parameter");
       }
     }
 
+
     return new this(schema, parameters, isAsync, hasAsyncCallback);
   }
 
   constructor(schema, parameters, isAsync, hasAsyncCallback) {
     super(schema);
     this.parameters = parameters;
     this.isAsync = isAsync;
     this.hasAsyncCallback = hasAsyncCallback;
@@ -1452,42 +1462,42 @@ class FunctionType extends Type {
 // particular value. Essentially this is a constant.
 class ValueProperty extends Entry {
   constructor(schema, name, value) {
     super(schema);
     this.name = name;
     this.value = value;
   }
 
-  inject(path, name, dest, context) {
-    dest[name] = this.value;
+  inject(path, dest, context) {
+    dest[this.name] = this.value;
   }
 }
 
 // Represents a "property" defined in a schema namespace that is not a
 // constant.
 class TypeProperty extends Entry {
-  constructor(schema, namespaceName, name, type, writable) {
+  constructor(schema, path, name, type, writable) {
     super(schema);
-    this.namespaceName = namespaceName;
+    this.path = path;
     this.name = name;
     this.type = type;
     this.writable = writable;
   }
 
   throwError(context, msg) {
-    throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
+    throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
   }
 
-  inject(path, name, dest, context) {
+  inject(path, dest, context) {
     if (this.unsupported) {
       return;
     }
 
-    let apiImpl = context.getImplementation(path.join("."), name);
+    let apiImpl = context.getImplementation(path.join("."), this.name);
 
     let getStub = () => {
       this.checkDeprecated(context);
       return apiImpl.getProperty();
     };
 
     let desc = {
       configurable: false,
@@ -1504,61 +1514,66 @@ class TypeProperty extends Entry {
         }
 
         apiImpl.setProperty(normalized.value);
       };
 
       desc.set = Cu.exportFunction(setStub, dest);
     }
 
-    Object.defineProperty(dest, name, desc);
+    Object.defineProperty(dest, this.name, desc);
   }
 }
 
 class SubModuleProperty extends Entry {
   // A SubModuleProperty represents a tree of objects and properties
   // to expose to an extension. Currently we support only a limited
   // form of sub-module properties, where "$ref" points to a
   // SubModuleType containing a list of functions and "properties" is
   // a list of additional simple properties.
   //
   // name: Name of the property stuff is being added to.
   // 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, name, namespaceName, reference, properties) {
+  constructor(schema, path, name, reference, properties) {
     super(schema);
     this.name = name;
-    this.namespaceName = namespaceName;
+    this.path = path;
+    this.namespaceName = path.join(".");
     this.reference = reference;
     this.properties = properties;
   }
 
-  inject(path, name, dest, context) {
-    exportLazyGetter(dest, name, () => {
+  inject(path, dest, context) {
+    exportLazyGetter(dest, this.name, () => {
       let obj = Cu.createObjectIn(dest);
 
-      let ns = Schemas.namespaces.get(this.namespaceName);
+      let ns = Schemas.getNamespace(this.namespaceName);
       let type = ns.get(this.reference);
       if (!type && this.reference.includes(".")) {
         let [namespaceName, ref] = this.reference.split(".");
-        ns = Schemas.namespaces.get(namespaceName);
+        ns = Schemas.getNamespace(namespaceName);
         type = ns.get(ref);
       }
-      if (!type || !(type instanceof SubModuleType)) {
-        throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
+
+      if (DEBUG) {
+        if (!type || !(type instanceof SubModuleType)) {
+          throw new Error(`Internal error: ${this.namespaceName}.${this.reference} ` +
+                          `is not a sub-module`);
+        }
       }
+      let subpath = [path, this.name];
+      let namespace = subpath.join(".");
 
       let functions = type.functions;
       for (let fun of functions) {
-        let subpath = path.concat(name);
-        let namespace = subpath.join(".");
         let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
         if (context.shouldInject(namespace, fun.name, allowedContexts)) {
-          fun.inject(subpath, fun.name, obj, context);
+          fun.inject(subpath, obj, context);
         }
       }
 
       // TODO: Inject this.properties.
 
       return obj;
     });
   }
@@ -1650,38 +1665,50 @@ class CallEntry extends Entry {
       return r.value;
     });
 
     return fixedArgs;
   }
 }
 
 // Represents a "function" defined in a schema namespace.
-class FunctionEntry extends CallEntry {
+FunctionEntry = class FunctionEntry extends CallEntry {
+  static parseSchema(schema, path) {
+    return new this(schema, path, schema.name,
+                    Schemas.parseSchema(schema, path,
+                      ["name", "unsupported", "returns",
+                       "permissions",
+                       "allowAmbiguousOptionalArguments"]),
+                    schema.unsupported || false,
+                    schema.allowAmbiguousOptionalArguments || false,
+                    schema.returns || null,
+                    schema.permissions || null);
+  }
+
   constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) {
     super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
     this.unsupported = unsupported;
     this.returns = returns;
     this.permissions = permissions;
 
     this.isAsync = type.isAsync;
     this.hasAsyncCallback = type.hasAsyncCallback;
   }
 
-  inject(path, name, dest, context) {
+  inject(path, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
-    exportLazyGetter(dest, name, () => {
-      let apiImpl = context.getImplementation(path.join("."), name);
+    exportLazyGetter(dest, this.name, () => {
+      let apiImpl = context.getImplementation(path.join("."), this.name);
 
       let stub;
       if (this.isAsync) {
         stub = (...args) => {
           this.checkDeprecated(context);
           let actuals = this.checkParameters(args, context);
           let callback = null;
           if (this.hasAsyncCallback) {
@@ -1706,46 +1733,65 @@ class FunctionEntry extends CallEntry {
           this.checkDeprecated(context);
           let actuals = this.checkParameters(args, context);
           return apiImpl.callFunction(actuals);
         };
       }
       return Cu.exportFunction(stub, dest);
     });
   }
-}
+};
 
 // Represents an "event" defined in a schema namespace.
 class Event extends CallEntry {
+  static parseSchema(event, path) {
+    let extraParameters = Array.from(event.extraParameters || [], param => ({
+      type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
+      name: param.name,
+      optional: param.optional || false,
+      default: param.default == undefined ? null : param.default,
+    }));
+
+    let extraProperties = ["name", "unsupported", "permissions", "extraParameters",
+                           // We ignore these properties for now.
+                           "returns", "filters"];
+
+    return new this(event, path, event.name,
+                    Schemas.parseSchema(event, path, extraProperties),
+                    extraParameters,
+                    event.unsupported || false,
+                    event.permissions || null);
+  }
+
   constructor(schema, path, name, type, extraParameters, unsupported, permissions) {
     super(schema, path, name, extraParameters);
     this.type = type;
     this.unsupported = unsupported;
     this.permissions = permissions;
   }
 
   checkListener(listener, context) {
     let r = this.type.normalize(listener, context);
     if (r.error) {
       this.throwError(context, "Invalid listener");
     }
     return r.value;
   }
 
-  inject(path, name, dest, context) {
+  inject(path, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
-    exportLazyGetter(dest, name, () => {
-      let apiImpl = context.getImplementation(path.join("."), name);
+    exportLazyGetter(dest, this.name, () => {
+      let apiImpl = context.getImplementation(path.join("."), this.name);
 
       let addStub = (listener, ...args) => {
         listener = this.checkListener(listener, context);
         let actuals = this.checkParameters(args, context);
         apiImpl.addListener(listener, actuals);
       };
 
       let removeStub = (listener) => {
@@ -1775,150 +1821,328 @@ const TYPES = Object.freeze(Object.assig
   boolean: BooleanType,
   function: FunctionType,
   integer: IntegerType,
   number: NumberType,
   object: ObjectType,
   string: StringType,
 }));
 
+const LOADERS = {
+  events: "loadEvent",
+  functions: "loadFunction",
+  properties: "loadProperty",
+  types: "loadType",
+};
+
+class Namespace extends Map {
+  constructor(name, path) {
+    super();
+
+    this._lazySchemas = [];
+
+    this.name = name;
+    this.path = name ? [...path, name] : [...path];
+
+    this.permissions = null;
+    this.allowedContexts = [];
+    this.defaultContexts = [];
+  }
+
+  /**
+   * Adds a JSON Schema object to the set of schemas that represent this
+   * namespace.
+   *
+   * @param {object} schema
+   *        A JSON schema object which partially describes this
+   *        namespace.
+   */
+  addSchema(schema) {
+    this._lazySchemas.push(schema);
+
+    for (let prop of ["permissions", "allowedContexts", "defaultContexts"]) {
+      if (schema[prop]) {
+        this[prop] = schema[prop];
+      }
+    }
+  }
+
+  /**
+   * Initializes the keys of this namespace based on the schema objects
+   * added via previous `addSchema` calls.
+   */
+  init() {
+    if (!this._lazySchemas) {
+      return;
+    }
+
+    for (let type of Object.keys(LOADERS)) {
+      this[type] = new DefaultMap(() => []);
+    }
+
+    for (let schema of this._lazySchemas) {
+      for (let type of schema.types || []) {
+        this.types.get(type.$extend || type.id).push(type);
+      }
+
+      for (let [name, prop] of Object.entries(schema.properties || {})) {
+        if (!prop.unsupported) {
+          this.properties.get(name).push(prop);
+        }
+      }
+
+      for (let fun of schema.functions || []) {
+        this.functions.get(fun.name).push(fun);
+      }
+
+      for (let event of schema.events || []) {
+        this.events.get(event.name).push(event);
+      }
+    }
+
+    // For each type of top-level property in the schema object, iterate
+    // over all properties of that type, and create a temporary key for
+    // each property pointing to its type. Those temporary properties
+    // are later used to instantiate an Entry object based on the actual
+    // schema object.
+    for (let type of Object.keys(LOADERS)) {
+      for (let key of this[type].keys()) {
+        this.set(key, type);
+      }
+    }
+
+    this._lazySchemas = null;
+
+    if (DEBUG) {
+      for (let [key, type] of this.entries()) {
+        this.initKey(key, type);
+      }
+    }
+  }
+
+  /**
+   * Initializes the value of a given key, by parsing the schema object
+   * associated with it and replacing its temporary value with an `Entry`
+   * instance.
+   *
+   * @param {string} key
+   *        The name of the property to initialize.
+   * @param {string} type
+   *        The type of property the key represents. Must have a
+   *        corresponding entry in the `LOADERS` object, pointing to the
+   *        initialization method for that type.
+   *
+   * @returns {Entry}
+   */
+  initKey(key, type) {
+    let loader = LOADERS[type];
+
+    for (let schema of this[type].get(key)) {
+      this.set(key, this[loader](key, schema));
+    }
+
+    return this.get(key);
+  }
+
+  loadType(name, type) {
+    if ("$extend" in type) {
+      return this.extendType(type);
+    }
+    return Schemas.parseSchema(type, this.path, ["id"]);
+  }
+
+  extendType(type) {
+    let targetType = this.get(type.$extend);
+
+    // Only allow extending object and choices types for now.
+    if (targetType instanceof ObjectType) {
+      type.type = "object";
+    } else if (DEBUG) {
+      if (!targetType) {
+        throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`);
+      } else if (!(targetType instanceof ChoiceType)) {
+        throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`);
+      }
+    }
+
+    let parsed = Schemas.parseSchema(type, this.path, ["$extend"]);
+
+    if (DEBUG && parsed.constructor !== targetType.constructor) {
+      throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
+    }
+
+    targetType.extend(parsed);
+
+    return targetType;
+  }
+
+  loadProperty(name, prop) {
+    if ("$ref" in prop) {
+      if (!prop.unsupported) {
+        return new SubModuleProperty(prop, this.path, name,
+                                     prop.$ref, prop.properties || {});
+      }
+    } else if ("value" in prop) {
+      return new ValueProperty(prop, name, prop.value);
+    } else {
+      // We ignore the "optional" attribute on properties since we
+      // don't inject anything here anyway.
+      let type = Schemas.parseSchema(prop, [this.name], ["optional", "writable"]);
+      return new TypeProperty(prop, this.path, name, type, prop.writable || false);
+    }
+  }
+
+  loadFunction(name, fun) {
+    return FunctionEntry.parseSchema(fun, this.path);
+  }
+
+  loadEvent(name, event) {
+    return Event.parseSchema(event, this.path);
+  }
+
+  /**
+   * Injects the properties of this namespace into the given object.
+   *
+   * @param {object} dest
+   *        The object into which to inject the namespace properties.
+   * @param {InjectionContext} context
+   *        The injection context with which to inject the properties.
+   */
+  injectInto(dest, context) {
+    for (let [name, entry] of this.entries()) {
+      if (entry.permissions && !entry.permissions.some(perm => context.hasPermission(perm))) {
+        continue;
+      }
+
+      let allowedContexts = entry.allowedContexts;
+      if (!allowedContexts.length) {
+        allowedContexts = this.defaultContexts;
+      }
+
+      if (context.shouldInject(this.path.join("."), name, allowedContexts)) {
+        entry.inject(this.path, dest, context);
+      }
+    }
+  }
+
+  inject(path, dest, context) {
+    exportLazyGetter(dest, this.name, () => {
+      let obj = Cu.createObjectIn(dest);
+
+      this.injectInto(obj, context);
+
+      // Only inject the namespace object if it isn't empty.
+      if (Object.keys(obj).length) {
+        return obj;
+      }
+    });
+  }
+
+  keys() {
+    this.init();
+    return super.keys();
+  }
+
+  * entries() {
+    for (let key of this.keys()) {
+      yield [key, this.get(key)];
+    }
+  }
+
+  get(key) {
+    this.init();
+    let value = super.get(key);
+
+    // The initial values of lazily-initialized schema properties are
+    // strings, pointing to the type of property, corresponding to one
+    // of the entries in the `LOADERS` object.
+    if (typeof value === "string") {
+      value = this.initKey(key, value);
+    }
+
+    return value;
+  }
+
+  /**
+   * Returns a Namespace object for the given namespace name. If a
+   * namespace object with this name does not already exist, it is
+   * created. If the name contains any '.' characters, namespaces are
+   * recursively created, for each dot-separated component.
+   *
+   * @param {string} name
+   *        The name of the sub-namespace to retrieve.
+   *
+   * @returns {Namespace}
+   */
+  getNamespace(name) {
+    let subName;
+
+    let idx = name.indexOf(".");
+    if (idx > 0) {
+      subName = name.slice(idx + 1);
+      name = name.slice(0, idx);
+    }
+
+    let ns = super.get(name);
+    if (!ns) {
+      ns = new Namespace(name, this.path);
+      this.set(name, ns);
+    }
+
+    if (subName) {
+      return ns.getNamespace(subName);
+    }
+    return ns;
+  }
+
+  has(key) {
+    this.init();
+    return super.has(key);
+  }
+}
+
 this.Schemas = {
   initialized: false,
 
   // Maps a schema URL to the JSON contained in that schema file. This
   // is useful for sending the JSON across processes.
   schemaJSON: new Map(),
 
   // Map[<schema-name> -> Map[<symbol-name> -> Entry]]
   // This keeps track of all the schemas that have been loaded so far.
-  namespaces: new Map(),
+  rootNamespace: new Namespace("", []),
 
-  register(namespaceName, symbol, value) {
-    let ns = this.namespaces.get(namespaceName);
-    if (!ns) {
-      ns = new Map();
-      ns.name = namespaceName;
-      ns.permissions = null;
-      ns.allowedContexts = [];
-      ns.defaultContexts = [];
-      this.namespaces.set(namespaceName, ns);
-    }
-    ns.set(symbol, value);
+  getNamespace(name) {
+    return this.rootNamespace.getNamespace(name);
   },
 
   parseSchema(schema, path, extraProperties = []) {
-    let allowedProperties = new Set(extraProperties);
+    let allowedProperties = DEBUG && new Set(extraProperties);
 
     if ("choices" in schema) {
       return ChoiceType.parseSchema(schema, path, allowedProperties);
     } else if ("$ref" in schema) {
       return RefType.parseSchema(schema, path, allowedProperties);
     }
 
-    if (!("type" in schema)) {
-      throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
-    }
-
-    allowedProperties.add("type");
+    let type = TYPES[schema.type];
 
-    let type = TYPES[schema.type];
-    if (!type) {
-      throw new Error(`Unexpected type ${schema.type}`);
-    }
-    return type.parseSchema(schema, path, allowedProperties);
-  },
+    if (DEBUG) {
+      allowedProperties.add("type");
 
-  parseFunction(path, fun) {
-    let f = new FunctionEntry(fun, path, fun.name,
-                              this.parseSchema(fun, path,
-                                ["name", "unsupported", "returns",
-                                 "permissions",
-                                 "allowAmbiguousOptionalArguments"]),
-                              fun.unsupported || false,
-                              fun.allowAmbiguousOptionalArguments || false,
-                              fun.returns || null,
-                              fun.permissions || null);
-    return f;
-  },
+      if (!("type" in schema)) {
+        throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
+      }
 
-  loadType(namespaceName, type) {
-    if ("$extend" in type) {
-      this.extendType(namespaceName, type);
-    } else {
-      this.register(namespaceName, type.id, this.parseSchema(type, [namespaceName], ["id"]));
-    }
-  },
-
-  extendType(namespaceName, type) {
-    let ns = Schemas.namespaces.get(namespaceName);
-    let targetType = ns && ns.get(type.$extend);
-
-    // Only allow extending object and choices types for now.
-    if (targetType instanceof ObjectType) {
-      type.type = "object";
-    } else if (!targetType) {
-      throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`);
-    } else if (!(targetType instanceof ChoiceType)) {
-      throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`);
+      if (!type) {
+        throw new Error(`Unexpected type ${schema.type}`);
+      }
     }
 
-    let parsed = this.parseSchema(type, [namespaceName], ["$extend"]);
-    if (parsed.constructor !== targetType.constructor) {
-      throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
-    }
-
-    targetType.extend(parsed);
-  },
-
-  loadProperty(namespaceName, name, prop) {
-    if ("$ref" in prop) {
-      if (!prop.unsupported) {
-        this.register(namespaceName, name, new SubModuleProperty(prop, name, namespaceName, prop.$ref,
-                                                                 prop.properties || {}));
-      }
-    } else if ("value" in prop) {
-      this.register(namespaceName, name, new ValueProperty(prop, name, prop.value));
-    } else {
-      // We ignore the "optional" attribute on properties since we
-      // don't inject anything here anyway.
-      let type = this.parseSchema(prop, [namespaceName], ["optional", "writable"]);
-      this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable || false));
-    }
-  },
-
-  loadFunction(namespaceName, fun) {
-    let f = this.parseFunction([namespaceName], fun);
-    this.register(namespaceName, fun.name, f);
-  },
-
-  loadEvent(namespaceName, event) {
-    let extras = event.extraParameters || [];
-    extras = extras.map(param => {
-      return {
-        type: this.parseSchema(param, [namespaceName], ["name", "optional", "default"]),
-        name: param.name,
-        optional: param.optional || false,
-        default: param.default == undefined ? null : param.default,
-      };
-    });
-
-    // We ignore these properties for now.
-    /* eslint-disable no-unused-vars */
-    let returns = event.returns;
-    let filters = event.filters;
-    /* eslint-enable no-unused-vars */
-
-    let type = this.parseSchema(event, [namespaceName],
-      ["name", "unsupported", "permissions",
-       "extraParameters", "returns", "filters"]);
-
-    let e = new Event(event, [namespaceName], event.name, type, extras,
-                      event.unsupported || false,
-                      event.permissions || null);
-    this.register(namespaceName, event.name, e);
+    return type.parseSchema(schema, path, allowedProperties);
   },
 
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
@@ -1944,66 +2168,42 @@ this.Schemas = {
       case "Schema:Delete":
         this.schemaJSON.delete(msg.data.url);
         this.flushSchemas();
         break;
     }
   },
 
   flushSchemas() {
-    XPCOMUtils.defineLazyGetter(this, "namespaces",
+    XPCOMUtils.defineLazyGetter(this, "rootNamespace",
                                 () => this.parseSchemas());
   },
 
   parseSchemas() {
-    Object.defineProperty(this, "namespaces", {
+    Object.defineProperty(this, "rootNamespace", {
       enumerable: true,
       configurable: true,
-      value: new Map(),
+      value: new Namespace("", []),
     });
 
     for (let json of this.schemaJSON.values()) {
       try {
         this.loadSchema(json);
       } catch (e) {
         Cu.reportError(e);
       }
     }
 
-    return this.namespaces;
+    return this.rootNamespace;
   },
 
   loadSchema(json) {
     for (let namespace of json) {
-      let name = namespace.namespace;
-
-      let types = namespace.types || [];
-      for (let type of types) {
-        this.loadType(name, type);
-      }
-
-      let properties = namespace.properties || {};
-      for (let propertyName of Object.keys(properties)) {
-        this.loadProperty(name, propertyName, properties[propertyName]);
-      }
-
-      let functions = namespace.functions || [];
-      for (let fun of functions) {
-        this.loadFunction(name, fun);
-      }
-
-      let events = namespace.events || [];
-      for (let event of events) {
-        this.loadEvent(name, event);
-      }
-
-      let ns = this.namespaces.get(name);
-      ns.permissions = namespace.permissions || null;
-      ns.allowedContexts = namespace.allowedContexts || [];
-      ns.defaultContexts = namespace.defaultContexts || [];
+      this.getNamespace(namespace.namespace)
+          .addSchema(namespace);
     }
   },
 
   load(url) {
     if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
       return readJSON(url).then(json => {
         this.schemaJSON.set(url, json);
 
@@ -2038,17 +2238,17 @@ this.Schemas = {
    *        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.namespaces.get(namespace);
+    let ns = this.getNamespace(namespace);
     if (ns && ns.permissions) {
       return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
     }
     return true;
   },
 
   exportLazyGetter,
 
@@ -2058,86 +2258,28 @@ this.Schemas = {
    * @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);
 
-    let createNamespace = ns => {
-      let obj = Cu.createObjectIn(dest);
-
-      for (let [name, entry] of ns) {
-        let allowedContexts = entry.allowedContexts;
-        if (!allowedContexts.length) {
-          allowedContexts = ns.defaultContexts;
-        }
-
-        if (context.shouldInject(ns.name, name, allowedContexts)) {
-          entry.inject([ns.name], name, obj, context);
-        }
-      }
-
-      // Remove the namespace object if it is empty
-      if (Object.keys(obj).length) {
-        return obj;
-      }
-    };
-
-    let createNestedNamespaces = (parent, namespaces) => {
-      for (let [prop, namespace] of namespaces) {
-        if (namespace instanceof DeepMap) {
-          exportLazyGetter(parent, prop, () => {
-            let obj = Cu.createObjectIn(parent);
-            createNestedNamespaces(obj, namespace);
-            return obj;
-          });
-        } else {
-          exportLazyGetter(parent, prop,
-                           () => createNamespace(namespace));
-        }
-      }
-    };
-
-    let nestedNamespaces = new DeepMap();
-    for (let ns of this.namespaces.values()) {
-      if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
-        continue;
-      }
-
-      if (!wrapperFuncs.shouldInject(ns.name, null, ns.allowedContexts)) {
-        continue;
-      }
-
-      if (ns.name.includes(".")) {
-        let path = ns.name.split(".");
-        let leafName = path.pop();
-
-        let parent = nestedNamespaces.getPath(...path);
-
-        parent.set(leafName, ns);
-      } else {
-        exportLazyGetter(dest, ns.name,
-                         () => createNamespace(ns));
-      }
-    }
-
-    createNestedNamespaces(dest, nestedNamespaces);
+    this.rootNamespace.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.namespaces.get(namespaceName);
+    let ns = this.getNamespace(namespaceName);
     let type = ns.get(prop);
 
     return type.normalize(obj, new Context(context));
   },
 };