Issues (204)

src/ClassBuilder.js (1 issue)

1
/* Javascript Object Inheritance Implementation                ______  ________
2
 * (c) 2016 <[email protected]>                             __ / / __ \/  _/  _/
3
 * Licensed under MIT.                                    / // / /_/ // /_/ /
4
 * ------------------------------------------------------ \___/\____/___/__*/
5
6
'use strict';
7
8
    JOII = typeof (JOII) !== 'undefined' ? JOII : {};
9
    JOII.ClassRegistry = {};
10
11
    /**
12
     * The ClassBuilder is responsible for creating a class definition based
13
     * on the given parameters and body. We use the PrototypeBuilder to create
14
     * a uniform prototype based on our own defined class body and the
15
     * prototypes of inherited definitions.
16
     *
17
     * The resulting function will be the class definition which creates its
18
     * own new 'scope' each time it's instantiated.
19
     *
20
     * @param string name
21
     * @param object parameters
22
     * @param object body
23
     * @return function
24
     */
25
    JOII.ClassBuilder = function() {
26
        var args                        = JOII.Compat.ParseArguments(arguments),
27
            name                        = args.name,
28
            parameters                  = args.parameters,
29
            body                        = args.body,
30
            is_static_generated         = args.is_static_generated === true;
31
        
32
        function static_scope_in() {
33
            // If 'this.__joii__' is not available, that would indicate that
34
            // we've been executed like a function rather than being instantiated.
35
            if (typeof (this) === 'undefined' || typeof (this.__joii__) === 'undefined') {
36
                // If the method __call exists, execute it and return its result.
37
38
                return definition.apply(undefined, arguments);
39
            }
40
41
            return new definition();
42
        }
43
        /**
44
         * Defines the class definition. This is the function that is executed
45
         * when the class is instantiated or executed. The function will relay
46
         * execution to the __construct or __call method, depending whether the
47
         * class was called as a function or instantiated using the 'new'
48
         * keyword.
49
         *
50
         * @return object The outer (public) class scope.
51
         */
52
        function definition() {
53
54
            var func_in         = function() { };
55
            func_in.prototype   = this;
56
            var scope_in_obj    = new func_in();
57
            
58
59
            // Create an inner and outer scope. The inner scope refers to the
60
            // 'this' variable, where the outer scope contains references to
61
            // all objects and functions accessible from the outside.
62
            var scope_in = generateInnerScope(this, arguments, scope_in_obj, false);
63
            
64
            // for __call implementations
65
            if (typeof (this) === 'undefined' || typeof (this.__joii__) === 'undefined' || typeof (scope_in) !== 'object' || typeof (scope_in.__joii__) === 'undefined') {
66
                return scope_in;
67
            }
68
69
            
70
71
            
72
            var scope_out = generateOuterScope(this, scope_in);
73
            
74
            // need to link the inner and outer scopes before calling constructors
75
            linkAPI(scope_in, scope_out);
76
            
77
78
            // apply meta traits
79
            JOII.callMetaMixin('beforeNew', scope_in, scope_out);
80
81
            for (var meta_index in scope_in.__joii__.metadata) {
82
                if (scope_in.__joii__.metadata.hasOwnProperty(meta_index) === false) continue;
83
                var meta = scope_in.__joii__.metadata[meta_index];
84
        
85
                JOII.callMetaMixin('onNew', scope_in, scope_out, meta);
86
            }
87
    
88
            JOII.callMetaMixin('afterNew', scope_in, scope_out);
89
90
91
            callConstructors(scope_in, arguments);
92
93
            return scope_out;
94
        }
95
96
        function callConstructors(scope_in, args)
97
        {
98
            // Does the class defintion have a constructor? If so, run it.
99
            for (var c in JOII.Config.constructors) {
100
                if (JOII.Config.constructors.hasOwnProperty(c)) {
101
                    var cc = JOII.Config.constructors[c];
102
                    if (typeof (scope_in[cc]) === 'function') {
103
                        scope_in[cc].apply(scope_in, args);
104
                        break;
105
                    }
106
                }
107
            }
108
            
109
            // deserialize data
110
            if (args.length == 1 && typeof args[0] == 'object' && '__joii_deserialize_object' in args[0]) {
111
                scope_in.deserialize(args[0].data);
112
            }
113
        }
114
115
        function linkAPI(scope_in, scope_out)
116
        {
117
            // Create a reference to the outer scope for use in fluid interfacing.
118
            scope_in.__api__ = scope_out;
119
120
            // Apply the API object to inherited classes to keep the super() functionality working no matter how deep
121
            // the inheritance-chain goes.
122
            // This feels really 'hacky' in my opinion, but it fixes issue #19 and doesn't break any other test.
123
            // As far as I can tell, there's no real performance impact on this, although I'm running this on a beast
124
            // of a computer. If anyone has a more elegant solution, a pull-request would be much appreciated!
125
            if (typeof scope_in.__joii__.parent !== 'undefined') {
126
                var current = scope_in.__joii__.parent;
127
                while (typeof current !== 'undefined') {
128
                    current.__api__ = scope_out;
129
                    current = current.__joii__.parent;
130
                }
131
            }
132
        }
133
134
        function generateInnerScope(scope, args, base_object, is_static_generated) {
135
            var scope_in = base_object || {};
136
137
            is_static_generated = is_static_generated || false;
138
139
            // Create a deep copy of the inner scope because we need to
140
            // dereference object-type properties. If we don't do this, object-
141
            // types are treated statically throughout all instances.
142
            scope_in = JOII.Compat.extend(true, {}, scope_in);
143
144
            if (typeof scope !== 'undefined') {
145
                JOII.CreateProperty(scope_in, '__joii__', (scope.__joii__));
146
            }
147
148
            if (typeof scope !== 'undefined' && typeof (scope_in.__joii__) === 'object') {
149
                // Can we be instantiated?
150
                if (scope_in.__joii__.is_abstract === true) {
151
                    throw 'An abstract class cannot be instantiated.';
152
                }
153
                if (!is_static_generated && scope_in.__joii__.is_static === true) {
154
                    throw 'A static class cannot be instantiated.';
155
                }
156
            }
157
158
            // If 'this.__joii__' is not available, that would indicate that
159
            // we've been executed like a function rather than being instantiated.
160
            if (typeof (scope) === 'undefined' || typeof (scope.__joii__) === 'undefined') {
161
                // If the method __call exists, execute it and return its result.
162
163
                if (typeof (static_scope_in) !== 'undefined')
164
                {
165
                    for (var c in JOII.Config.callables) {
166
                        if (JOII.Config.callables.hasOwnProperty(c)) {
167
                            if (typeof (static_scope_in[JOII.Config.callables[c]]) === 'function') {
168
                                var result = static_scope_in[JOII.Config.callables[c]].apply(body, args);
169
                                if (result === body) {
170
                                    throw JOII.Config.callables[c] + ' cannot return itself.';
171
                                }
172
                                return result;
173
                            }
174
                        }
175
                    }
176
                }
177
                throw 'This class cannot be called as a function because it\'s lacking the __call method.';
178
            }
179
180
181
182
            // Are we attempting to instantiate an abstract class?
183
            if (scope.__joii__.is_abstract) {
184
                throw 'Cannot instantiate abstract class ' + scope.__joii__.name;
185
            }
186
187
188
            return scope_in;
189
190
        }
191
192
        function generateOuterScope(scope, scope_in, base_object) {
193
            var scope_out = base_object || {};
194
            
195
            if (typeof scope !== 'undefined' && typeof (scope.__joii__) === 'object') {
196
                
197
                JOII.CreateProperty(scope_out, '__joii__', (scope.__joii__));
198
            
199
                // Can we be instantiated?
200
                if (scope_out.__joii__.is_abstract === true) {
201
                    throw 'An abstract class cannot be instantiated.';
202
                }
203
204
                // The outside scope.
205
                for (var i in scope) {
206
                    var meta = scope_in.__joii__.metadata[i];
207
208
                    if (meta && 'overloads' in meta) {
209
                        for (var fn_meta in meta.overloads) {
210
                            // Test missing abstract implementations...
211
                            if (meta.overloads[fn_meta] && meta.overloads[fn_meta].is_abstract === true) {
212
                                throw 'Missing abstract member implementation of ' + i + '(' + meta.overloads[fn_meta].parameters.join(', ') + ')';
213
                            }
214
                        }
215
                    } else if (meta && meta.is_abstract === true) {
216
                        throw 'Missing abstract member implementation of "' + i + '".';
217
                    }
218
                }
219
            }
220
221
            bindPublicMethods(scope_in, scope_out);
222
223
            return scope_out;
224
        }
225
226
        function bindPublicMethods(from_obj, to_obj)
227
        {
228
            // The outside scope.
229
            for (var i in from_obj) {
230
                var meta = from_obj.__joii__.metadata[i];
231
                
232
                // Only allow public functions in the outside scope.
233
                if (typeof (from_obj[i]) === 'function' && (typeof (meta) === 'undefined' || meta.visibility === 'public') && (i !== '__call')) {
234
                    to_obj[i] = JOII.Compat.Bind(from_obj[i], from_obj);
235
                }
236
            }
237
        }
238
239
240
        if (typeof (body) == 'function') {
241
            body = body(static_scope_in);
242
        }
243
        
244
        if (typeof (body) != 'object') {
245
            throw 'Invalid parameter types given. Expected: ([[[string], object], <object|function>]).';
246
        }
247
248
        
249
250
        // Apply to prototype to the instantiator to allow extending the
251
        // class definition upon other definitions without instantiation.
252
        definition.prototype = JOII.PrototypeBuilder(name, parameters, body, false, is_static_generated);
253
        
254
255
256
        // Apply constants to the definition
257
        for (var i in definition.prototype.__joii__.constants) {
258
            JOII.CreateProperty(definition, i, definition.prototype.__joii__.constants[i], false);
259
        }
260
261
        // Does the class implement an enumerator?
262
        if (typeof (parameters['enum']) === 'string') {
263
            var e = JOII.EnumBuilder(parameters['enum'], definition);
264
            if (parameters.expose_enum === true) {
265
                var g = typeof window === 'object' ? window : global;
266
                if (typeof (g[parameters['enum']]) !== 'undefined') {
267
                    throw 'Cannot expose Enum "' + parameters['enum'] + '" becase it already exists in the global scope.';
268
                }
269
                g[parameters['enum']] = e;
270
            }
271
        }
272
273
        // Override toString to return a class symbol.
274
        var n = arguments[0];
275
        definition.toString = function() {
276
            if (typeof (n) === 'string') {
277
                return '[class ' + n + ']';
278
            }
279
            return '[class Class]';
280
        };
281
282
        // Store defined interfaces in the metadata.
283
        definition.prototype.__joii__.interfaces = parameters['implements'];
284
285
        // TODO performance can be increased here by storing the parsed
286
        //      interfaces in the 'interfaces' array in __joii__.
287
288
        // Recursive function for retrieving a list of interfaces from the
289
        // current class and the rest of the inheritance tree.
290
        bindGetInterfaces(definition.prototype.__joii__);
291
292
293
        function bindGetInterfaces(joii)
294
        {
295
            
296
            // Recursive function for retrieving a list of interfaces from the
297
            // current class and the rest of the inheritance tree.
298
            joii.getInterfaces = JOII.Compat.Bind(function() {
299
                var interfaces = [],
300
                    getRealInterface = JOII.Compat.Bind(function(i) {
301
                        if (typeof (i) === 'function') {
302
                            return i;
303
                        } else if (typeof (i) === 'string') {
304
                            if (typeof (JOII.InterfaceRegistry[i]) === 'undefined') {
305
                                throw 'Interface "' + i + '" does not exist.';
306
                            }
307
                            return JOII.InterfaceRegistry[i];
308
                        }
309
                    }, this);
310
311
                // Fetch interfaces from the parent list - if they exist.
312
                if (typeof (this.parent) !== 'undefined' && typeof (this.parent.__joii__) !== 'undefined') {
313
                    interfaces = this.parent.__joii__.getInterfaces();
314
                }
315
316
                if (typeof (this.interfaces) !== 'undefined') {
317
                    if (typeof (this.interfaces) === 'object') {
318
                        for (var i in this.interfaces) {
319
                            if (!this.interfaces.hasOwnProperty(i)) {
320
                                continue;
321
                            }
322
                            interfaces.push(getRealInterface(this.interfaces[i]));
323
                        }
324
                    } else {
325
                        interfaces.push(getRealInterface(this.interfaces));
326
                    }
327
                }
328
329
                return interfaces;
330
            }, joii);
331
        }
332
333
        // If any interfaces are implemented in this class, validate them
334
        // immediately rather than doing so during instantiation. If the
335
        // class is declared abstract, the validation is skipped.
336
        if (parameters.abstract !== true) {
337
            var interfaces = definition.prototype.__joii__.getInterfaces();
338
            for (var ii in interfaces) {
339
                if (interfaces.hasOwnProperty(ii) && typeof (interfaces[ii]) === 'function') {
340
                    interfaces[ii](definition);
341
                }
342
            }
343
        }
344
345
        
346
        if (parameters['static'] !== true && !is_static_generated) {
347
348
            // check to make sure serialize doesn't exist yet, or if it does - it's capable of being overloaded without breaking BC
349
            if ((!('serialize' in definition.prototype.__joii__.metadata)) || (('overloads' in definition.prototype.__joii__.metadata['serialize']) && (definition.prototype.__joii__.metadata['serialize']['overloads'][0].parameters.length > 0 || definition.prototype.__joii__.metadata['serialize']['overloads'].length > 1))) {
350
                
351
                /**
352
                 * Serializes all serializable properties of an object. Public members are serializable by default.
353
                 *
354
                 * @return {String}
355
                 */
356
                var generated_fn = function() {
357
                    return JSON.stringify(this.serialize(true));
358
                };
359
                // uses an inheritance style add, so it won't overwrite custom functions with the same signature
360
                var serialize_meta = JOII.ParseClassProperty('public function serialize()');
361
                JOII.addFunctionToPrototype(definition.prototype, serialize_meta, generated_fn, true);
362
363
                
364
                /**
365
                 * Serializes all serializable properties of an object. Public members are serializable by default.
366
                 *
367
                 * @return {Object}
368
                 */
369
                var generated_fn = function(bool_return_object) {
370
                    var obj = { __joii_type: this.__joii__.name };
371
372
                    for (var key in this.__joii__.metadata) {
373
                        var val = this.__joii__.metadata[key];
374
375
                        if (val.serializable) {
376
                            
377
                            var getter_name = JOII.GenerateGetterName(val);
378
                            var currentValue = null;
379
                            if (typeof (this[getter_name]) === 'function') {
380
                                // use getter if it exists. This allows custom getters to translate the data properly if needed.
381
                                currentValue = this[getter_name]();
382
                            } else {
383
                                currentValue = this[val.name];
384
                            }
385
386
                            if (!val.is_enum && typeof (currentValue) === 'object' && currentValue !== null) {
387
                                if ('serialize' in currentValue) {
388
                                    obj[val.name] = currentValue.serialize(true);
389
                                } else {
390
                                    obj[val.name] = JOII.Compat.flattenObject(currentValue);
391
                                }
392
                            } else {
393
                                obj[val.name] = currentValue;
394
                            }
395
                        }
396
                    }
397
398
                    return obj;
399
                };
400
                // uses an inheritance style add, so it won't overwrite custom functions with the same signature
401
                var serialize_meta = JOII.ParseClassProperty('public function serialize(boolean)');
402
                JOII.addFunctionToPrototype(definition.prototype, serialize_meta, generated_fn, true);
403
            }
404
405
406
407
            // check to make sure deserialize doesn't exist yet, or if it does - it's capable of being overloaded without breaking BC
408
            if ((!('deserialize' in definition.prototype.__joii__.metadata)) || (('overloads' in definition.prototype.__joii__.metadata['deserialize']) && (definition.prototype.__joii__.metadata['deserialize']['overloads'][0].parameters.length > 0 || definition.prototype.__joii__.metadata['deserialize']['overloads'].length > 1))) {
409
                /**
410
                 * Deserializes a class (called on an object instance to populate it)
411
                 *
412
                 * @param {String}
413
                 */
414
                var generated_fn = function(json) {
415
                    this.deserialize(JSON.parse(json));
416
                };
417
                // uses an inheritance style add, so it won't overwrite custom functions with the same signature
418
                var deserialize_meta = JOII.ParseClassProperty('public function deserialize(string)');
419
                JOII.addFunctionToPrototype(definition.prototype, deserialize_meta, generated_fn, true);
420
                
421
                /**
422
                 * Deserializes a class (called on an object instance to populate it)
423
                 *
424
                 * @param {Object}
425
                 */
426
                generated_fn = function(obj) {
427
                    for (var key in (this.__joii__.metadata)) {
428
                        var val = this.__joii__.metadata[key];
429
430
                        if (val.serializable) {
431
                            if (val.name in obj && typeof (obj[val.name]) != 'function') {
432
                                var setter_name = JOII.GenerateSetterName(val);
433
                                var getter_name = JOII.GenerateGetterName(val);
434
435
436
                                if (typeof (obj[val.name]) === 'object' && obj[val.name] !== null && '__joii_type' in (obj[val.name])) {
437
                                    var name = obj[val.name].__joii_type;
438
                                    // Check for Interface-types
439
                                    if (typeof (JOII.InterfaceRegistry[name]) !== 'undefined') {
440
                                        throw 'Cannot instantiate an interface.';
441
                                    }
442
                                    // Check for Class-types
443
                                    else if (typeof (JOII.ClassRegistry[name]) !== 'undefined') {
444
                                        
445
                                        var currentValue = null;
446
                                        if (typeof (this[getter_name]) === 'function') {
447
                                            // use getter if it exists. This allows custom getters to translate the data properly if needed.
448
                                            currentValue = this[getter_name]();
449
                                        } else {
450
                                            currentValue = this[val.name];
451
                                        }
452
453
                                        if (typeof (currentValue) === 'object' && currentValue !== null && currentValue.__joii__.name === name) {
454
                                            // try to deserialize in place if the object already exists. This avoids breaking object references.
455
                                            currentValue.deserialize(obj[val.name]);
456
                                        } else {
457
                                            if (typeof (this[setter_name]) === 'function') {
458
                                                // use setter if it exists. This allows custom setters to translate the data properly.
459
                                                this[setter_name](JOII.ClassRegistry[name].deserialize(obj[val.name]));
460
                                            } else {
461
                                                // need to set directly
462
                                                this[val.name] = JOII.ClassRegistry[name].deserialize(obj[val.name]);
463
                                            }
464
                                        }
465
                                    } else {
466
                                        throw 'Class ' + name + ' not currently in scope!';
467
                                    }
468
                                } else if (typeof (obj[val.name]) === 'object' && obj[val.name] !== null) {
469
470
                                    var currentValue = null;
471
                                    if (typeof (this[getter_name]) === 'function') {
472
                                        // use getter if it exists. This allows custom getters to translate the data properly if needed.
473
                                        currentValue = this[getter_name]();
474
                                    } else {
475
                                        currentValue = this[val.name];
476
                                    }
477
478
                                    // normal object. Crawl through it to find JOII objects.
479
                                    var new_val = JOII.Compat.inflateObject(obj[val.name], currentValue);
480
481
                                    if (typeof (this[setter_name]) === 'function') {
482
                                        // use setter if it exists. This allows custom setters to translate the data properly.
483
                                        this[setter_name](new_val);
484
                                    } else {
485
                                        this[val.name] = new_val;
486
                                    }
487
                                } else {
488
                                    if (typeof (this[setter_name]) === 'function') {
489
                                        // use setter if it exists. This allows custom setters to translate the data properly.
490
                                        this[setter_name](obj[val.name]);
491
                                    } else {
492
                                        this[val.name] = obj[val.name];
493
                                    }
494
                                }
495
                            }
496
                        }
497
                    }
498
                };
499
                // uses an inheritance style add, so it won't overwrite custom functions with the same signature
500
                deserialize_meta = JOII.ParseClassProperty('public function deserialize(object)');
501
                JOII.addFunctionToPrototype(definition.prototype, deserialize_meta, generated_fn, true);
502
503
            };
0 ignored issues
show
This semicolons seems to be unnecessary.
Loading history...
504
505
        }
506
507
508
509
        
510
        // if it's not a static class, generate it's static backing field
511
        if (!is_static_generated && typeof (parameters['enum']) !== 'string') {
512
            
513
            var __in_joii_static_class_constructor = false;
514
515
            function staticDefinition() {
516
517
                var func_in         = function() { };
518
                func_in.prototype   = this;
519
                var scope_in_obj    = new func_in();
520
521
                static_scope_in.prototype = definition;
522
523
                // Create an inner static scope, for private/protected members                
524
                var scope_in = generateInnerScope(this, [], scope_in_obj, true);
525
            
526
                // Create the static field, and copy it into the object we created before.
527
                // Need to copy it this way, so that the object reference is still the same, 
528
                // since we may have passed it into the optional user function which generates the body
529
                static_scope_in = JOII.Compat.extend(true, static_scope_in, scope_in);
530
                
531
                // bind any public static members to the outside class
532
                //bindPublicMethods(static_scope_in, definition);
533
534
                definition = generateOuterScope(static_scope_in, static_scope_in, definition);
535
                
536
                // need to link the inner and outer scopes before calling constructors
537
                linkAPI(static_scope_in, definition);
538
                
539
                // static constructors can't have parameters
540
                callConstructors(static_scope_in, []);
541
                
542
                return static_scope_in;
543
            }
544
            
545
            __in_joii_static_class_constructor = true;
546
547
            // Apply to prototype to the instantiator to allow extending the
548
            // class definition upon other definitions without instantiation.
549
            staticDefinition.prototype = JOII.PrototypeBuilder(name, parameters, body, false, true);
550
            
551
            // Store defined interfaces in the metadata.
552
            staticDefinition.prototype.__joii__.interfaces = parameters['implements'];
553
554
            bindGetInterfaces(staticDefinition.prototype.__joii__);
555
            
556
            // If any interfaces are implemented in this class, validate them
557
            // immediately rather than doing so during instantiation. If the
558
            // class is declared abstract, the validation is skipped.
559
            if (parameters.abstract !== true) {
560
                var interfaces = staticDefinition.prototype.__joii__.getInterfaces();
561
                for (var ii in interfaces) {
562
                    if (interfaces.hasOwnProperty(ii) && typeof (interfaces[ii]) === 'function') {
563
                        interfaces[ii](staticDefinition);
564
                    }
565
                }
566
            }
567
568
            /**
569
             * Deserializes a class (called as a static method - instantiates a new object and populates it)
570
             *
571
             * @param {String}
572
             */
573
            var generated_fn = function(json) {
574
                return this.deserialize(JSON.parse(json));
575
            };
576
            // uses an inheritance style add, so it won't overwrite custom functions with the same signature
577
            var deserialize_meta = JOII.ParseClassProperty('public static function deserialize(string)');
578
            JOII.addFunctionToPrototype(staticDefinition.prototype, deserialize_meta, generated_fn, true);
579
580
            
581
            /**
582
             * Deserializes a class (called as a static method - instantiates a new object and populates it)
583
             *
584
             * @param {Object}
585
             */
586
            generated_fn = function(obj) {
587
                var deserialize_object = {
588
                    '__joii_deserialize_object': true,
589
                    'data': obj
590
                };
591
                return new definition(deserialize_object);
592
            };
593
            // uses an inheritance style add, so it won't overwrite custom functions with the same signature
594
            deserialize_meta = JOII.ParseClassProperty('public static function deserialize(object)');
595
            JOII.addFunctionToPrototype(staticDefinition.prototype, deserialize_meta, generated_fn, true);
596
597
598
            /**
599
             * Gets the current static scope
600
             *
601
             * @param {String}
602
             */
603
            /*
604
            var generated_fn = function() {
605
                return static_scope_in;
606
            };
607
            // uses an inheritance style add, so it won't overwrite custom functions with the same signature
608
            var deserialize_meta = JOII.ParseClassProperty('private function getStatic()');
609
            JOII.addFunctionToPrototype(definition.prototype, deserialize_meta, generated_fn, true);
610
            */
611
612
            // create an object for the static members, and store a reference to it
613
            inner_static_objects[name] = new staticDefinition();
614
            
615
            
616
            definition.__joii__.prototype = staticDefinition.prototype;
617
            
618
619
        }
620
621
622
        
623
        
624
        // Register the class by the given name to make it usable as a type
625
        // inside property declarations.
626
        if (typeof (JOII.ClassRegistry[name]) !== 'undefined') {
627
            throw 'Another class named "' + name + '" already exists.';
628
        }
629
        JOII.ClassRegistry[name] = definition;
630
631
632
633
        definition.prototype = JOII.Compat.extend(true, {}, definition.prototype);
634
635
        return definition;
636
    };
637