Skip to content

Commit

Permalink
feat: rework ValueObject to HObject (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mararok authored Sep 23, 2024
1 parent 87cb7ec commit 67c79c4
Show file tree
Hide file tree
Showing 26 changed files with 307 additions and 306 deletions.
29 changes: 29 additions & 0 deletions src/Domain/ValueObject/AbstractValueObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
JsonSerialize,
LogicError,
type HObjectType,
type PlainParseError,
type R
} from '../../Util';

export type AnyValueObject = AbstractValueObject<any>;
export type ValueObjectType<T extends AnyValueObject = AnyValueObject> = HObjectType<T>;

export abstract class AbstractValueObject<T extends AnyValueObject> implements JsonSerialize {
/**
* Creates ValueObject from plain value
* @param this
* @param plain
* @returns
*/
public static parse<T>(this: (new (...args: any[]) => any), plain: unknown): R<T, PlainParseError> {
throw new LogicError('Not implemented or AOT generated');
}

public abstract equals(o: T): boolean;
public abstract toString(): string;

public toJSON(): any {
throw new LogicError('Not implemented or AOT generated');
}
}
7 changes: 4 additions & 3 deletions src/Domain/ValueObject/AccountId.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HObjectTypeMeta } from "../../Util";
import { StringValue } from "./StringValue";
import { ValueObject } from "./ValueObject";

@ValueObject('Account')
export class AccountId extends StringValue<AccountId> {}
export class AccountId extends StringValue<AccountId> {
public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Account', 'ValueObject', 'AccountId', AccountId);
}
53 changes: 23 additions & 30 deletions src/Domain/ValueObject/DateTime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AbstractValueObject, ValueObject } from './ValueObject';
import { AbstractValueObject, type ValueObjectType } from './AbstractValueObject';
import { OK, R } from '../../Util/Result';
import { DateTimeFormatter, Duration, Instant, LocalDateTime, Period, ZoneId, ZoneOffset, convert } from '@js-joda/core';
import { HObjectTypeMeta, InvalidStringPlainParseIssue, InvalidTypePlainParseIssue, PlainParseHelper, TooSmallPlainParseIssue, type PlainParsableHObjectType, type PlainParseError } from "../../Util";

export type DateTimeRawType = number;
export const DEFAULT_DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
Expand All @@ -25,11 +26,12 @@ function createJsJodaFromTimestamp(v: number): LocalDateTime {
return LocalDateTime.ofEpochSecond(v, ZoneOffset.UTC);
}

@ValueObject('Core')
/**
* DateTime in UTC zone value object
*/
export class DateTime extends AbstractValueObject<DateTime> {
public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'DateTime', DateTime);

public constructor(private readonly value: LocalDateTime) {
super();
}
Expand All @@ -42,34 +44,26 @@ export class DateTime extends AbstractValueObject<DateTime> {
return new this(LocalDateTime.now(ZoneOffset.UTC));
}

/**
* Creates instance from various primitive value like Date, timestamp or formatted datetime string.
* @param v
* @returns instance
*/
public static c(v: Date | number | string): R<DateTime> {

switch (typeof v) {
case 'number': return this.fromTimestamp(v);
public static parse<T extends DateTime>(this: ValueObjectType<T>, plain: unknown): R<T, PlainParseError> {
switch (typeof plain) {
case 'number': return DateTime.fromTimestamp(plain) as any;
case 'string':
try {
return OK(new this(createJsJodaFromString(v)));
return OK(new this(createJsJodaFromString(plain)));
} catch (e) {
return AbstractValueObject.invalidRaw(DateTime, {
raw: v,
msg: 'invalid format: ' + (e as Error).message,
});
return PlainParseHelper.HObjectParseErr(DateTime, [
new InvalidStringPlainParseIssue('datetime', {}, 'Given plain string is not valid datetime')
]) as any;
}
}

if (v instanceof Date) {
return OK(new this(createJsJodaFromDate(v)));
if (plain instanceof Date) {
return OK(new this(createJsJodaFromDate(plain)));
}

return AbstractValueObject.invalidRaw(DateTime, {
raw: v,
msg: 'unsupported datetime raw type',
});
return PlainParseHelper.HObjectParseErr(DateTime, [
new InvalidTypePlainParseIssue(["number", "string", "Date"], typeof plain)
]) as any;
}

/**
Expand All @@ -94,14 +88,13 @@ export class DateTime extends AbstractValueObject<DateTime> {
* @param timestamp
* @returns
*/
public static fromTimestamp(timestamp: number): R<DateTime> {
public static fromTimestamp(timestamp: number): R<DateTime, PlainParseError> {
if (timestamp < 0) {
return AbstractValueObject.invalidRaw(DateTime, {
raw: timestamp,
msg: 'invalid timestamp',
});
return PlainParseHelper.HObjectParseErr(DateTime, [
TooSmallPlainParseIssue.numberGTE(0, timestamp)
]);
}
return OK(new this(createJsJodaFromTimestamp(timestamp)));
return OK(new DateTime(createJsJodaFromTimestamp(timestamp)));
}

/**
Expand Down Expand Up @@ -176,7 +169,7 @@ export class DateTime extends AbstractValueObject<DateTime> {
return this.formatDateTime();
}

public toJSON(): any {
public toJSON(): number {
return this.t;
}
}
}
10 changes: 4 additions & 6 deletions src/Domain/ValueObject/Email.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { ValueObject } from './ValueObject';
import { HObjectTypeMeta } from "../../Util";
import { EmailHash } from './EmailHash';
import { RegexStringIdRawType, RegexStringValue } from './RegexStringValue';
import { RegexStringValue } from './RegexStringValue';

const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;

export type EmailRawType = RegexStringIdRawType;

@ValueObject('Core')
export class Email extends RegexStringValue<Email> {
public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'Email', Email);

protected static getRegex(): RegExp {
public static getRegex(): RegExp {
return EMAIL_REGEX;
}

Expand Down
12 changes: 5 additions & 7 deletions src/Domain/ValueObject/EmailHash.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { StringValue } from './StringValue';
import * as crypto from 'crypto';
import { hash } from 'node:crypto';
import { Email } from './Email';
import { ValueObject } from './ValueObject';
import { RegexStringIdRawType } from './RegexStringValue';
import { HObjectTypeMeta } from "../../Util";

export type EmailHashRawType = RegexStringIdRawType;

@ValueObject('Core')
export class EmailHash extends StringValue<EmailHash> {
public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'EmailHash', EmailHash);

public static createFromEmail(email: Email): EmailHash {
return new EmailHash(crypto.createHash('sha1').update(email.v).digest('hex'));
return new EmailHash(hash('sha1', email.v, "hex"));
}
}
7 changes: 3 additions & 4 deletions src/Domain/ValueObject/RefId.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { StringValue } from "./StringValue";
import { ValueObject } from "./ValueObject";
import { customAlphabet } from 'nanoid';
import { HObjectTypeMeta } from "../../Util";

const RefIdGenerator = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-', 21);

/**
* Represents unique random string id. Generates nanoid - 21 characters
*/
@ValueObject('Core')
export class RefId extends StringValue<RefId> {
public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'RefId', RefId);

public static gen(): RefId {
return new RefId(RefIdGenerator());
}

}
}
52 changes: 42 additions & 10 deletions src/Domain/ValueObject/RegexStringValue.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
import { OK, Result } from '../../Util/Result';
import { AbstractValueObject } from './ValueObject';
import { StringValue } from './StringValue';

export type RegexStringIdRawType = string;
import { LogicError, OK, PlainParseHelper, PlainParseIssue, type PlainParseError, type R } from '../../Util';
import { AbstractValueObject, type ValueObjectType } from "./AbstractValueObject";

export type RegexStringSubtype<T> = {
new (value: string): T;
new(value: string): T;
getRegex(): RegExp;
};
export abstract class RegexStringValue<T extends RegexStringValue<any>> extends StringValue<T> {
public static checkRawValue<T>(this: RegexStringSubtype<T>, value: string): Result<boolean> {
return this.getRegex().test(value) ? OK(true) : AbstractValueObject.invalidRaw(this, { raw: value });
} & ValueObjectType;

export abstract class RegexStringValue<T extends RegexStringValue<any>> extends AbstractValueObject<T> {
public constructor(public readonly v: string) {
super();
}

public static parse<T extends RegexStringValue<any>>(this: RegexStringSubtype<T>, plain: unknown): R<T, PlainParseError> {
const parsed = PlainParseHelper.parseStringRegex(plain, this.getRegex());
if (parsed instanceof PlainParseIssue) {
return PlainParseHelper.HObjectParseErr(this, [parsed]);
}

return OK(new this(parsed));
}

public static getRegex(): RegExp {
throw new LogicError("Must be implemented in value object class");
}

/**
* Creates instance without extra validation.
* @param v
* @returns
*/
public static cs<T extends RegexStringValue<any>>(this: RegexStringSubtype<T>, v: string): T {
return new (this as any)(v);
}

public equals(other: T): boolean {
return this.v === other.v;
}

public toString(): string {
return this.v;
}

public toJSON(): string {
return this.v;
}
}
45 changes: 0 additions & 45 deletions src/Domain/ValueObject/SimpleValueObject.ts

This file was deleted.

43 changes: 40 additions & 3 deletions src/Domain/ValueObject/StringValue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
import { SimpleValueObject, SimpleValueObjectConstructor } from './SimpleValueObject';
import { HObjectTypeMeta, OK, PlainParseError, PlainParseHelper, PlainParseIssue, type R } from "../../Util";
import { AbstractValueObject, type ValueObjectType } from "./AbstractValueObject";

export type StringValueConstructor<T extends StringValue<any> = StringValue<any>> = SimpleValueObjectConstructor<T, string>;
export class StringValue<T extends StringValue<any> = any> extends AbstractValueObject<T> {
public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'String', StringValue);


public constructor(public readonly v: string) {
super();
}

public static parse<T extends StringValue>(this: ValueObjectType<T>, plain: unknown): R<T, PlainParseError> {
const parsed = PlainParseHelper.parseString(plain);
if (parsed instanceof PlainParseIssue) {
return PlainParseHelper.HObjectParseErr(this, [parsed]);
}

return OK(new this(parsed));
}

/**
* Creates instance without extra validation.
* @param v
* @returns
*/
public static cs<T extends StringValue>(this: ValueObjectType<T>, v: string): T {
return new (this as any)(v);
}

public equals(other: T): boolean {
return this.v === other.v;
}

public toString(): string {
return this.v;
}

public toJSON(): string {
return this.v;
}
}

export abstract class StringValue<T extends StringValue<any> = any> extends SimpleValueObject<T, string> {}
Loading

0 comments on commit 67c79c4

Please sign in to comment.