Bug 1333403 - Part 1: Implement $import keyword for schema namespaces draft
authorTomislav Jovanovic <tomica@gmail.com>
Wed, 07 Jun 2017 16:44:19 +0200
changeset 592122 c2ccd2821f7a73bf683f366d81ede9819613c982
parent 592117 b7e57100a79cc71dba99280b3a2b4aba719d8565
child 592123 e0e2b1864f74df220260e573ef90b4dfa6ebf820
push id63281
push userbmo:tomica@gmail.com
push dateSat, 10 Jun 2017 14:46:45 +0000
bugs1333403
milestone55.0a1
Bug 1333403 - Part 1: Implement $import keyword for schema namespaces MozReview-Commit-ID: 5CYsQa3b05C
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -2214,20 +2214,23 @@ const LOADERS = {
   types: "loadType",
 };
 
 class Namespace extends Map {
   constructor(name, path) {
     super();
 
     this._lazySchemas = [];
+    this.initialized = false;
 
     this.name = name;
     this.path = name ? [...path, name] : [...path];
 
+    this.superNamespace = null;
+
     this.permissions = null;
     this.allowedContexts = [];
     this.defaultContexts = [];
   }
 
   /**
    * Adds a JSON Schema object to the set of schemas that represent this
    * namespace.
@@ -2239,27 +2242,35 @@ class Namespace extends Map {
   addSchema(schema) {
     this._lazySchemas.push(schema);
 
     for (let prop of ["permissions", "allowedContexts", "defaultContexts"]) {
       if (schema[prop]) {
         this[prop] = schema[prop];
       }
     }
+
+    if (schema.$import) {
+      this.superNamespace = Schemas.getNamespace(schema.$import);
+    }
   }
 
   /**
    * Initializes the keys of this namespace based on the schema objects
    * added via previous `addSchema` calls.
    */
-  init() { // eslint-disable-line complexity
-    if (!this._lazySchemas) {
+  init() {
+    if (this.initialized) {
       return;
     }
 
+    if (this.superNamespace) {
+      this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
+    }
+
     for (let type of Object.keys(LOADERS)) {
       this[type] = new DefaultMap(() => []);
     }
 
     for (let schema of this._lazySchemas) {
       for (let type of schema.types || []) {
         if (!type.unsupported) {
           this.types.get(type.$extend || type.id).push(type);
@@ -2291,17 +2302,17 @@ class Namespace extends Map {
     // 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;
+    this.initialized = true;
 
     if (DEBUG) {
       for (let key of this.keys()) {
         this.get(key);
       }
     }
   }
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -1294,16 +1294,94 @@ add_task(async function testNestedNamesp
   //    "Got the expected property defined in the CustomType instance)
   //
   // ok(instanceOfCustomType.onEvent &&
   //    instanceOfCustomType.onEvent.addListener &&
   //    typeof instanceOfCustomType.onEvent.addListener == "function",
   //    "Got the expected event defined in the CustomType instance");
 });
 
+let $importJson = [
+  {
+    namespace: "from_the",
+    $import: "future",
+  },
+  {
+    namespace: "future",
+    properties: {
+      PROP1: {value: "original value"},
+      PROP2: {value: "second original"},
+    },
+    types: [
+      {
+        id: "Colour",
+        type: "string",
+        enum: ["red", "white", "blue"],
+      },
+    ],
+    functions: [
+      {
+        name: "dye",
+        type: "function",
+        parameters: [
+          {name: "arg", $ref: "Colour"},
+        ],
+      },
+    ],
+  },
+  {
+    namespace: "embrace",
+    $import: "future",
+    properties: {
+      PROP2: {value: "overridden value"},
+    },
+    types: [
+      {
+        id: "Colour",
+        type: "string",
+        enum: ["blue", "orange"],
+      },
+    ],
+  },
+];
+
+add_task(async function test_$import() {
+  let url = "data:," + JSON.stringify($importJson);
+  await Schemas.load(url);
+
+  let root = {};
+  tallied = null;
+  Schemas.inject(root, wrapper);
+  equal(tallied, null);
+
+  equal(root.from_the.PROP1, "original value", "imported property");
+  equal(root.from_the.PROP2, "second original", "second imported property");
+  equal(root.from_the.Colour.RED, "red", "imported enum type");
+  equal(typeof root.from_the.dye, "function", "imported function");
+
+  root.from_the.dye("white");
+  verify("call", "from_the", "dye", ["white"]);
+
+  Assert.throws(() => root.from_the.dye("orange"),
+                /Invalid enumeration value/,
+                "original imported argument type Colour doesn't include 'orange'");
+
+  equal(root.embrace.PROP1, "original value", "imported property");
+  equal(root.embrace.PROP2, "overridden value", "overridden property");
+  equal(root.embrace.Colour.ORANGE, "orange", "overridden enum type");
+  equal(typeof root.embrace.dye, "function", "imported function");
+
+  root.embrace.dye("orange");
+  verify("call", "embrace", "dye", ["orange"]);
+
+  Assert.throws(() => root.embrace.dye("white"),
+                /Invalid enumeration value/,
+                "overridden argument type Colour doesn't include 'white'");
+});
+
 add_task(async function testLocalAPIImplementation() {
   let countGet2 = 0;
   let countProp3 = 0;
   let countProp3SubFoo = 0;
 
   let testingApiObj = {
     get PROP1() {
       // PROP1 is a schema-defined constant.