From c96c3f88b1980b1b4500630efffbf5ce6729e399 Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Thu, 24 Dec 2020 14:17:47 +0000 Subject: [PATCH 1/8] ci: change the build trigger --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10fa095..ff8c86e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: Build -on: [push, pull_request] +on: [pull_request] jobs: build: From 2f40df812a983f7d15ebf1d71206e8c57ac90715 Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Thu, 24 Dec 2020 14:46:27 +0000 Subject: [PATCH 2/8] feat: allow common interface for checking if VO is null --- src/CompositeValueObject.ts | 12 +++++ src/Scalars/BooleanScalar.ts | 4 ++ src/Scalars/FloatScalar.ts | 4 ++ src/Scalars/IntegerScalar.ts | 4 ++ src/Scalars/NullScalar.ts | 4 ++ src/Scalars/StringScalar.ts | 4 ++ src/Scalars/__tests__/BooleanScalar.test.ts | 4 ++ src/Scalars/__tests__/FloatScalar.test.ts | 4 ++ src/Scalars/__tests__/IntegerScalar.test.ts | 4 ++ src/Scalars/__tests__/NullScalar.test.ts | 4 ++ src/Scalars/__tests__/StringScalar.test.ts | 4 ++ src/ValueObject.ts | 3 ++ src/__tests__/CompositeValueObject.test.ts | 58 +++++++++++++++++++++ src/__tests__/ValueObject.test.ts | 8 +++ 14 files changed, 121 insertions(+) diff --git a/src/CompositeValueObject.ts b/src/CompositeValueObject.ts index 1fea7bb..8fb3664 100644 --- a/src/CompositeValueObject.ts +++ b/src/CompositeValueObject.ts @@ -49,6 +49,18 @@ export class CompositeValueObject< return isObjectEqual(this.toNative(), object.toNative() as GenericObject); }; + public isNull = (): boolean => { + return Object.keys(this.value).reduce( + (result: boolean, key: string): boolean => { + if (result === false) { + return false; + } + return this.value[key].isNull(); + }, + true + ); + }; + public toNative = (): GenericObject => { return Object.keys(this.value).reduce((result: GenericObject, key) => { // eslint-disable-next-line no-param-reassign diff --git a/src/Scalars/BooleanScalar.ts b/src/Scalars/BooleanScalar.ts index 87deb60..f258abe 100644 --- a/src/Scalars/BooleanScalar.ts +++ b/src/Scalars/BooleanScalar.ts @@ -21,6 +21,10 @@ export class BooleanScalar extends ValueObject { return this.value === object.value; }; + public isNull = (): boolean => { + return false; + }; + public toNative = (): boolean => { return this.value; }; diff --git a/src/Scalars/FloatScalar.ts b/src/Scalars/FloatScalar.ts index 61f10a0..266229a 100644 --- a/src/Scalars/FloatScalar.ts +++ b/src/Scalars/FloatScalar.ts @@ -13,6 +13,10 @@ export class FloatScalar extends ValueObject { return this.value === object.value; }; + public isNull = (): boolean => { + return false; + }; + public toNative = (): number => { return this.value; }; diff --git a/src/Scalars/IntegerScalar.ts b/src/Scalars/IntegerScalar.ts index 93b7f69..0598e94 100644 --- a/src/Scalars/IntegerScalar.ts +++ b/src/Scalars/IntegerScalar.ts @@ -13,6 +13,10 @@ export class IntegerScalar extends ValueObject { return this.value === object.value; }; + public isNull = (): boolean => { + return false; + }; + public toNative = (): BigInt => { return this.value; }; diff --git a/src/Scalars/NullScalar.ts b/src/Scalars/NullScalar.ts index 49f1793..f6ae977 100644 --- a/src/Scalars/NullScalar.ts +++ b/src/Scalars/NullScalar.ts @@ -13,6 +13,10 @@ export class NullScalar extends ValueObject { return object.value === null; }; + public isNull = (): boolean => { + return true; + }; + public toNative = (): null => { return null; }; diff --git a/src/Scalars/StringScalar.ts b/src/Scalars/StringScalar.ts index 203837b..bf8315d 100644 --- a/src/Scalars/StringScalar.ts +++ b/src/Scalars/StringScalar.ts @@ -13,6 +13,10 @@ export class StringScalar extends ValueObject { return this.value === object.value; }; + public isNull = (): boolean => { + return false; + }; + public toNative = (): string => { return this.value; }; diff --git a/src/Scalars/__tests__/BooleanScalar.test.ts b/src/Scalars/__tests__/BooleanScalar.test.ts index a7fc8ca..d1226fc 100644 --- a/src/Scalars/__tests__/BooleanScalar.test.ts +++ b/src/Scalars/__tests__/BooleanScalar.test.ts @@ -37,6 +37,10 @@ describe("Test BooleanScalar", () => { expect(testBooleanClass.isSame(new BooleanScalar(false))).not.toBeTruthy(); }); + test("isNull() returns false", () => { + expect(testBooleanClass.isNull()).not.toBeTruthy(); + }); + test("isTrue() returns true when true", () => { const t = new BooleanScalar(true); expect(t.isTrue()).toBeTruthy(); diff --git a/src/Scalars/__tests__/FloatScalar.test.ts b/src/Scalars/__tests__/FloatScalar.test.ts index ca4048d..8200513 100644 --- a/src/Scalars/__tests__/FloatScalar.test.ts +++ b/src/Scalars/__tests__/FloatScalar.test.ts @@ -25,4 +25,8 @@ describe("Test FloatScalar", () => { testFloatClass.isSame(new FloatScalar(differentFloat)) ).not.toBeTruthy(); }); + + test("isNull() returns false", () => { + expect(testFloatClass.isNull()).not.toBeTruthy(); + }); }); diff --git a/src/Scalars/__tests__/IntegerScalar.test.ts b/src/Scalars/__tests__/IntegerScalar.test.ts index 12fe75d..cc6d18a 100644 --- a/src/Scalars/__tests__/IntegerScalar.test.ts +++ b/src/Scalars/__tests__/IntegerScalar.test.ts @@ -27,4 +27,8 @@ describe("Test IntegerScalar", () => { testIntegerClass.isSame(new IntegerScalar(differentInteger)) ).not.toBeTruthy(); }); + + test("isNull() returns false", () => { + expect(testIntegerClass.isNull()).not.toBeTruthy(); + }); }); diff --git a/src/Scalars/__tests__/NullScalar.test.ts b/src/Scalars/__tests__/NullScalar.test.ts index 818a6e8..5d324b4 100644 --- a/src/Scalars/__tests__/NullScalar.test.ts +++ b/src/Scalars/__tests__/NullScalar.test.ts @@ -17,4 +17,8 @@ describe("Test Null", () => { test("isSame() returns true when given null valueobject", () => { expect(testStringClass.isSame(new NullScalar())).toBeTruthy(); }); + + test("isNull() returns true", () => { + expect(testStringClass.isNull()).toBeTruthy(); + }); }); diff --git a/src/Scalars/__tests__/StringScalar.test.ts b/src/Scalars/__tests__/StringScalar.test.ts index 557344b..719ada0 100644 --- a/src/Scalars/__tests__/StringScalar.test.ts +++ b/src/Scalars/__tests__/StringScalar.test.ts @@ -24,4 +24,8 @@ describe("Test StringScalar", () => { testStringClass.isSame(new StringScalar("Not the same")) ).not.toBeTruthy(); }); + + test("isNull() returns false", () => { + expect(testStringClass.isNull()).not.toBeTruthy(); + }); }); diff --git a/src/ValueObject.ts b/src/ValueObject.ts index 4aad9e4..7c6c152 100644 --- a/src/ValueObject.ts +++ b/src/ValueObject.ts @@ -13,6 +13,7 @@ export interface ValueObjectInterface value: V; }> { isSame(object: ValueObjectInterface): boolean; + isNull(): boolean; toNative(): Native; } @@ -49,6 +50,8 @@ export abstract class ValueObject implements ValueObjectInterface { abstract isSame(object: ValueObjectInterface): boolean; + abstract isNull(): boolean; + abstract toNative(): Native; } diff --git a/src/__tests__/CompositeValueObject.test.ts b/src/__tests__/CompositeValueObject.test.ts index de72359..e96e5f2 100644 --- a/src/__tests__/CompositeValueObject.test.ts +++ b/src/__tests__/CompositeValueObject.test.ts @@ -2,6 +2,7 @@ import { StringScalar } from "../Scalars/StringScalar"; import { CompositeValueObject } from "../CompositeValueObject"; import { FloatScalar } from "../Scalars/FloatScalar"; import { getType } from "../ValueObject"; +import { NullScalar } from "../Scalars"; class Comp extends CompositeValueObject<{ name: StringScalar; @@ -54,6 +55,47 @@ class ComplexComp extends CompositeValueObject<{ } } +class CompWithNulls extends CompositeValueObject<{ + name: StringScalar; + null1: NullScalar; +}> { + constructor(name: StringScalar, null1: NullScalar) { + super( + { + name, + null1 + }, + Comp + ); + } + + public static fromNative(value: { name: string }): CompWithNulls { + return new this( + StringScalar.fromNative(value.name), + NullScalar.fromNative() + ); + } +} + +class CompOnlyNulls extends CompositeValueObject<{ + null1: NullScalar; + null2: NullScalar; +}> { + constructor(null1: NullScalar, null2: NullScalar) { + super( + { + null1, + null2 + }, + Comp + ); + } + + public static fromNative(): CompOnlyNulls { + return new this(NullScalar.fromNative(), NullScalar.fromNative()); + } +} + describe("Test CompositeValueObject", () => { test("should maintain immutability of the properties of .value property", () => { expect(() => { @@ -137,4 +179,20 @@ describe("Test CompositeValueObject", () => { ); expect(obj.getName().toNative()).toBe("some name"); }); + + test("isNull() returns false when 1 or more properties are not null", () => { + const wNull = new CompWithNulls( + StringScalar.fromNative("some string"), + NullScalar.fromNative() + ); + expect(wNull.isNull()).not.toBeTruthy(); + }); + + test("isNull() returns true when all properties are null", () => { + const wNull = new CompOnlyNulls( + NullScalar.fromNative(), + NullScalar.fromNative() + ); + expect(wNull.isNull()).toBeTruthy(); + }); }); diff --git a/src/__tests__/ValueObject.test.ts b/src/__tests__/ValueObject.test.ts index 5a8d27e..07d3b61 100644 --- a/src/__tests__/ValueObject.test.ts +++ b/src/__tests__/ValueObject.test.ts @@ -9,6 +9,10 @@ class Stub extends ValueObject { return object.value === this.value; }; + public isNull = (): boolean => { + return false; + }; + toNative = (): string => { return this.value; }; @@ -27,6 +31,10 @@ class StubMissingFromNative extends ValueObject { return false; }; + public isNull = (): boolean => { + return false; + }; + toNative = (): string => { return this.value; }; From 64cdf2482349742b689e049607e7dddd5bbd023d Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Sun, 27 Dec 2020 23:17:42 +0000 Subject: [PATCH 3/8] test: add test for CompositeValueObject null --- src/__tests__/CompositeValueObject.test.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/__tests__/CompositeValueObject.test.ts b/src/__tests__/CompositeValueObject.test.ts index e96e5f2..4efec4f 100644 --- a/src/__tests__/CompositeValueObject.test.ts +++ b/src/__tests__/CompositeValueObject.test.ts @@ -80,22 +80,33 @@ class CompWithNulls extends CompositeValueObject<{ class CompOnlyNulls extends CompositeValueObject<{ null1: NullScalar; null2: NullScalar; + null3: NullScalar; }> { - constructor(null1: NullScalar, null2: NullScalar) { + constructor(null1: NullScalar, null2: NullScalar, null3: NullScalar) { super( { null1, - null2 + null2, + null3 }, Comp ); } public static fromNative(): CompOnlyNulls { - return new this(NullScalar.fromNative(), NullScalar.fromNative()); + return new this( + NullScalar.fromNative(), + NullScalar.fromNative(), + NullScalar.fromNative() + ); } } +beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); +}); + describe("Test CompositeValueObject", () => { test("should maintain immutability of the properties of .value property", () => { expect(() => { @@ -190,6 +201,7 @@ describe("Test CompositeValueObject", () => { test("isNull() returns true when all properties are null", () => { const wNull = new CompOnlyNulls( + NullScalar.fromNative(), NullScalar.fromNative(), NullScalar.fromNative() ); From de9bed9ac4062148791649affda5e5afd08a934e Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Sun, 27 Dec 2020 23:19:07 +0000 Subject: [PATCH 4/8] feat: add null check for Enum and NullScalar --- src/EnumValueObject.ts | 4 ++++ src/Scalars/NullScalar.ts | 2 +- src/Scalars/__tests__/NullScalar.test.ts | 5 ++++- src/__tests__/EnumValueObjects.test.ts | 4 ++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/EnumValueObject.ts b/src/EnumValueObject.ts index 1f9f0bb..103289f 100644 --- a/src/EnumValueObject.ts +++ b/src/EnumValueObject.ts @@ -15,6 +15,10 @@ export class EnumValueObject extends ValueObject { return object.value === this.value; }; + public isNull = (): boolean => { + return false; + }; + public toNative = (): EnumValue => { return this.value; }; diff --git a/src/Scalars/NullScalar.ts b/src/Scalars/NullScalar.ts index f6ae977..8da049f 100644 --- a/src/Scalars/NullScalar.ts +++ b/src/Scalars/NullScalar.ts @@ -10,7 +10,7 @@ export class NullScalar extends ValueObject { } public isSame = (object: ValueObject): boolean => { - return object.value === null; + return object.toNative() === null; }; public isNull = (): boolean => { diff --git a/src/Scalars/__tests__/NullScalar.test.ts b/src/Scalars/__tests__/NullScalar.test.ts index 5d324b4..7daf6e2 100644 --- a/src/Scalars/__tests__/NullScalar.test.ts +++ b/src/Scalars/__tests__/NullScalar.test.ts @@ -15,7 +15,10 @@ describe("Test Null", () => { }); test("isSame() returns true when given null valueobject", () => { - expect(testStringClass.isSame(new NullScalar())).toBeTruthy(); + const matched = new NullScalar(); + const matchSpy = jest.spyOn(matched, "toNative"); + expect(testStringClass.isSame(matched)).toBeTruthy(); + expect(matchSpy).toHaveBeenCalled(); }); test("isNull() returns true", () => { diff --git a/src/__tests__/EnumValueObjects.test.ts b/src/__tests__/EnumValueObjects.test.ts index 5e26b4b..47c3781 100644 --- a/src/__tests__/EnumValueObjects.test.ts +++ b/src/__tests__/EnumValueObjects.test.ts @@ -39,6 +39,10 @@ describe("Test EnumValueObject", () => { testStringClass.isSame(new EnumValueObject(differentString)) ).not.toBeTruthy(); }); + + test("isNull() returns false", () => { + expect(testStringClass.isNull()).not.toBeTruthy(); + }); }); describe("test Eunerate() mixin", () => { From 11f4de3cfd5a9e466c4ec20c95db5d421b225533 Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Sun, 27 Dec 2020 23:20:18 +0000 Subject: [PATCH 5/8] refactor: introduce ObjectUtils to improve testability and code reuse --- src/CompositeValueObject.ts | 42 +++------------------- src/Utills/ObjectUtils.ts | 39 ++++++++++++++++++++ src/Utills/__tests__/ObjectUtils.test.ts | 46 ++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 src/Utills/ObjectUtils.ts create mode 100644 src/Utills/__tests__/ObjectUtils.test.ts diff --git a/src/CompositeValueObject.ts b/src/CompositeValueObject.ts index 8fb3664..84f8d10 100644 --- a/src/CompositeValueObject.ts +++ b/src/CompositeValueObject.ts @@ -1,4 +1,5 @@ import { ValueObject, GenericObject } from "./ValueObject"; +import { ObjectUtils } from "./Utills/ObjectUtils"; export type CompositeProperties = Record>; @@ -10,43 +11,10 @@ export class CompositeValueObject< } public isSame = (object: ValueObject): boolean => { - const isObject = (obj: unknown): boolean => { - return obj != null && typeof obj === "object"; - }; - - const isObjectEqual = ( - object1: GenericObject, - object2: GenericObject - ): boolean => { - const keys1 = Object.keys(object1); - const keys2 = Object.keys(object2); - - if (keys1.length !== keys2.length) { - return false; - } - - return keys1.reduce((result: boolean, key: string): boolean => { - if (result === false) { - return false; - } - - const val1 = object1[key]; - const val2 = object2[key]; - const areObjects = isObject(val1) && isObject(val2); - - if ( - (areObjects && - !isObjectEqual(val1 as GenericObject, val2 as GenericObject)) || - (!areObjects && val1 !== val2) - ) { - return false; - } - - return true; - }, true); - }; - - return isObjectEqual(this.toNative(), object.toNative() as GenericObject); + return ObjectUtils.isObjectEqual( + this.toNative(), + object.toNative() as GenericObject + ); }; public isNull = (): boolean => { diff --git a/src/Utills/ObjectUtils.ts b/src/Utills/ObjectUtils.ts new file mode 100644 index 0000000..bf12e17 --- /dev/null +++ b/src/Utills/ObjectUtils.ts @@ -0,0 +1,39 @@ +import { GenericObject } from "../ValueObject"; + +export const ObjectUtils = { + isObject: (obj: unknown): boolean => { + return obj != null && typeof obj === "object"; + }, + isObjectEqual: (object1: GenericObject, object2: GenericObject): boolean => { + const keys1 = Object.keys(object1); + const keys2 = Object.keys(object2); + + if (keys1.length !== keys2.length) { + return false; + } + + return keys1.reduce((result: boolean, key: string): boolean => { + if (result === false) { + return false; + } + + const val1 = object1[key]; + const val2 = object2[key]; + const areObjects = + ObjectUtils.isObject(val1) && ObjectUtils.isObject(val2); + + if ( + (areObjects && + !ObjectUtils.isObjectEqual( + val1 as GenericObject, + val2 as GenericObject + )) || + (!areObjects && val1 !== val2) + ) { + return false; + } + + return true; + }, true); + } +}; diff --git a/src/Utills/__tests__/ObjectUtils.test.ts b/src/Utills/__tests__/ObjectUtils.test.ts new file mode 100644 index 0000000..240b26d --- /dev/null +++ b/src/Utills/__tests__/ObjectUtils.test.ts @@ -0,0 +1,46 @@ +import { GenericObject } from "../../ValueObject"; +import { ObjectUtils } from "../ObjectUtils"; + +describe("Test isObjectEqual", () => { + test("returns false if keys lengths are different", () => { + expect( + ObjectUtils.isObjectEqual({ key1: "oo" }, { key1: "oo", key2: "oo" }) + ).not.toBeTruthy(); + }); + + test("returns true for exactly matching simple objects", () => { + expect( + ObjectUtils.isObjectEqual({ key1: "oo" }, { key1: "oo" }) + ).toBeTruthy(); + }); + + test("returns true for exactly matching nested objects", () => { + expect( + ObjectUtils.isObjectEqual( + { key1: "oo", key2: { n1: "1" } }, + { key1: "oo", key2: { n1: "1" } } + ) + ).toBeTruthy(); + }); + + test("returns false for not matching nested objects", () => { + expect( + ObjectUtils.isObjectEqual( + { key1: "oo", key2: { n1: "1" } }, + { key1: "oo", key2: "1" } + ) + ).not.toBeTruthy(); + }); +}); + +describe("Test isObject", () => { + test("returns true if is object", () => { + expect(ObjectUtils.isObject({ key: "value" })).toBeTruthy(); + }); + test("returns false if is not object", () => { + expect(ObjectUtils.isObject("this is not an object")).not.toBeTruthy(); + }); + test("returns false if is null", () => { + expect(ObjectUtils.isObject(null)).not.toBeTruthy(); + }); +}); From 0623a1e39d02a3d4b26426df4b9a6706db615f9a Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Fri, 8 Jan 2021 23:37:23 +0000 Subject: [PATCH 6/8] refactor: change the way fromNative is managed and remove need for defining "type" BREAKING CHANGE: value objects no longer require the second "type" argument to the super method in the constructor and it has been removed --- src/CompositeValueObject.ts | 4 +- src/EnumValueObject.ts | 4 -- src/Scalars/BooleanScalar.ts | 4 -- src/Scalars/FloatScalar.ts | 4 -- src/Scalars/IntegerScalar.ts | 4 -- src/Scalars/NullScalar.ts | 2 +- src/Scalars/StringScalar.ts | 4 -- src/Utills/__tests__/ObjectUtils.test.ts | 1 - src/ValueObject.ts | 16 +++----- src/__tests__/CompositeValueObject.test.ts | 46 ++++++++-------------- src/__tests__/ValueObject.test.ts | 18 +++------ 11 files changed, 31 insertions(+), 76 deletions(-) diff --git a/src/CompositeValueObject.ts b/src/CompositeValueObject.ts index 84f8d10..c9ff49a 100644 --- a/src/CompositeValueObject.ts +++ b/src/CompositeValueObject.ts @@ -6,8 +6,8 @@ export type CompositeProperties = Record>; export class CompositeValueObject< T extends CompositeProperties > extends ValueObject { - constructor(args: T, type: unknown) { - super(Object.freeze(args), type); + constructor(args: T) { + super(Object.freeze(args)); } public isSame = (object: ValueObject): boolean => { diff --git a/src/EnumValueObject.ts b/src/EnumValueObject.ts index 103289f..d13d651 100644 --- a/src/EnumValueObject.ts +++ b/src/EnumValueObject.ts @@ -3,10 +3,6 @@ import { ValueObject, ValueObjectConstructor } from "./ValueObject"; type EnumValue = string | number; export class EnumValueObject extends ValueObject { - constructor(value: EnumValue) { - super(value, EnumValueObject); - } - public static fromNative(value: EnumValue): EnumValueObject { return new this(value); } diff --git a/src/Scalars/BooleanScalar.ts b/src/Scalars/BooleanScalar.ts index f258abe..f092c9a 100644 --- a/src/Scalars/BooleanScalar.ts +++ b/src/Scalars/BooleanScalar.ts @@ -1,10 +1,6 @@ import { ValueObject } from "../ValueObject"; export class BooleanScalar extends ValueObject { - constructor(value: boolean) { - super(value, BooleanScalar); - } - public static fromNative(value: boolean): BooleanScalar { return new this(value); } diff --git a/src/Scalars/FloatScalar.ts b/src/Scalars/FloatScalar.ts index 266229a..d6f6080 100644 --- a/src/Scalars/FloatScalar.ts +++ b/src/Scalars/FloatScalar.ts @@ -1,10 +1,6 @@ import { ValueObject } from "../ValueObject"; export class FloatScalar extends ValueObject { - constructor(value: number) { - super(value, FloatScalar); - } - public static fromNative(value: number): FloatScalar { return new this(value); } diff --git a/src/Scalars/IntegerScalar.ts b/src/Scalars/IntegerScalar.ts index 0598e94..2251f3b 100644 --- a/src/Scalars/IntegerScalar.ts +++ b/src/Scalars/IntegerScalar.ts @@ -1,10 +1,6 @@ import { ValueObject } from "../ValueObject"; export class IntegerScalar extends ValueObject { - constructor(value: BigInt) { - super(value, IntegerScalar); - } - public static fromNative(value: BigInt): ValueObject { return new this(value); } diff --git a/src/Scalars/NullScalar.ts b/src/Scalars/NullScalar.ts index 8da049f..498e4f9 100644 --- a/src/Scalars/NullScalar.ts +++ b/src/Scalars/NullScalar.ts @@ -2,7 +2,7 @@ import { ValueObject } from "../ValueObject"; export class NullScalar extends ValueObject { constructor() { - super(null, NullScalar); + super(null); } public static fromNative(): NullScalar { diff --git a/src/Scalars/StringScalar.ts b/src/Scalars/StringScalar.ts index bf8315d..dfa6933 100644 --- a/src/Scalars/StringScalar.ts +++ b/src/Scalars/StringScalar.ts @@ -1,10 +1,6 @@ import { ValueObject } from "../ValueObject"; export class StringScalar extends ValueObject { - constructor(value: string) { - super(value, StringScalar); - } - public static fromNative(value: string): StringScalar { return new this(value); } diff --git a/src/Utills/__tests__/ObjectUtils.test.ts b/src/Utills/__tests__/ObjectUtils.test.ts index 240b26d..ee260e6 100644 --- a/src/Utills/__tests__/ObjectUtils.test.ts +++ b/src/Utills/__tests__/ObjectUtils.test.ts @@ -1,4 +1,3 @@ -import { GenericObject } from "../../ValueObject"; import { ObjectUtils } from "../ObjectUtils"; describe("Test isObjectEqual", () => { diff --git a/src/ValueObject.ts b/src/ValueObject.ts index 7c6c152..61d6dc6 100644 --- a/src/ValueObject.ts +++ b/src/ValueObject.ts @@ -31,21 +31,11 @@ export const hasMember = ( return Object.keys(obj).filter((method) => method === property).length !== 0; }; -const enforceFromNative = ( - obj: ValueObjectInterface, - c: O -): void => { - if (Object.keys(c).filter((method) => method === "fromNative").length === 0) { - throw Error(`${getType(obj)} must include a fromNative method`); - } -}; - export abstract class ValueObject implements ValueObjectInterface { readonly value: V; - constructor(value: V, type: unknown) { + constructor(value: V) { this.value = value; - enforceFromNative(this, type); } abstract isSame(object: ValueObjectInterface): boolean; @@ -53,6 +43,10 @@ export abstract class ValueObject implements ValueObjectInterface { abstract isNull(): boolean; abstract toNative(): Native; + + public static fromNative(value: any): any { + throw Error(`Value objects must implement a fromNative method`); + } } export function DomainObjectFrom( diff --git a/src/__tests__/CompositeValueObject.test.ts b/src/__tests__/CompositeValueObject.test.ts index 4efec4f..6d7df98 100644 --- a/src/__tests__/CompositeValueObject.test.ts +++ b/src/__tests__/CompositeValueObject.test.ts @@ -9,13 +9,10 @@ class Comp extends CompositeValueObject<{ number: FloatScalar; }> { constructor(name: StringScalar, number: FloatScalar) { - super( - { - name, - number - }, - Comp - ); + super({ + name, + number + }); } public static fromNative(value: { name: string; number: number }): Comp { @@ -35,13 +32,10 @@ class ComplexComp extends CompositeValueObject<{ composite: Comp; }> { constructor(name: StringScalar, composite: Comp) { - super( - { - name, - composite - }, - Comp - ); + super({ + name, + composite + }); } public static fromNative(value: { @@ -60,13 +54,10 @@ class CompWithNulls extends CompositeValueObject<{ null1: NullScalar; }> { constructor(name: StringScalar, null1: NullScalar) { - super( - { - name, - null1 - }, - Comp - ); + super({ + name, + null1 + }); } public static fromNative(value: { name: string }): CompWithNulls { @@ -83,14 +74,11 @@ class CompOnlyNulls extends CompositeValueObject<{ null3: NullScalar; }> { constructor(null1: NullScalar, null2: NullScalar, null3: NullScalar) { - super( - { - null1, - null2, - null3 - }, - Comp - ); + super({ + null1, + null2, + null3 + }); } public static fromNative(): CompOnlyNulls { diff --git a/src/__tests__/ValueObject.test.ts b/src/__tests__/ValueObject.test.ts index 07d3b61..d0439e4 100644 --- a/src/__tests__/ValueObject.test.ts +++ b/src/__tests__/ValueObject.test.ts @@ -1,10 +1,6 @@ import { ValueObject, getType, DomainObjectFrom } from "../ValueObject"; class Stub extends ValueObject { - constructor(value: string) { - super(value, Stub); - } - isSame = (object: ValueObject): boolean => { return object.value === this.value; }; @@ -23,10 +19,6 @@ class Stub extends ValueObject { } class StubMissingFromNative extends ValueObject { - constructor(value: string) { - super(value, StubMissingFromNative); - } - isSame = (): boolean => { return false; }; @@ -45,10 +37,10 @@ describe("Test ValueObject abstract class()", () => { expect(getType(new Stub("jh"))).toBe("Stub"); }); - test("should throw exception for 'StubMissingFromNative' class that doesn't include fromNative() method", () => { + test("fromNative() should throw exception class instantiation with no fromNative method", () => { expect(() => { - getType(new StubMissingFromNative("jh")); - }).toThrow("StubMissingFromNative must include a fromNative method"); + getType(StubMissingFromNative.fromNative("jh")); + }).toThrow("Value objects must implement a fromNative method"); }); }); @@ -68,7 +60,7 @@ describe("Test DomainObjectFrom() mixin", () => { expect(domainInstance.isSame(stubInstance)).toBeTruthy(); }); - test("should enforce property requirement that defined uniqueness", () => { + test("should enforce property requirement that defines uniqueness", () => { const domainString = "this string"; class DomainObject extends DomainObjectFrom(Stub) {} const DomainObject2 = DomainObjectFrom(Stub); @@ -113,6 +105,7 @@ describe("Test DomainObjectFrom() mixin", () => { expect( testFunc(new DomainObject1(testString), new DomainObject2(testString)) ).toBeTruthy(); + expect(new DomainObject1(testString)).not.toBeInstanceOf(DomainObject2); // compiler should complain about this // testFunc(new DomainObject2(testString), new DomainObject1(testString)); @@ -127,6 +120,7 @@ describe("Test DomainObjectFrom() mixin", () => { const obj = DomainObject.fromNative("Some String"); expect(getType(obj)).toBe("DomainObject"); + expect(obj).toBeInstanceOf(DomainObject); expect(obj.toNative()).toBe("Some String"); }); }); From d4beacc0dffe81c406711f742c4d13b05a97706c Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Sat, 9 Jan 2021 14:58:40 +0000 Subject: [PATCH 7/8] feat: add NullableValueObject --- README.md | 49 ++++++++++++++- src/NullableValueObject.ts | 41 +++++++++++++ src/Utills/index.ts | 1 + src/__tests__/NullableValueObject.test.ts | 74 +++++++++++++++++++++++ src/index.ts | 2 + 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 src/NullableValueObject.ts create mode 100644 src/Utills/index.ts create mode 100644 src/__tests__/NullableValueObject.test.ts diff --git a/README.md b/README.md index 9f5494e..c552574 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ class User extends CompositeValueObject<{ name, email, isRegistered - }, User); + }); } public static fromNative(value: { name: string; email: string, isRegistered: boolean }): User { @@ -196,4 +196,49 @@ class EmailAddress extends DomainObjectFrom( } } ) { } -``` \ No newline at end of file +``` + +## Nullable Value Objects +The abstract `NullableValueObject` class allows wrapping a `null` and a non-`null` implementation into the same interface as a `ValueObjectInterface`. You just have to define 3 static methods: `fromNative()` which does the null / non-null negotiation, and, `nonNullImplementation()` and `nullImplementation()` which return the relevant implementations for the non-null and the null conditions. These methods should each return a `ValueObjectInterface`. By default `NullableValueObject` includes a `nullImplementation()` that returns a `NullScalar`. However this can be overridden and return any `ValueObjectInterface` implementation you like. + +```typescript +class NullableUserName extends NullableValueObject { + public static fromNative(value: NullOr): NullableUserName { + return new this(this.getWhichNullImplementation(value)); + } + + public static nonNullImplementation(value: string): StringScalar { + return new StringScalar("fixed string"); + } +} + +const nullVersion = NullableUserName.fromNative(null); +console.log(nullVersion.isNull()) // -> true + +const nonNullVersion = NullableUserName.fromNative("John Doe"); +console.log(nonNullVersion.isNull()) // -> false + +console.log(nonNullVersion.isSame(nullVersion)) // -> false +``` + +Optionally override the default `nullImplementation()`: +```typescript +class NullImplementationValueObject extends ValueObject { + ... +} + +class NullableUserName extends NullableValueObject { + public static fromNative(value: NullOr): NullableUserName { + return new this(this.getWhichNullImplementation(value)); + } + + public static nullImplementation(): StringScalar { + return new NullImplementationValueObject(); + } + + public static nonNullImplementation(value: string): StringScalar { + return new StringScalar("fixed string"); + } +} +``` + \ No newline at end of file diff --git a/src/NullableValueObject.ts b/src/NullableValueObject.ts new file mode 100644 index 0000000..3676770 --- /dev/null +++ b/src/NullableValueObject.ts @@ -0,0 +1,41 @@ +import { NullScalar } from "./Scalars"; +import { Native, ValueObject, ValueObjectInterface } from "./ValueObject"; + +export type NullOr = V | null; + +export abstract class NullableValueObject extends ValueObject< + ValueObjectInterface> +> { + public isNull = (): boolean => { + return this.value.isNull(); + }; + + public isSame = ( + object: ValueObjectInterface>> + ): boolean => { + return this.value.isSame(object.value); + }; + + public toNative = (): Native => { + return this.value.toNative(); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected static getWhichNullImplementation(value: Native): any { + if (value === null) { + return this.nullImplementation(); + } + return this.nonNullImplementation(value); + } + + static nullImplementation(): unknown { + return new NullScalar(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static nonNullImplementation(value: unknown): unknown { + throw new Error( + "nonNullImplementation() Method must be implemented in child class." + ); + } +} diff --git a/src/Utills/index.ts b/src/Utills/index.ts new file mode 100644 index 0000000..2deb7a6 --- /dev/null +++ b/src/Utills/index.ts @@ -0,0 +1 @@ +export * from "./ObjectUtils"; diff --git a/src/__tests__/NullableValueObject.test.ts b/src/__tests__/NullableValueObject.test.ts new file mode 100644 index 0000000..22f5e36 --- /dev/null +++ b/src/__tests__/NullableValueObject.test.ts @@ -0,0 +1,74 @@ +import { NullableValueObject, NullOr } from "../NullableValueObject"; +import { StringScalar } from "../Scalars/StringScalar"; + +class NullableStringObject extends NullableValueObject { + public static fromNative(value: NullOr): NullableStringObject { + return new this(this.getWhichNullImplementation(value)); + } + + public static nullImplementation(): StringScalar { + // so we know this is being called + return new StringScalar("null-implementation"); + } + + public static nonNullImplementation(value: string): StringScalar { + return new StringScalar(value); + } +} + +class NullableProperStringObject extends NullableValueObject { + public static fromNative(value: NullOr): NullableStringObject { + return new this(this.getWhichNullImplementation(value)); + } + + public static nonNullImplementation(value: string): StringScalar { + return new StringScalar("fixed string"); + } +} + +class NullableMissingNonNullImplementation extends NullableValueObject { + public static fromNative(value: NullOr): NullableStringObject { + return new this(this.getWhichNullImplementation(value)); + } +} + +beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); +}); + +describe("Test NullableValueObject", () => { + test("isNull() should proxy to wrapped value object", () => { + const obj1 = NullableProperStringObject.fromNative(null); + expect(obj1.isNull()).toBeTruthy(); + }); + + test("isSame() should proxy to wrapped value object", () => { + const obj1 = NullableProperStringObject.fromNative("some string"); + const obj2 = NullableStringObject.fromNative("some string"); + expect(obj1.isSame(obj2)).toBeFalsy(); + }); + + test("toNative() should proxy to wrapped value object", () => { + const obj = NullableProperStringObject.fromNative("some string"); + expect(obj.toNative()).toBe("fixed string"); + }); + + test("fromNative() should create a null value object when it recieves null", () => { + const obj = NullableStringObject.fromNative(null); + expect(obj.toNative()).toBe("null-implementation"); + }); + + test("fromNative() should create a non-null value object when it recieves non-null", () => { + const obj = NullableStringObject.fromNative("some value"); + expect(obj.toNative()).toBe("some value"); + }); + + test("class missing nonNullImplementation should throw exception", () => { + expect(() => { + NullableMissingNonNullImplementation.fromNative("some string"); + }).toThrowError( + "nonNullImplementation() Method must be implemented in child class." + ); + }); +}); diff --git a/src/index.ts b/src/index.ts index bd6a22d..f33f52c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ export * from "./Scalars"; +export * from "./Utills"; export * from "./ValueObject"; export * from "./EnumValueObject"; export * from "./CompositeValueObject"; +export * from "./NullableValueObject"; From aed11202b8da99de292817db3e7874eb4f286fef Mon Sep 17 00:00:00 2001 From: Kev Baldwyn Date: Sat, 9 Jan 2021 15:16:48 +0000 Subject: [PATCH 8/8] docs: improve documentation and examples, add ToC --- README.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c552574..4f6dd79 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,21 @@ -# TS Value Objects - ![Build](https://github.com/kevbaldwyn/ts-valueobjects/workflows/Build/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/kevbaldwyn/ts-valueobjects/badge.svg?branch=master)](https://coveralls.io/github/kevbaldwyn/ts-valueobjects?branch=master) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fkevbaldwyn%2Fts-valueobjects%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/kevbaldwyn/ts-valueobjects/master) +- [Core principles](#core-principles) + - [Inspiration](#inspiration) +- [ValueObjectInterface](#valueobjectinterfacet) +- [Type helper classes](#type-helper-classes) + - [StringScalar](#stringscalar) + - [FloatScalar](#floatscalar) + - [BooleanScalar](#booleanscalar) + - [IntegerScalar](#integerscalar) + - [NullScalar](#nullscalar) + - [Enum Type Helper](#enum-type-helper) +- [Composite Value Objects](#composite-value-objects) +- [Domain Value Objects](#domain-value-objects) +- [Nullable Value Objects](#nullable-value-objects) + ## Core principles This package is built around 3 core principles: 1. Value objects MUST be immutable @@ -54,8 +66,9 @@ const someCoordinate: ValueObjectInterface<{x:number, y:number}> = { ``` ## Type helper classes -Creating lots of value objects this way can get verbose so you can use some of the included classes for creating common scalar types (`StringScalar`, `FloatScalar`, `IntegerScalar`, `BooleanScalar`, `NullScalar`), and other more complex types such as `EnumValueObject`. +Creating lots of value objects this way can get verbose so you can use some of the included classes for creating common scalar types (`StringScalar`, `FloatScalar`, `IntegerScalar`, `BooleanScalar`, `NullScalar`). +### StringScalar ```typescript import { StringScalar } from "ts-valueobjects"; @@ -71,7 +84,59 @@ const anotherEmailAddress = StringScalar.fromNative('another-email@example.com') console.log(anEmailAddress.isSame(anotherEmailAddress)); // false ``` -## Enum Type Helper +### FloatScalar +```typescript +import { FloatScalar } from "ts-valueobjects"; + +const floatValue = FloatScalar.fromNative(23.5); +const floatValue = new FloatScalar(23.5); + +floatValue.isNull(); +floatValue.isSame(...); +floatValue.toNative(); +``` + +### BooleanScalar +```typescript +import { BooleanScalar } from "ts-valueobjects"; + +const boolValue = BooleanScalar.true(); +const boolValue = BooleanScalar.false(); +const boolValue = BooleanScalar.fromNatiave(true); +const boolValue = new BooleanScalar(true); + +boolValue.isNull(); +boolValue.isSame(...); +boolValue.toNative(); +boolValue.isTrue(); +boolValue.isFalse(); +``` + +### IntegerScalar +```typescript +import { IntegerScalar } from "ts-valueobjects"; + +const integerValue = IntegerScalar.fromNative(BigInt(1)); +const integerValue = new IntegerScalar(BigInt(1)); + +integerValue.isNull(); +integerValue.isSame(...); +integerValue.toNative(); +``` + +### NullScalar +```typescript +import { NullScalar } from "ts-valueobjects"; + +const nullValue = NullScalar.fromNative(); +const nullValue = new NullScalar(); + +integerValue.isNull(); +integerValue.isSame(...); +integerValue.toNative(); +``` + +### Enum Type Helper Using the helper for cretaing Enums will throw errors when trying to access properties that do not exist: ```typescript import { Enumerate, EnumValueObject } from "ts-valueobjects"; @@ -92,6 +157,8 @@ const value = Enumerated.fromNative(Enumerated.VAL1); // ok The `CompositeValueObject` allows you to create value objects that are more complex and contain any number of other value objects (including nested `CompositeValueObject`s and Domain Objects). ```typescript +import { CompositeValueObject } from "ts-valueobjects"; + class User extends CompositeValueObject<{ name: StringScalar; email: StringScalar; @@ -202,6 +269,8 @@ class EmailAddress extends DomainObjectFrom( The abstract `NullableValueObject` class allows wrapping a `null` and a non-`null` implementation into the same interface as a `ValueObjectInterface`. You just have to define 3 static methods: `fromNative()` which does the null / non-null negotiation, and, `nonNullImplementation()` and `nullImplementation()` which return the relevant implementations for the non-null and the null conditions. These methods should each return a `ValueObjectInterface`. By default `NullableValueObject` includes a `nullImplementation()` that returns a `NullScalar`. However this can be overridden and return any `ValueObjectInterface` implementation you like. ```typescript +import { NullableValueObject, NullOr, StringScalar } from "ts-valueobjects"; + class NullableUserName extends NullableValueObject { public static fromNative(value: NullOr): NullableUserName { return new this(this.getWhichNullImplementation(value));