From 26d2c169a9adb4f541433c46d02b5351a941c08b Mon Sep 17 00:00:00 2001 From: Scott Kyle Date: Mon, 9 May 2016 16:15:10 -0700 Subject: [PATCH] Support ES6 class inheritance from Realm.Object Unfortunately, this was broken because our native Realm.Object constructor was not presenting itself as a function in JS. This fixes that and adds tests. --- examples/ReactExample/components/realm.js | 14 ++--- src/jsc/jsc_class.hpp | 63 +++++++++++++++++------ tests/js/list-tests.js | 3 ++ tests/js/object-tests.js | 4 ++ tests/js/realm-tests.js | 3 ++ tests/js/results-tests.js | 3 ++ 6 files changed, 69 insertions(+), 21 deletions(-) diff --git a/examples/ReactExample/components/realm.js b/examples/ReactExample/components/realm.js index 4b00f9d7..13d042ad 100644 --- a/examples/ReactExample/components/realm.js +++ b/examples/ReactExample/components/realm.js @@ -20,20 +20,22 @@ import Realm from 'realm'; -class Todo {} +class Todo extends Realm.Object {} Todo.schema = { name: 'Todo', properties: { - done: {type: Realm.Types.BOOL, default: false}, - text: Realm.Types.STRING, + done: {type: 'bool', default: false}, + text: 'string', }, }; -class TodoList {} + +class TodoList extends Realm.Object {} TodoList.schema = { name: 'TodoList', properties: { - name: Realm.Types.STRING, - items: {type: Realm.Types.LIST, objectType: 'Todo', default: []}, + name: 'string', + items: {type: 'list', objectType: 'Todo'}, }, }; + export default new Realm({schema: [Todo, TodoList]}); diff --git a/src/jsc/jsc_class.hpp b/src/jsc/jsc_class.hpp index f60080f6..0a236219 100644 --- a/src/jsc/jsc_class.hpp +++ b/src/jsc/jsc_class.hpp @@ -48,10 +48,7 @@ class ObjectWrap { } static JSObjectRef create_constructor(JSContextRef ctx) { - if (JSClassRef constructor_class = get_constructor_class()) { - return JSObjectMake(ctx, constructor_class, nullptr); - } - return JSObjectMakeConstructor(ctx, get_class(), construct); + return JSObjectMake(ctx, get_constructor_class(), nullptr); } static JSClassRef get_class() { @@ -92,7 +89,9 @@ class ObjectWrap { static std::vector get_methods(const MethodMap &); static std::vector get_properties(const PropertyMap &); + static JSValueRef call(JSContextRef, JSObjectRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*); static JSObjectRef construct(JSContextRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*); + static void initialize_constructor(JSContextRef, JSObjectRef); static void finalize(JSObjectRef); static void get_property_names(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); static JSValueRef get_property(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); @@ -104,7 +103,7 @@ class ObjectWrap { } static bool has_instance(JSContextRef ctx, JSObjectRef constructor, JSValueRef value, JSValueRef* exception) { - return JSValueIsObjectOfClass(ctx, value, get_class()); + return has_instance(ctx, value); } }; @@ -158,19 +157,18 @@ inline JSClassRef ObjectWrap::create_class() { template inline JSClassRef ObjectWrap::create_constructor_class() { - // Skip creating a special constructor class if possible. - if (!s_class.constructor && s_class.static_methods.empty() && s_class.static_properties.empty()) { - return nullptr; - } - JSClassDefinition definition = kJSClassDefinitionEmpty; std::vector methods; std::vector properties; definition.attributes = kJSClassAttributeNoAutomaticPrototype; - definition.className = s_class.name.c_str(); + definition.className = "Function"; + definition.initialize = initialize_constructor; definition.hasInstance = has_instance; + // This must be set for `typeof constructor` to be 'function'. + definition.callAsFunction = call; + if (s_class.constructor) { definition.callAsConstructor = construct; } @@ -220,22 +218,57 @@ inline std::vector ObjectWrap::get_properties(const Pr } template -inline JSObjectRef ObjectWrap::construct(JSContextRef ctx, JSObjectRef constructor, size_t argc, const JSValueRef arguments[], JSValueRef* exception) { - if (!s_class.constructor) { - *exception = jsc::Exception::value(ctx, "Illegal constructor"); +inline JSValueRef ObjectWrap::call(JSContextRef ctx, JSObjectRef function, JSObjectRef this_object, size_t argc, const JSValueRef arguments[], JSValueRef* exception) { + // This should only be called as a super() call in the constructor of a subclass. + if (!has_instance(ctx, this_object)) { + *exception = jsc::Exception::value(ctx, s_class.name + " cannot be called as a function"); return nullptr; } - JSObjectRef this_object = ObjectWrap::create_instance(ctx); + // Classes without a constructor should still be subclassable. + if (s_class.constructor) { + try { + s_class.constructor(ctx, this_object, argc, arguments); + } + catch (std::exception &e) { + *exception = jsc::Exception::value(ctx, e); + return nullptr; + } + } + + return JSValueMakeUndefined(ctx); +} + +template +inline JSObjectRef ObjectWrap::construct(JSContextRef ctx, JSObjectRef constructor, size_t argc, const JSValueRef arguments[], JSValueRef* exception) { + if (!s_class.constructor) { + *exception = jsc::Exception::value(ctx, s_class.name + " is not a constructor"); + return nullptr; + } + + JSObjectRef this_object = create_instance(ctx); try { s_class.constructor(ctx, this_object, argc, arguments); } catch (std::exception &e) { *exception = jsc::Exception::value(ctx, e); + return nullptr; } return this_object; } +template +inline void ObjectWrap::initialize_constructor(JSContextRef ctx, JSObjectRef constructor) { + static const String prototype_string = "prototype"; + + // Set the prototype of the constructor to be Function.prototype. + Object::set_prototype(ctx, constructor, Object::get_prototype(ctx, JSObjectMakeFunctionWithCallback(ctx, nullptr, call))); + + // Set the constructor prototype to be the prototype generated from the instance JSClassRef. + JSObjectRef prototype = Object::validated_get_object(ctx, JSObjectMakeConstructor(ctx, get_class(), construct), prototype_string); + Object::set_property(ctx, constructor, prototype_string, prototype, js::ReadOnly | js::DontEnum | js::DontDelete); +} + template inline void ObjectWrap::finalize(JSObjectRef object) { // This is called for the most derived class before superclasses. diff --git a/tests/js/list-tests.js b/tests/js/list-tests.js index cb316b31..2549e92f 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -36,6 +36,9 @@ module.exports = BaseTest.extend({ TestCase.assertThrows(function() { new Realm.List(); }); + + TestCase.assertEqual(typeof Realm.List, 'function'); + TestCase.assertTrue(Realm.List instanceof Function); }, testListLength: function() { diff --git a/tests/js/object-tests.js b/tests/js/object-tests.js index 3abad10b..d4351fbf 100644 --- a/tests/js/object-tests.js +++ b/tests/js/object-tests.js @@ -435,10 +435,14 @@ module.exports = BaseTest.extend({ testObjectConstructor: function() { var realm = new Realm({schema: [schemas.TestObject]}); + realm.write(function() { var obj = realm.create('TestObject', {doubleCol: 1}); TestCase.assertTrue(obj instanceof Realm.Object); }); + + TestCase.assertEqual(typeof Realm.Object, 'function'); + TestCase.assertTrue(Realm.Object instanceof Function); }, testIsValid: function() { diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 35294bd4..5e557b4d 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -27,6 +27,9 @@ module.exports = BaseTest.extend({ testRealmConstructor: function() { var realm = new Realm({schema: []}); TestCase.assertTrue(realm instanceof Realm); + + TestCase.assertEqual(typeof Realm, 'function'); + TestCase.assertTrue(Realm instanceof Function); }, testRealmConstructorPath: function() { diff --git a/tests/js/results-tests.js b/tests/js/results-tests.js index 99229238..8a3f3f4e 100644 --- a/tests/js/results-tests.js +++ b/tests/js/results-tests.js @@ -34,6 +34,9 @@ module.exports = BaseTest.extend({ TestCase.assertThrows(function() { new Realm.Results(); }); + + TestCase.assertEqual(typeof Realm.Results, 'function'); + TestCase.assertTrue(Realm.Results instanceof Function); }, testResultsLength: function() {