Skip to content

Commit

Permalink
Merge pull request #5 from kevbaldwyn/1.2
Browse files Browse the repository at this point in the history
Nullable VOs and simplified bootstrap required to create VOs
  • Loading branch information
kevbaldwyn authored Jan 9, 2021
2 parents 29050b2 + aed1120 commit 7f49556
Show file tree
Hide file tree
Showing 24 changed files with 486 additions and 102 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Build

on: [push, pull_request]
on: [pull_request]

jobs:
build:
Expand Down
126 changes: 120 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<T>](#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
Expand Down Expand Up @@ -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";

Expand All @@ -71,7 +84,59 @@ const anotherEmailAddress = StringScalar.fromNative('[email protected]')
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";
Expand All @@ -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;
Expand All @@ -102,7 +169,7 @@ class User extends CompositeValueObject<{
name,
email,
isRegistered
}, User);
});
}

public static fromNative(value: { name: string; email: string, isRegistered: boolean }): User {
Expand Down Expand Up @@ -196,4 +263,51 @@ class EmailAddress extends DomainObjectFrom(
}
}
) { }
```
```

## 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
import { NullableValueObject, NullOr, StringScalar } from "ts-valueobjects";

class NullableUserName extends NullableValueObject<string> {
public static fromNative(value: NullOr<string>): 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<null> {
...
}

class NullableUserName extends NullableValueObject<string> {
public static fromNative(value: NullOr<string>): 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");
}
}
```

50 changes: 15 additions & 35 deletions src/CompositeValueObject.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,32 @@
import { ValueObject, GenericObject } from "./ValueObject";
import { ObjectUtils } from "./Utills/ObjectUtils";

export type CompositeProperties = Record<string, ValueObject<unknown>>;

export class CompositeValueObject<
T extends CompositeProperties
> extends ValueObject<T> {
constructor(args: T, type: unknown) {
super(Object.freeze(args), type);
constructor(args: T) {
super(Object.freeze(args));
}

public isSame = (object: ValueObject<CompositeProperties>): 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 ObjectUtils.isObjectEqual(
this.toNative(),
object.toNative() as GenericObject
);
};

return keys1.reduce((result: boolean, key: string): boolean => {
public isNull = (): boolean => {
return Object.keys(this.value).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 this.value[key].isNull();
},
true
);
};

public toNative = (): GenericObject => {
Expand Down
8 changes: 4 additions & 4 deletions src/EnumValueObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import { ValueObject, ValueObjectConstructor } from "./ValueObject";
type EnumValue = string | number;

export class EnumValueObject extends ValueObject<EnumValue> {
constructor(value: EnumValue) {
super(value, EnumValueObject);
}

public static fromNative(value: EnumValue): EnumValueObject {
return new this(value);
}
Expand All @@ -15,6 +11,10 @@ export class EnumValueObject extends ValueObject<EnumValue> {
return object.value === this.value;
};

public isNull = (): boolean => {
return false;
};

public toNative = (): EnumValue => {
return this.value;
};
Expand Down
41 changes: 41 additions & 0 deletions src/NullableValueObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NullScalar } from "./Scalars";
import { Native, ValueObject, ValueObjectInterface } from "./ValueObject";

export type NullOr<V> = V | null;

export abstract class NullableValueObject<V> extends ValueObject<
ValueObjectInterface<NullOr<V>>
> {
public isNull = (): boolean => {
return this.value.isNull();
};

public isSame = (
object: ValueObjectInterface<ValueObjectInterface<NullOr<V>>>
): 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."
);
}
}
8 changes: 4 additions & 4 deletions src/Scalars/BooleanScalar.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { ValueObject } from "../ValueObject";

export class BooleanScalar extends ValueObject<boolean> {
constructor(value: boolean) {
super(value, BooleanScalar);
}

public static fromNative(value: boolean): BooleanScalar {
return new this(value);
}
Expand All @@ -21,6 +17,10 @@ export class BooleanScalar extends ValueObject<boolean> {
return this.value === object.value;
};

public isNull = (): boolean => {
return false;
};

public toNative = (): boolean => {
return this.value;
};
Expand Down
8 changes: 4 additions & 4 deletions src/Scalars/FloatScalar.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { ValueObject } from "../ValueObject";

export class FloatScalar extends ValueObject<number> {
constructor(value: number) {
super(value, FloatScalar);
}

public static fromNative(value: number): FloatScalar {
return new this(value);
}
Expand All @@ -13,6 +9,10 @@ export class FloatScalar extends ValueObject<number> {
return this.value === object.value;
};

public isNull = (): boolean => {
return false;
};

public toNative = (): number => {
return this.value;
};
Expand Down
8 changes: 4 additions & 4 deletions src/Scalars/IntegerScalar.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { ValueObject } from "../ValueObject";

export class IntegerScalar extends ValueObject<BigInt> {
constructor(value: BigInt) {
super(value, IntegerScalar);
}

public static fromNative(value: BigInt): ValueObject<BigInt> {
return new this(value);
}
Expand All @@ -13,6 +9,10 @@ export class IntegerScalar extends ValueObject<BigInt> {
return this.value === object.value;
};

public isNull = (): boolean => {
return false;
};

public toNative = (): BigInt => {
return this.value;
};
Expand Down
8 changes: 6 additions & 2 deletions src/Scalars/NullScalar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { ValueObject } from "../ValueObject";

export class NullScalar extends ValueObject<null> {
constructor() {
super(null, NullScalar);
super(null);
}

public static fromNative(): NullScalar {
return new this();
}

public isSame = (object: ValueObject<null>): boolean => {
return object.value === null;
return object.toNative() === null;
};

public isNull = (): boolean => {
return true;
};

public toNative = (): null => {
Expand Down
Loading

0 comments on commit 7f49556

Please sign in to comment.