UNPKG

joii

Version:

Javascript Object Inheritance Implementation

599 lines (472 loc) 24.1 kB
/* Javascript Object Inheritance Implementation ______ ________ * (c) 2016 <harold@iedema.me> __ / / __ \/ _/ _/ * Licensed under MIT. / // / /_/ // /_/ / * ------------------------------------------------------ \___/\____/___/__*/ var JOII = require('../../dist/joii').JOII; /** * Tests function overloading */ test('ClassBuilder:FunctionOverloadingTests', function(assert) { try { // *************************** // Abstract tests // *************************** // An abstract property must also have a functional one in the same class. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { } }); new a(); }, function(err) { return err === 'Missing abstract member implementation of test(string)'; }, 'Validate: Missing implementation of abstract properties.'); // An abstract property must be implemented by a child class. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, {}); new b(); }, function(err) { return err === 'Missing abstract member implementation of test(string)'; }, 'Validate: Missing implementation of abstract properties.'); // A final property cannot be implemented by a child class. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'final public function test(string)': function() { }, 'public function test(string, num)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'public function test(string)': function() { } }); new b(); }, function(err) { return err === 'Final member "test(string)" cannot be overwritten.'; }, 'Validate: Missing implementation of abstract properties.'); // A final property cannot be implemented by a child class. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'final public function test(string)': function() { }, 'final public function test(string, num)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'final public function test(string)': function() { } }); new b(); }, function(err) { return err === 'Final member "test(string)" cannot be overwritten.'; }, 'Validate: Missing implementation of abstract properties.'); // This shouldn't throw an error var a = JOII.ClassBuilder({}, { 'public function test(string)': function() { }, 'final public function test(string, num)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'final public function test(string)': function() { } }); new b(); // This shouldn't throw an error var a = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { }, 'public function test(string, num)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'final public function test(string)': function() { } }); new b(); // This shouldn't throw an error var a = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { }, 'final public function test(string, num)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'public function test(string)': function() { } }); new b(); // An abstract property must be implemented by a child class. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'abstract public function test(number)': function() { }, 'abstract public function test(string)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'public function test(string)': function() { } }); new b(); }, function(err) { return err === 'Missing abstract member implementation of test(number)'; }, 'Validate: Missing implementation of abstract properties.'); // An abstract property must be implemented by a child class. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'abstract public function test(number)': function() { }, 'abstract public function test(string)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'public function test(number)': function() { } }); new b(); }, function(err) { return err === 'Missing abstract member implementation of test(string)'; }, 'Validate: Missing implementation of abstract properties (different order).'); // Visibility of an abstract property may not change. assert.throws(function() { var a = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'protected function test(string)': function() { } }); new b(); }, function(err) { return err === 'Member "test" must be public as defined in the parent class.'; }, 'Validate: Visibility change of abstract implementation.'); // This should _not_ throw an exception. var a = JOII.ClassBuilder({}, { 'abstract public function test(number)': function() { }, 'abstract public function test(string)': function() { } }); var b = JOII.ClassBuilder({ 'extends': a }, { 'public function test(number)': function() { }, 'public function test(string)': function() { } }); new b(); // Defining an abstract and functional property in the same class is valid. (beware of declaration order) var c = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { }, 'public function test(string)': function() { } }); new c(); // Implement abstract property in the middle of the inheritance chain. var c1 = JOII.ClassBuilder({}, { 'abstract public function test(string)': function() { } }); var c2 = JOII.ClassBuilder({ 'extends': c1 }, {}); var c3 = JOII.ClassBuilder({ 'extends': c2 }, { 'public function test(string)': function() { } }); var c4 = JOII.ClassBuilder({ 'extends': c3 }, {}); // Should not throw exception for missing an abstract implementation. new c4(); // *************************** // Call tests // *************************** var C1 = JOII.ClassBuilder({ '__call(number)': function(p) { return 1 + p; }, '__call(string)': function(p) { return p + " passed"; } }); assert.strictEqual(C1(1), 2, '__call number overload function OK.'); assert.strictEqual(C1("test"), "test passed", '__call string overload function OK.'); // __call may not return "this", because it references to the static // definition of the class body. assert.throws(function() { var a = JOII.ClassBuilder({ 'public function foo': function() { }, '__call(number)': function() { return this; } }); a(1); }, function(err) { return err === '__call cannot return itself.'; }, '__call cannot return itself.'); // Test the context of a static call. var C2 = JOII.ClassBuilder({ a: 1, b: "test", '__call(number)': function(val) { this.a = val; }, '__call(string)': function(val) { this.b = val; } , '__call()': function() { return this.a; } }); var c2 = new C2(); assert.strictEqual(c2.getA(), 1, 'c2.getA() returns this.a (1)'); assert.strictEqual(c2.getB(), "test", 'c2.getB() returns this.b ("test")'); assert.strictEqual(C2(), 1, '__call returns this.a (1)'); // Update the value, THEN create another instance and check again... C2(2); C2("test again"); var c2a = new C2(); assert.strictEqual(C2(), 2, '__call returns this.a (2)'); assert.strictEqual(c2.getA(), 1, 'c2.getA() returns this.a (1)'); assert.strictEqual(c2a.getA(), 1, 'c2a.getA() returns this.a (1)'); assert.strictEqual(c2.getB(), "test", 'c2.getB() returns this.b ("test")'); assert.strictEqual(c2a.getB(), "test", 'c2a.getB() returns this.b ("test")'); // 3.1.0: Custom callable method names. var C3 = Class({ '<>(number)': function(val) { return val + 1; }, '<>(string)': function(val) { return val + " passed"; } }); assert.strictEqual(C3(4), 5, 'New default call number overload method "<>" used.'); assert.strictEqual(C3("test"), "test passed", 'New default call string overload method "<>" used.'); JOII.Config.addCallable('execute'); var C4 = Class({ 'execute(number)': function(val) { return val + 1; }, 'execute(string)': function(val) { return val + " passed"; } }); assert.strictEqual(C4(4), 5, 'Custom call number overload method "execute" used.'); assert.strictEqual(C4("test"), "test passed", 'Custom call string overload method "execute" used.'); JOII.Config.removeCallable('execute'); // *************************** // General overloading tests // *************************** var UniqueTestClass = JOII.ClassBuilder('UniqueTestClass', { x: 6, y: "something else" }); var SuperClass = JOII.ClassBuilder('SuperClass', { a: 1, b: "test", 'test(number)': function(val) { this.a = val; }, 'test(string)': function(val) { this.b = val; }, 'test(UniqueTestClass)': function(val) { this.a = val.getX(); this.b = val.getY(); }, 'test(object)': function(val) { if ('a' in val) { this.a = val.a; } if ('b' in val) { this.b = val.b; } } }); var sc = new SuperClass(); sc.test(2); assert.strictEqual(sc.getA(), 2, 'sc.getA() returns this.a (2)'); sc.test("success"); assert.strictEqual(sc.getB(), "success", 'sc.getB() returns this.b ("success")'); sc.test({ a: 5, b: "passed" }); assert.strictEqual(sc.getA(), 5, 'sc.getA() returns this.a (5)'); assert.strictEqual(sc.getB(), "passed", 'sc.getB() returns this.b ("passed")'); var testClass = new UniqueTestClass(); sc.test(testClass); assert.strictEqual(sc.getA(), 6, 'sc.getA() returns this.a (6)'); assert.strictEqual(sc.getB(), "something else", 'sc.getB() returns this.b ("something else")'); var SubClass1 = JOII.ClassBuilder('SubClass1', { 'extends': SuperClass }, { 'test(number)': function(val) { this.a = val * 2; }, 'test(number, string)': function(num, str) { this.a = num * 4; this.b = str + " passed"; }, 'test(string, number)': function(str, num) { this.test(num); this.test(str); } }); var subc = new SubClass1(); // first, test the methods that were inherited and not overridden subc.test("success"); assert.strictEqual(subc.getB(), "success", 'subc.getB() returns this.b ("success")'); subc.test({ a: 5, b: "passed" }); assert.strictEqual(subc.getA(), 5, 'subc.getA() returns this.a (5)'); assert.strictEqual(subc.getB(), "passed", 'subc.getB() returns this.b ("passed")'); // test override subc.test(5); assert.strictEqual(subc.getA(), 10, 'subc.getA() returns this.a (10)'); // test added overload subc.test(10, "testing"); assert.strictEqual(subc.getA(), 40, 'subc.getA() returns this.a (40)'); assert.strictEqual(subc.getB(), "testing passed", 'subc.getB() returns this.b ("testing passed")'); // test overload calling different overloads, from different levels of inheritance subc.test("asdf", 100); assert.strictEqual(subc.getA(), 200, 'subc.getA() returns this.a (200)'); assert.strictEqual(subc.getB(), "asdf", 'subc.getB() returns this.b ("asdf")'); var CustomClass = JOII.ClassBuilder('CustomClass', { 'SubClass1 custom': null, 'construct(number)': function(num) { this.custom = new SubClass1(); this.custom.test(num); }, 'construct(string)': function(str) { this.custom = new SubClass1(); this.custom.test(str); }, 'construct(number, string)': function(num, str) { this.custom = new SubClass1(); this.custom.test(num, str); }, 'test(number)': function(num) { this.custom.test(num); }, 'test(string)': function(str) { this.custom.test(str); }, 'test(SubClass1)': function(val) { this.custom = val; } }); var custom1 = new CustomClass(5); assert.strictEqual(custom1.getCustom().getA(), 10, 'custom1.getCustom().getA() returns this.custom.a (10)'); var custom2 = new CustomClass("success"); assert.strictEqual(custom2.getCustom().getB(), "success", 'custom1.getCustom().getB() returns this.custom.a ("success")'); subc.test("asdf", 100); custom2.test(subc); assert.strictEqual(custom2.getCustom().getA(), 200, 'custom2.getCustom().getA() returns this.custom.a (200)'); assert.strictEqual(custom2.getCustom().getB(), "asdf", 'custom2.getCustom().getB() returns this.custom.b ("asdf")'); assert.throws(function() { custom2.test(1, 2, 3, 4); }, function(err) { return err === 'Couldn\'t find a function handler to match: test(number, number, number, number).'; }, 'Validate: No matching overload.'); assert.throws(function() { var c5 = JOII.ClassBuilder({ 'construct( number )': function(num) { }, 'construct(number)': function(num) { } }); new c5(); }, function(err) { return err === 'Member "construct(number)" is defined twice.'; }, 'Validate: Duplicate function overload.'); assert.throws(function() { var c6 = JOII.ClassBuilder({ 'public test(number)': function(num) { }, 'private test(number)': function(num) { } }); new c6(); }, function(err) { return err === 'Member test: inconsistent visibility.'; }, 'Validate: Consistent visibility of overloads.'); assert.throws(function() { var c7 = JOII.ClassBuilder({ 'test(number)': function(num) { }, 'test(str)': 'invalid' }); new c7(); }, function(err) { return err === 'Member test specifies parameters, but it\'s value isn\'t a function.'; }, 'Validate: Function body.'); assert.throws(function() { var c8 = JOII.ClassBuilder({ 'test(number)': function(num) { }, 'test': 'invalid' }); new c8(); }, function(err) { return err === 'Member test overloads an existing function, but it\'s value isn\'t a function.'; }, 'Validate: Consistent overloads.'); assert.throws(function() { var c9 = JOII.ClassBuilder({ 'test': 'invalid', 'test(number)': function(num) { } }); new c9(); }, function(err) { return err === 'Member test overloads an existing property, but the previous property isn\'t a function.'; }, 'Validate: Consistent overloads (reverse order).'); // *************************** // Variadic overloading tests // *************************** var C10 = JOII.ClassBuilder({ 'variadic(number)': function(num) { return "variadic (number): " + num; }, 'variadic(number, string)': function(num, str) { return "variadic (number, string): " + str + ": " + num; }, 'variadic(number, ...)': function(num, args) { return "variadic (number, ...): " + num + ' = ' + args.join(', '); }, 'variadic(number, string, ...)': function(num, str, args) { return "variadic (number, string, ...): " + str + ": " + num + ' = ' + args.join(', '); } }); var c10 = new C10(); assert.strictEqual(c10.variadic(1), "variadic (number): 1", 'c10.variadic(1) returns variadic (number): 1'); assert.strictEqual(c10.variadic(1, "Test"), "variadic (number, string): Test: 1", 'c10.variadic(1, "Test") returns variadic (number, string): Test: 1'); assert.strictEqual(c10.variadic(1, 2), "variadic (number, ...): 1 = 2", 'c10.variadic(1, 2) returns "variadic (number, ...): 1 = 2"'); assert.strictEqual(c10.variadic(1, 2, "asdf"), "variadic (number, ...): 1 = 2, asdf", 'c10.variadic(1, 2, "asdf") returns "variadic (number, ...): 1 = 2, asdf"'); assert.strictEqual(c10.variadic(1, "asdf", 2, 3), "variadic (number, string, ...): asdf: 1 = 2, 3", 'c10.variadic(1, "asdf", 2, 3) returns "variadic (number, string, ...): asdf: 1 = 2, 3"'); assert.throws(function() { var c11 = JOII.ClassBuilder({ 'variadic(string, ..., string)': function(num, str, args) { } }); new c11(); }, function(err) { return err === 'Member variadic: Variadic parameter (...) must be the last in the function parameter list.'; }, 'Validate: Variadic last function parameter.'); var ValueClass = Class('ValueClass', { id: 6, value: "", 'construct()': function() { }, 'construct(number, string)': function(id, val) { this.id = id; this.value = val; }, 'construct(ValueClass)': function(vc) { this.id = vc.id; this.value = vc.value; }, }); var LookupClass = Class('LookupClass', { 'private object table': [], // lookup a value object by id 'get(number)': function(num) { for (var i in this.table) { var val = this.table[i]; if (val.getId() == num) { return val; } } }, // overload the get function to lookup a value object by it's value 'get(string)': function(str) { for (var i in this.table) { var val = this.table[i]; if (val.getValue() == str) { return val; } } }, // add a new value, by providing a ValueClass object 'add(ValueClass)': function(vc) { this.table.push(vc); }, // add a new value, by providing an numeric id and string value 'add(number, string)': function(id, val) { var vc = new ValueClass(); vc.setId(id); vc.setValue(val); // call the add function above with our new ValueClass object this.add(vc); }, // add a new value, by providing an anonymous object containing the id and value 'add(object)': function(obj) { if ('id' in obj && 'value' in obj) { var vc = new ValueClass(); vc.setId(obj.id); vc.setValue(obj.value); // call the add function above with our new ValueClass object this.add(vc); } }, // add several new values at the same time 'add(...)': function(args) { for (var i in args) { var val = args[i]; this.add(val); } } }); var table = new LookupClass(); var value1 = new ValueClass(1, 'bob'); // add an instance of ValueClass table.add(value1); // add with a number and string table.add(2, 'joe'); // add an anonymous object table.add({ id: 3, value: 'sam' }); // create a few different objects var value2 = new ValueClass(4, 'george'); var value3 = new ValueClass(7, 'bill'); var obj1 = { id: 5, value: 'jenny' }; var obj2 = { id: 6, value: 'laura' }; // add several things at once table.add(value2, obj1, obj2, value3); assert.strictEqual(table.get(5).getValue(), "jenny", 'Verify: table.get(5).getValue() == "jenny"'); assert.strictEqual(table.get(2).getValue(), "joe", 'Verify: table.get(2).getValue() == "joe"'); assert.strictEqual(table.get("sam").getId(), 3, 'Verify: table.get("sam").getId() == 3'); assert.strictEqual(table.get("laura").getId(), 6, 'Verify: table.get("laura").getId() == 6'); assert.strictEqual(table.get(4).getValue(), "george", 'Verify: table.get(4).getValue() == "george"'); var IMyInterface = Interface('IMyInterface', { // Interface body ... }); var RandomClass = Class('RandomClass', { implements: IMyInterface }, {}); var MyClass2 = Class('MyClass2', { // anything which implements IMyInterface will be passed here 'test(IMyInterface)': function(val) { return 1; }, // RandomClass implements IMyInterface, so the above method will always match it. // This method will NEVER be called 'test(RandomClass)': function(val) { return 2; } }); var mc = new MyClass2(); var rc = new RandomClass(); assert.strictEqual(mc.test(rc), 1, 'Verify: Overloads order of operations with parent matching'); var MyClass3 = Class('MyClass3', { // ALL JOII classes ultimately inherit from object, // so this will override ALL other one parameter methods if placed above them // unless they're other native types (such as string) 'test(object)': function(val) { return 3; }, // RandomClass is an object, so the above method will always match it. // This method will NEVER be called 'test(RandomClass)': function(val) { return 4; }, // This method WILL be called, because it has an extra parameter to match against 'test(RandomClass, string)': function(val, str) { return 5; } }); var mc = new MyClass3(); assert.strictEqual(mc.test(rc), 3, 'Verify: Overloads order of operations with parent matching'); assert.strictEqual(mc.test(rc, "test"), 5, 'Verify: Overloads order of operations with parent matching'); } catch (e) { QUnit.pushFailure(e); } });