Selaa lähdekoodia

Initializable decorator refactoring #1

sanex3339 7 vuotta sitten
vanhempi
commit
4b25c31f48

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
dist/index.js


+ 116 - 30
src/decorators/Initializable.ts

@@ -2,54 +2,140 @@
 
 import { IInitializable } from '../interfaces/IInitializable';
 
+const defaultDescriptor: PropertyDescriptor = {
+    configurable: true,
+    enumerable: true
+};
+const initializedTargetMetadataKey: string = '_initialized';
+const initializablePropertiesSetMetadataKey: string = '_initializablePropertiesSet';
+const constructorMethodName: string = 'constructor';
+
 /**
- * @param {string} initializeMethodKey
- * @returns {any}
+ * @param {string} initializeMethodName
+ * @returns {(target: IInitializable, propertyKey: (string | symbol)) => any}
  */
 export function initializable (
-    initializeMethodKey: string = 'initialize'
+    initializeMethodName: string = 'initialize'
 ): (target: IInitializable, propertyKey: string | symbol) => any {
     const decoratorName: string = Object.keys(this)[0];
 
     return (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor => {
-        const descriptor: PropertyDescriptor = {
-            configurable: true,
-            enumerable: true
-        };
-        const initializeMethod: Function = target[initializeMethodKey];
+        const initializeMethod: Function = target[initializeMethodName];
 
         if (!initializeMethod || typeof initializeMethod !== 'function') {
-            throw new Error(`\`${initializeMethodKey}\` method with initialization logic not ` +
-                `found. \`@${decoratorName}\` decorator requires \`${initializeMethodKey}\` method`);
+            throw new Error(`\`${initializeMethodName}\` method with initialization logic not ` +
+                `found. \`@${decoratorName}\` decorator requires \`${initializeMethodName}\` method`);
+        }
+
+        if (!target[initializablePropertiesSetMetadataKey]) {
+            target[initializablePropertiesSetMetadataKey] = new Set();
+        }
+
+        wrapTargetMethodsInInitializedCheck(target, initializeMethodName);
+        wrapInitializeMethodInInitializeCheck(target, initializeMethodName, propertyKey);
+
+        return wrapInitializableProperty(target, propertyKey);
+    };
+}
+
+/**
+ * Wraps all target methods with additional logic that check that this methods will called after `initialize` method
+ *
+ * @param {IInitializable} target
+ * @param {string} initializeMethodName
+ */
+function wrapTargetMethodsInInitializedCheck (target: IInitializable, initializeMethodName: string): void {
+    const ownPropertyNames: string[] = Object.getOwnPropertyNames(target);
+    const prohibitedPropertyNames: string[] = [initializeMethodName, constructorMethodName];
+
+    target[initializedTargetMetadataKey] = false;
+
+    ownPropertyNames.forEach((propertyName: string) => {
+        const isProhibitedPropertyName: boolean = prohibitedPropertyNames.includes(propertyName)
+            || target[initializablePropertiesSetMetadataKey].has(propertyName);
+
+        if (isProhibitedPropertyName) {
+            return;
+        }
+
+        const targetProperty: any = target[propertyName];
+
+        if (typeof targetProperty !== 'function') {
+            return;
         }
 
-        const metadataPropertyKey: string = `_${propertyKey}`;
-        const propertyDescriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(target, metadataPropertyKey) || descriptor;
-        const methodDescriptor: PropertyDescriptor = Object.getOwnPropertyDescriptor(target, initializeMethodKey) || descriptor;
+        const methodDescriptor: PropertyDescriptor = Object
+            .getOwnPropertyDescriptor(target, propertyName) || defaultDescriptor;
         const originalMethod: Function = methodDescriptor.value;
 
-        Object.defineProperty(target, propertyKey, {
-            ...propertyDescriptor,
-            get: function (): any {
-                if (this[metadataPropertyKey] === undefined) {
-                    throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
+        Object.defineProperty(target, propertyName, {
+            ...methodDescriptor,
+            value: function (): void {
+                if (!this[initializedTargetMetadataKey]) {
+                    throw new Error(`Class should be initialized with \`${initializeMethodName}()\` method`);
                 }
 
-                return this[metadataPropertyKey];
-            },
-            set: function (newVal: any): void {
-                this[metadataPropertyKey] = newVal;
+                originalMethod.apply(this, arguments);
             }
         });
-        Object.defineProperty(target, initializeMethodKey, {
-            ...methodDescriptor,
-            value: function (): void {
-                originalMethod.apply(this, arguments);
+    });
+}
+
+/**
+ * Wraps `initialize` method with additional logic to check that `initialized` properties will set
+ *
+ * @param {IInitializable} target
+ * @param {string} initializeMethodName
+ * @param {string | symbol} propertyKey
+ */
+function wrapInitializeMethodInInitializeCheck (
+    target: IInitializable,
+    initializeMethodName: string,
+    propertyKey: string | symbol
+): void {
+    const methodDescriptor: PropertyDescriptor = Object
+        .getOwnPropertyDescriptor(target, initializeMethodName) || defaultDescriptor;
+    const originalMethod: Function = methodDescriptor.value;
+
+    Object.defineProperty(target, initializeMethodName, {
+        ...methodDescriptor,
+        value: function (): void {
+            originalMethod.apply(this, arguments);
+
+            this[initializedTargetMetadataKey] = true;
+
+            if (this[propertyKey]) {}
+        }
+    });
+}
+
+/**
+ * Wraps initializable property in additional checks
+ *
+ * @param {IInitializable} target
+ * @param {string | symbol} propertyKey
+ * @returns {PropertyDescriptor}
+ */
+function wrapInitializableProperty (target: IInitializable, propertyKey: string | symbol): PropertyDescriptor {
+    target[initializablePropertiesSetMetadataKey].add(propertyKey);
 
-                if (this[propertyKey]) {}
+    const initializablePropertyMetadataKey: string = `_${propertyKey}`;
+    const propertyDescriptor: PropertyDescriptor = Object
+            .getOwnPropertyDescriptor(target, initializablePropertyMetadataKey) || defaultDescriptor;
+
+    Object.defineProperty(target, propertyKey, {
+        ...propertyDescriptor,
+        get: function (): any {
+            if (this[initializablePropertyMetadataKey] === undefined) {
+                throw new Error(`Property \`${propertyKey}\` is not initialized! Initialize it first!`);
             }
-        });
 
-        return propertyDescriptor;
-    };
+            return this[initializablePropertyMetadataKey];
+        },
+        set: function (newVal: any): void {
+            this[initializablePropertyMetadataKey] = newVal;
+        }
+    });
+
+    return propertyDescriptor;
 }

+ 64 - 15
test/unit-tests/decorators/initializable/Initializable.spec.ts

@@ -1,3 +1,5 @@
+import 'reflect-metadata';
+
 import { assert } from 'chai';
 
 import { initializable } from '../../../../src/decorators/Initializable';
@@ -28,29 +30,76 @@ describe('@initializable', () => {
             });
         });
 
-        describe('variant #2: custom initialization method name is passed', () => {
-            const testFunc: () => void = () => {
-                class Foo implements IInitializable {
-                    @initializable()
-                    public property!: string;
+        describe('variant #2: `initialize` method should be called first', () => {
+            describe('variant #1: `initialize` method was called first', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.property = property;
+                        }
 
-                    public initialize (): void {
+                        public bar (): void {}
                     }
 
-                    public bar (property: string): void {
-                        this.property = property;
+                    const foo: Foo = new Foo();
+
+                    foo.initialize('baz');
+                    foo.bar();
+                };
+
+                it('should throws an error if `initialize` method wasn\'t called first', () => {
+                    assert.doesNotThrow(testFunc, /Class should be initialized/);
+                });
+            });
+
+            describe('variant #2: `initialize` method wasn\'t called first', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.property = property;
+                        }
+
+                        public bar (): void {}
                     }
-                }
 
-                const foo: Foo = new Foo();
+                    const foo: Foo = new Foo();
 
-                foo.bar('baz');
+                    foo.bar();
+                    foo.initialize('baz');
+                };
 
-                foo.property;
-            };
+                it('should throws an error if `initialize` method wasn\'t called first', () => {
+                    assert.throws(testFunc, /Class should be initialized/);
+                });
+            });
 
-            it('shouldn\'t throws an errors if custom initialization method name is passed', () => {
-                assert.doesNotThrow(testFunc, Error);
+            describe('variant #3: `initialize` method wasn\'t called', () => {
+                const testFunc: () => void = () => {
+                    class Foo {
+                        @initializable()
+                        public property!: string;
+
+                        public initialize (property: string): void {
+                            this.property = property;
+                        }
+
+                        public bar (): void {}
+                    }
+
+                    const foo: Foo = new Foo();
+
+                    foo.bar();
+                };
+
+                it('should throws an error if `initialize` method wasn\'t called first', () => {
+                    assert.throws(testFunc, /Class should be initialized/);
+                });
             });
         });
 

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä