[gjs/wip/gobj-kitchen-sink: 1/20] lang: Introduce meta classes



commit 5c28c56f8defdfd97f18255bf506e3556579dc48
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Wed Dec 7 22:36:13 2011 +0100

    lang: Introduce meta classes
    
    Classes created with new Lang.Class are now first class objects
    themselves, and Lang.Class is itself a Lang.Class (to the extent
    permitted by JS). It is therefore possible to inherit from Lang.Class
    and create custom python-like metaclasses, instances of which are
    actual instantiable classes.

 modules/lang.js          |  106 +++++++++++++++++++++++++++++++-------
 test/js/testClass.js     |   17 ++++++
 test/js/testMetaClass.js |  126 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 229 insertions(+), 20 deletions(-)
---
diff --git a/modules/lang.js b/modules/lang.js
index d6903f8..b44b0e2 100644
--- a/modules/lang.js
+++ b/modules/lang.js
@@ -134,13 +134,19 @@ function defineAccessorProperty(object, name, getter, setter) {
 // https://github.com/mootools/moootools-core
 
 function _Base() {
+    throw new TypeError('Cannot instantiate abstract class _Base');
 }
 
+_Base.__super__ = null;
 _Base.prototype._init = function() { };
+_Base.prototype._construct = function() {
+    this._init.apply(this, arguments);
+    return this;
+};
 _Base.prototype.__name__ = '_Base';
 _Base.prototype.toString = function() {
     return '[object ' + this.__name__ + ']';
-}
+};
 
 function _parent() {
     if (!this.__caller__)
@@ -158,7 +164,36 @@ function _parent() {
     return previous.apply(this, arguments);
 }
 
-function wrapFunction(obj, name, meth) {
+function getMetaClass(params) {
+    if (params.MetaClass)
+        return params.MetaClass;
+
+    if (params.Extends && params.Extends.prototype.__metaclass__)
+        return params.Extends.prototype.__metaclass__;
+
+    return null;
+}
+
+function Class(params) {
+    let metaClass = getMetaClass(params);
+
+    if (metaClass && metaClass != this.constructor) {
+        // Trick to apply variadic arguments to constructors --
+        // bind the arguments into the constructor function.
+        let args = Array.prototype.slice.call(arguments);
+        let curried = Function.prototype.bind.apply(metaClass, [,].concat(args));
+        return new curried();
+    } else {
+        return this._construct.apply(this, arguments);
+    }
+}
+
+Class.__super__ = _Base;
+Class.prototype = Object.create(_Base.prototype);
+Class.prototype.constructor = Class;
+Class.prototype.__name__ = 'Class';
+
+Class.prototype.wrapFunction = function(name, meth) {
     if (meth._origin) meth = meth._origin;
 
     function wrapper() {
@@ -171,17 +206,25 @@ function wrapFunction(obj, name, meth) {
 
     wrapper._origin = meth;
     wrapper._name = name;
-    wrapper._owner = obj;
+    wrapper._owner = this;
 
     return wrapper;
 }
 
-function Class(params) {
+Class.prototype.toString = function() {
+    return '[object ' + this.__name__ + ' for ' + this.prototype.__name__ + ']';
+};
+
+Class.prototype._construct = function(params) {
     if (!params.Name) {
         throw new TypeError("Classes require an explicit 'Name' parameter.");
     }
     let name = params.Name;
 
+    let parent = params.Extends;
+    if (!parent)
+        parent = _Base;
+
     let newClass;
     if (params.Abstract) {
         newClass = function() {
@@ -191,23 +234,43 @@ function Class(params) {
         newClass = function() {
             this.__caller__ = null;
 
-            return this._init.apply(this, arguments);
+            return this._construct.apply(this, arguments);
         };
     }
 
-    let parent = params.Extends;
-    if (!parent)
-        parent = _Base;
+    // Since it's not possible to create a constructor with
+    // a custom [[Prototype]], we have to do this to make
+    // "newClass instanceof Class" work, and so we can inherit
+    // methods/properties of Class.prototype, like wrapFunction.
+    newClass.__proto__ = this.constructor.prototype;
+
+    newClass.__super__ = parent;
+    newClass.prototype = Object.create(parent.prototype);
+    newClass.prototype.constructor = newClass;
+
+    newClass._init.apply(newClass, arguments);
+
+    Object.defineProperty(newClass.prototype, '__metaclass__',
+                          { writable: false,
+                            configurable: false,
+                            enumerable: false,
+                            value: this.constructor });
+
+    return newClass;
+};
+
+Class.prototype._init = function(params) {
+    let name = params.Name;
 
     let propertyObj = { };
-    let propertyDescriptors = Object.getOwnPropertyNames(params).forEach(function(name) {
+    Object.getOwnPropertyNames(params).forEach(function(name) {
         if (name == 'Name' || name == 'Extends' || name == 'Abstract')
             return;
 
         let descriptor = Object.getOwnPropertyDescriptor(params, name);
 
         if (typeof descriptor.value === 'function')
-            descriptor.value = wrapFunction(newClass, name, descriptor.value);
+            descriptor.value = this.wrapFunction(name, descriptor.value);
 
         // we inherit writable and enumerable from the property
         // descriptor of params (they're both true if created from an
@@ -215,16 +278,19 @@ function Class(params) {
         descriptor.configurable = false;
 
         propertyObj[name] = descriptor;
-    });
-
-    newClass.__super__ = parent;
-    newClass.prototype = Object.create(parent.prototype, propertyObj);
-    newClass.prototype.constructor = newClass;
-    newClass.prototype.__name__ = name;
-    newClass.prototype.parent = _parent;
-
-    return newClass;
-}
+    }.bind(this));
+
+    Object.defineProperties(this.prototype, propertyObj);
+    Object.defineProperties(this.prototype, {
+        '__name__': { writable: false,
+                      configurable: false,
+                      enumerable: false,
+                      value: name },
+        'parent': { writable: false,
+                    configurable: false,
+                    enumerable: false,
+                    value: _parent }});
+};
 
 // Merge stuff defined in native code
 copyProperties(imports.langNative, this);
diff --git a/test/js/testClass.js b/test/js/testClass.js
index f61de3f..14d8aaf 100644
--- a/test/js/testClass.js
+++ b/test/js/testClass.js
@@ -103,6 +103,14 @@ const AbstractImpl2 = new Lang.Class({
     // no _init here, we inherit the parent one
 });
 
+const CustomConstruct = new Lang.Class({
+    Name: 'CustomConstruct',
+
+    _construct: function(one, two) {
+        return [one, two];
+    }
+});
+
 function testClassFramework() {
     let newMagic = new MagicBase('A');
     assertEquals('A',  newMagic.a);
@@ -185,4 +193,13 @@ function testCrossCall() {
     assertEquals(50, res);
 }
 
+function testConstruct() {
+    let instance = new CustomConstruct(1, 2);
+
+    assertTrue(instance instanceof Array);
+    assertTrue(!(instance instanceof CustomConstruct));
+
+    assertArrayEquals([1, 2], instance);
+}
+
 gjstestRun();
diff --git a/test/js/testMetaClass.js b/test/js/testMetaClass.js
new file mode 100644
index 0000000..fd2df91
--- /dev/null
+++ b/test/js/testMetaClass.js
@@ -0,0 +1,126 @@
+// application/javascript;version=1.8 -*- mode: js; indent-tabs-mode: nil -*-
+
+if (!('assertEquals' in this)) { /* allow running this test standalone */
+    imports.lang.copyPublicProperties(imports.jsUnit, this);
+    gjstestRun = function() { return imports.jsUnit.gjstestRun(window); };
+}
+
+const Lang = imports.lang;
+
+function assertArrayEquals(expected, got) {
+    assertEquals(expected.length, got.length);
+    for (let i = 0; i < expected.length; i ++) {
+        assertEquals(expected[i], got[i]);
+    }
+}
+
+const NormalClass = new Lang.Class({
+    Name: 'NormalClass',
+
+    _init: function() {
+        this.one = 1;
+    }
+});
+
+let Subclassed = [];
+const MetaClass = new Lang.Class({
+    Name: 'MetaClass',
+    Extends: Lang.Class,
+
+    _init: function(params) {
+        Subclassed.push(params.Name);
+        this.parent(params);
+
+        if (params.Extended) {
+            this.prototype.dynamic_method = this.wrapFunction('dynamic_method', function() {
+                return 73;
+            });
+
+            this.DYNAMIC_CONSTANT = 2;
+        }
+    }
+});
+
+const CustomMetaOne = new MetaClass({
+    Name: 'CustomMetaOne',
+    Extends: NormalClass,
+    Extended: false,
+
+    _init: function() {
+        this.parent();
+
+        this.two = 2;
+    }
+});
+
+const CustomMetaTwo = new MetaClass({
+    Name: 'CustomMetaTwo',
+    Extends: NormalClass,
+    Extended: true,
+
+    _init: function() {
+        this.parent();
+
+        this.two = 2;
+    }
+});
+
+// This should inherit CustomMeta, even though
+// we use Lang.Class
+const CustomMetaSubclass = new Lang.Class({
+    Name: 'CustomMetaSubclass',
+    Extends: CustomMetaOne,
+    Extended: true,
+
+    _init: function() {
+        this.parent();
+
+        this.three = 3;
+    }
+});
+
+function testMetaClass() {
+    assertArrayEquals(['CustomMetaOne',
+                       'CustomMetaTwo',
+                       'CustomMetaSubclass'], Subclassed);
+
+    assertTrue(NormalClass instanceof Lang.Class);
+    assertTrue(MetaClass instanceof Lang.Class);
+
+    assertTrue(CustomMetaOne instanceof Lang.Class);
+    assertTrue(CustomMetaOne instanceof MetaClass);
+
+    assertEquals(2, CustomMetaTwo.DYNAMIC_CONSTANT);
+    assertUndefined(CustomMetaOne.DYNAMIC_CONSTANT);
+}
+
+function testMetaInstance() {
+    let instanceOne = new CustomMetaOne();
+
+    assertEquals(1, instanceOne.one);
+    assertEquals(2, instanceOne.two);
+
+    assertRaises(function() {
+        instanceOne.dynamic_method();
+    });
+
+    let instanceTwo = new CustomMetaTwo();
+    assertEquals(1, instanceTwo.one);
+    assertEquals(2, instanceTwo.two);
+    assertEquals(73, instanceTwo.dynamic_method());
+}
+
+function testMetaSubclass() {
+    assertTrue(CustomMetaSubclass instanceof MetaClass);
+
+    let instance = new CustomMetaSubclass();
+
+    assertEquals(1, instance.one);
+    assertEquals(2, instance.two);
+    assertEquals(3, instance.three);
+
+    assertEquals(73, instance.dynamic_method());
+    assertEquals(2, CustomMetaSubclass.DYNAMIC_CONSTANT);
+}
+
+gjstestRun();



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]