From 67c79c4ca10742f71f451d14f40e2409c4e9151e Mon Sep 17 00:00:00 2001 From: Mararok <5163714+Mararok@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:00:26 +0200 Subject: [PATCH] feat: rework ValueObject to HObject (#55) --- src/Domain/ValueObject/AbstractValueObject.ts | 29 +++++++ src/Domain/ValueObject/AccountId.ts | 7 +- src/Domain/ValueObject/DateTime.ts | 53 ++++++------- src/Domain/ValueObject/Email.ts | 10 +-- src/Domain/ValueObject/EmailHash.ts | 12 ++- src/Domain/ValueObject/RefId.ts | 7 +- src/Domain/ValueObject/RegexStringValue.ts | 52 +++++++++--- src/Domain/ValueObject/SimpleValueObject.ts | 45 ----------- src/Domain/ValueObject/StringValue.ts | 43 +++++++++- src/Domain/ValueObject/UBigIntValue.ts | 46 +++++------ src/Domain/ValueObject/UIntValue.ts | 66 ++++++++++------ src/Domain/ValueObject/UserId.ts | 7 +- src/Domain/ValueObject/ValueObject.ts | 79 ------------------- src/Domain/ValueObject/index.ts | 3 +- src/Util/Feature/HObjectTypeMeta.ts | 3 +- src/Util/Plain/PlainParseHelper.ts | 34 +++++++- src/Util/Plain/PlainParseIssue.ts | 30 +++++-- src/Util/Plain/types.ts | 2 +- test/helper/TestDto.ts | 2 - test/unit/Domain/ValueObject/DateTime.test.ts | 14 ++-- test/unit/Domain/ValueObject/Email.test.ts | 14 ++-- .../Domain/ValueObject/UBigIntValue.test.ts | 14 ++-- .../unit/Domain/ValueObject/UIntValue.test.ts | 9 ++- .../Domain/ValueObject/ValueObject.test.ts | 27 ------- test/unit/Util/Dto.test.ts | 3 +- .../Util/Plain/ObjectPropertyTypes.test.ts | 2 +- 26 files changed, 307 insertions(+), 306 deletions(-) create mode 100644 src/Domain/ValueObject/AbstractValueObject.ts delete mode 100644 src/Domain/ValueObject/SimpleValueObject.ts delete mode 100644 src/Domain/ValueObject/ValueObject.ts delete mode 100644 test/unit/Domain/ValueObject/ValueObject.test.ts diff --git a/src/Domain/ValueObject/AbstractValueObject.ts b/src/Domain/ValueObject/AbstractValueObject.ts new file mode 100644 index 0000000..6952fc2 --- /dev/null +++ b/src/Domain/ValueObject/AbstractValueObject.ts @@ -0,0 +1,29 @@ +import { + JsonSerialize, + LogicError, + type HObjectType, + type PlainParseError, + type R +} from '../../Util'; + +export type AnyValueObject = AbstractValueObject; +export type ValueObjectType = HObjectType; + +export abstract class AbstractValueObject implements JsonSerialize { + /** + * Creates ValueObject from plain value + * @param this + * @param plain + * @returns + */ + public static parse(this: (new (...args: any[]) => any), plain: unknown): R { + 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'); + } +} diff --git a/src/Domain/ValueObject/AccountId.ts b/src/Domain/ValueObject/AccountId.ts index 8d44f3a..6c21574 100644 --- a/src/Domain/ValueObject/AccountId.ts +++ b/src/Domain/ValueObject/AccountId.ts @@ -1,5 +1,6 @@ +import { HObjectTypeMeta } from "../../Util"; import { StringValue } from "./StringValue"; -import { ValueObject } from "./ValueObject"; -@ValueObject('Account') -export class AccountId extends StringValue {} \ No newline at end of file +export class AccountId extends StringValue { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Account', 'ValueObject', 'AccountId', AccountId); +} \ No newline at end of file diff --git a/src/Domain/ValueObject/DateTime.ts b/src/Domain/ValueObject/DateTime.ts index f6d3e94..218d642 100644 --- a/src/Domain/ValueObject/DateTime.ts +++ b/src/Domain/ValueObject/DateTime.ts @@ -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"); @@ -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 { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'DateTime', DateTime); + public constructor(private readonly value: LocalDateTime) { super(); } @@ -42,34 +44,26 @@ export class DateTime extends AbstractValueObject { 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 { - - switch (typeof v) { - case 'number': return this.fromTimestamp(v); + public static parse(this: ValueObjectType, plain: unknown): R { + 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; } /** @@ -94,14 +88,13 @@ export class DateTime extends AbstractValueObject { * @param timestamp * @returns */ - public static fromTimestamp(timestamp: number): R { + public static fromTimestamp(timestamp: number): R { 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))); } /** @@ -176,7 +169,7 @@ export class DateTime extends AbstractValueObject { return this.formatDateTime(); } - public toJSON(): any { + public toJSON(): number { return this.t; } -} +} \ No newline at end of file diff --git a/src/Domain/ValueObject/Email.ts b/src/Domain/ValueObject/Email.ts index 9cda279..956848b 100644 --- a/src/Domain/ValueObject/Email.ts +++ b/src/Domain/ValueObject/Email.ts @@ -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 { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'Email', Email); - protected static getRegex(): RegExp { + public static getRegex(): RegExp { return EMAIL_REGEX; } diff --git a/src/Domain/ValueObject/EmailHash.ts b/src/Domain/ValueObject/EmailHash.ts index 020de67..597871d 100644 --- a/src/Domain/ValueObject/EmailHash.ts +++ b/src/Domain/ValueObject/EmailHash.ts @@ -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 { + 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")); } } diff --git a/src/Domain/ValueObject/RefId.ts b/src/Domain/ValueObject/RefId.ts index 72a811b..7f9c38e 100644 --- a/src/Domain/ValueObject/RefId.ts +++ b/src/Domain/ValueObject/RefId.ts @@ -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 { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'RefId', RefId); public static gen(): RefId { return new RefId(RefIdGenerator()); } - -} +} \ No newline at end of file diff --git a/src/Domain/ValueObject/RegexStringValue.ts b/src/Domain/ValueObject/RegexStringValue.ts index df01b0b..c5dd36d 100644 --- a/src/Domain/ValueObject/RegexStringValue.ts +++ b/src/Domain/ValueObject/RegexStringValue.ts @@ -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 = { - new (value: string): T; + new(value: string): T; getRegex(): RegExp; -}; -export abstract class RegexStringValue> extends StringValue { - public static checkRawValue(this: RegexStringSubtype, value: string): Result { - return this.getRegex().test(value) ? OK(true) : AbstractValueObject.invalidRaw(this, { raw: value }); +} & ValueObjectType; + +export abstract class RegexStringValue> extends AbstractValueObject { + public constructor(public readonly v: string) { + super(); + } + + public static parse>(this: RegexStringSubtype, plain: unknown): R { + 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>(this: RegexStringSubtype, 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; } } diff --git a/src/Domain/ValueObject/SimpleValueObject.ts b/src/Domain/ValueObject/SimpleValueObject.ts deleted file mode 100644 index 15dc2c5..0000000 --- a/src/Domain/ValueObject/SimpleValueObject.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Result, ERR, OK } from '../../Util/Result'; -import { AbstractValueObject } from './ValueObject'; - -/** - * For pass child class constructor as parameter - */ -export type SimpleValueObjectConstructor = { - new (v: VT): T; - c>(this: SimpleValueObjectConstructor, value: any): Result; - checkRawValue(value: any): Result; -}; - -export class SimpleValueObject, VT> extends AbstractValueObject { - public readonly v: VT; - - public constructor(value: VT) { - super(); - this.v = value; - } - - public static c>(this: SimpleValueObjectConstructor, value: any): Result { - const checkResult = this.checkRawValue(value); - return checkResult.isError() ? ERR(checkResult.e) : OK(new this(value)); - } - - public static cs>(this: SimpleValueObjectConstructor, value: any): T { - return new this(value); - } - - public static checkRawValue(value: any): Result { - return OK(true); - } - - public equals(other: T): boolean { - return this.v === other.v; - } - - public toString(): any { - return this.v; - } - - public toJSON(): any { - return this.v; - } -} diff --git a/src/Domain/ValueObject/StringValue.ts b/src/Domain/ValueObject/StringValue.ts index 8eb25f4..72eaaf6 100644 --- a/src/Domain/ValueObject/StringValue.ts +++ b/src/Domain/ValueObject/StringValue.ts @@ -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 = StringValue> = SimpleValueObjectConstructor; +export class StringValue = any> extends AbstractValueObject { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'String', StringValue); + + + public constructor(public readonly v: string) { + super(); + } + + public static parse(this: ValueObjectType, plain: unknown): R { + 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(this: ValueObjectType, 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 = any> extends SimpleValueObject {} diff --git a/src/Domain/ValueObject/UBigIntValue.ts b/src/Domain/ValueObject/UBigIntValue.ts index b2dae50..3a70214 100644 --- a/src/Domain/ValueObject/UBigIntValue.ts +++ b/src/Domain/ValueObject/UBigIntValue.ts @@ -1,31 +1,33 @@ -import { ERR, OK, Result } from '../../Util'; -import { SimpleValueObject, SimpleValueObjectConstructor } from './SimpleValueObject'; -import { AbstractValueObject, ValueObject } from './ValueObject'; - -export type UBigIntValueConstructor = SimpleValueObjectConstructor; - -@ValueObject('Core') -export class UBigIntValue = any> extends SimpleValueObject { - public static c>(this: UBigIntValueConstructor, value: any): Result { - if (typeof value === 'string') { - try { - value = BigInt(value); - } catch (e) { - return AbstractValueObject.invalidRaw(this, { raw: value }); - } +import { HObjectTypeMeta, OK, PlainParseHelper, PlainParseIssue, type PlainParseError, type R } from '../../Util'; +import { AbstractValueObject, type ValueObjectType } from './AbstractValueObject'; + +export class UBigIntValue = any> extends AbstractValueObject { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'Core', 'ValueObject', 'UBigInt', UBigIntValue); + + public constructor(public readonly v: bigint) { + super(); + } + + public static parse(this: ValueObjectType, plain: unknown): R { + const parsed = PlainParseHelper.parseBigInt64GTE(plain, 0n); + if (parsed instanceof PlainParseIssue) { + return PlainParseHelper.HObjectParseErr(this, [parsed]); } - const checkResult = this.checkRawValue(value); - return checkResult.isError() ? ERR(checkResult.e) : OK(new this(value)); + return OK(new this(parsed)); } - public static cs>(this: UBigIntValueConstructor, value: any): T { - value = BigInt(value.toString()); - return new this(value); + /** + * Creates instance without extra validation. + * @param v + * @returns + */ + public static cs(this: ValueObjectType, v: string | number | bigint): T { + return new (this as any)(BigInt(v)); } - public static checkRawValue(value: any): Result { - return value >= 0n ? OK(true) : AbstractValueObject.invalidRaw(this, { raw: value }); + public equals(other: T): boolean { + return this.v === other.v; } public toString(): string { diff --git a/src/Domain/ValueObject/UIntValue.ts b/src/Domain/ValueObject/UIntValue.ts index a98e784..12d0eda 100644 --- a/src/Domain/ValueObject/UIntValue.ts +++ b/src/Domain/ValueObject/UIntValue.ts @@ -1,40 +1,56 @@ -import { ERR, OK, Result } from '../../Util'; -import { SimpleValueObject, SimpleValueObjectConstructor } from './SimpleValueObject'; -import { AbstractValueObject, ValueObject } from './ValueObject'; - -export type UIntValueConstructor = SimpleValueObjectConstructor; - -@ValueObject('Core') -export class UIntValue = any> extends SimpleValueObject { - public static c>(this: UIntValueConstructor, value: any): Result { - if (typeof value === 'string') { - try { - value = Number.parseInt(value); - } catch (e) { - return AbstractValueObject.invalidRaw(this, { raw: value }); - } +import { HObjectTypeMeta, InvalidTypePlainParseIssue, OK, PlainParseHelper, TooSmallPlainParseIssue, type PlainParseError, type R } from '../../Util'; +import { AbstractValueObject, type ValueObjectType } from './AbstractValueObject'; + +export class UIntValue = any> extends AbstractValueObject { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'User', 'ValueObject', 'UInt', UIntValue); + + public constructor(public readonly v: number) { + super(); + } + + public static parse(this: ValueObjectType, plain: unknown): R { + let parsed: number; + switch (typeof plain) { + case 'number': + parsed = Math.trunc(plain); + break; + case 'string': + parsed = parseInt(plain); + break; + default: + return PlainParseHelper.HObjectParseErr(this, [new InvalidTypePlainParseIssue('number', typeof plain)]); } - const checkResult = this.checkRawValue(value); - return checkResult.isError() ? ERR(checkResult.e) : OK(new this(value)); + if (parsed < 0) { + const issue = TooSmallPlainParseIssue.numberGTE(0, parsed); + return PlainParseHelper.HObjectParseErr(this, [issue]); + } + + return OK(new this(parsed)); } - public static cs>(this: UIntValueConstructor, value: any): T { - if (typeof value === 'string') { - value = Number.parseInt(value); + /** + * Creates instance without extra validation. + * @param v + * @returns + */ + public static cs(this: ValueObjectType, v: string | number): T { + if (typeof v === 'string') { + v = Number.parseInt(v); } - return new this(value); + + return new this(v); } - public static checkRawValue(value: number): Result { - return value >= 0 ? OK(true) : AbstractValueObject.invalidRaw(this, { raw: value }); + public equals(other: T): boolean { + return this.v === other.v; } public toString(): string { return this.v.toString(); } - public toJSON(): number { - return this.v; + public toJSON(): string { + return this.v.toString(); } } diff --git a/src/Domain/ValueObject/UserId.ts b/src/Domain/ValueObject/UserId.ts index 0cce288..154f4f9 100644 --- a/src/Domain/ValueObject/UserId.ts +++ b/src/Domain/ValueObject/UserId.ts @@ -1,5 +1,6 @@ +import { HObjectTypeMeta } from "../../Util"; import { StringValue } from "./StringValue"; -import { ValueObject } from "./ValueObject"; -@ValueObject('Account') -export class UserId extends StringValue {} +export class UserId extends StringValue { + public static readonly HOBJ_META = HObjectTypeMeta.domain('Core', 'User', 'ValueObject', 'UserId', UserId); +} diff --git a/src/Domain/ValueObject/ValueObject.ts b/src/Domain/ValueObject/ValueObject.ts deleted file mode 100644 index 8442e33..0000000 --- a/src/Domain/ValueObject/ValueObject.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { AppError, CustomPlainParseIssue, ERR, JsonSerialize, pascalCaseToSnakeCase, PlainParseHelper, Result,type PlainParsableHObjectType, type PlainParseError, type R } from '../../Util'; - -export interface ValueObjectMeta { - readonly module: string; - readonly class: string; -} - -export const VALUE_OBJECT_META_PROPERTY = '__VOMETA'; - -export type AnyValueObject = AbstractValueObject; -export type ValueObjectConstructor = PlainParsableHObjectType; - - -/** - * Decorator - * @param moduleName Name of module - */ -export function ValueObject(moduleName: string): (constructor: ValueObjectConstructor) => void { - return function (constructor: ValueObjectConstructor) { - constructor.prototype[VALUE_OBJECT_META_PROPERTY] = { - module: moduleName, - class: constructor.name, - }; - }; -} - -export function ValueObjectInvalidRawValueError(meta: ValueObjectMeta, data: any = null): AppError { - return new AppError({ - type: pascalCaseToSnakeCase(meta.module) + '.domain.value_object.' + pascalCaseToSnakeCase(meta.class) + '.invalid_raw_value', - data, - code: 400, - }); -} - -export function checkEnumValueObject(value: any, enumType: any, meta: ValueObjectMeta, data: any = null): AppError | null { - if (!(value in enumType)) { - return ValueObjectInvalidRawValueError(meta, data); - } - return null; -} - -export abstract class AbstractValueObject implements JsonSerialize { - /** - * @deprecated use invalidRaw - * @param meta - * @param data - * @returns - */ - protected static createInvalidRawValueError(meta: ValueObjectMeta, data: any = null): AppError { - return ValueObjectInvalidRawValueError(this.prototype[VALUE_OBJECT_META_PROPERTY], data); - } - - protected static invalidRaw(valueObjectClass: any, data: any = null): Result { - const meta = valueObjectClass.prototype[VALUE_OBJECT_META_PROPERTY]; - if (!meta) { - throw new Error(VALUE_OBJECT_META_PROPERTY + " property isn't defined, add @ValueObject decorator to " + valueObjectClass.name); - } - return ERR(ValueObjectInvalidRawValueError(meta, data)); - } - - /** - * Creates ValueObject from plain value - * @param this - * @param plain - * @returns - */ - public static parse(this: ValueObjectConstructor, plain: unknown): R { - const result: R = this['c'](plain); - if (result.isError()) { - return PlainParseHelper.HObjectParseErr(this, [new CustomPlainParseIssue(result.e.data, 'invalid plain')]); - } - - return result as any; - } - - public abstract equals(o: T): boolean; - public abstract toString(): string; - public abstract toJSON(): any; -} diff --git a/src/Domain/ValueObject/index.ts b/src/Domain/ValueObject/index.ts index 322e7e6..e689d4d 100644 --- a/src/Domain/ValueObject/index.ts +++ b/src/Domain/ValueObject/index.ts @@ -1,6 +1,5 @@ // BASE -export * from './ValueObject'; -export * from './SimpleValueObject'; +export * from './AbstractValueObject'; // Account export * from './AccountId'; diff --git a/src/Util/Feature/HObjectTypeMeta.ts b/src/Util/Feature/HObjectTypeMeta.ts index 6eaa1da..da7c3a0 100644 --- a/src/Util/Feature/HObjectTypeMeta.ts +++ b/src/Util/Feature/HObjectTypeMeta.ts @@ -1,6 +1,7 @@ import { LogicError } from "../Error"; import { pascalCaseToSnakeCase } from "../functions"; import type { JsonSerialize } from "../Json/JsonSerialize"; +import type { PlainParseError } from "../Plain"; import type { R } from "../Result"; import type { JsonExcluded, JsonObjectType } from "../types"; import type { HFeatureBackendLayer } from "./types"; @@ -11,7 +12,7 @@ export type HObjectType = { export type PlainParsableHObjectType = { new(...args: any[]): T; - parse(this: PlainParsableHObjectType, plain: unknown): R; + parse(this: PlainParsableHObjectType, plain: unknown): R; }; diff --git a/src/Util/Plain/PlainParseHelper.ts b/src/Util/Plain/PlainParseHelper.ts index 416d1d6..148a946 100644 --- a/src/Util/Plain/PlainParseHelper.ts +++ b/src/Util/Plain/PlainParseHelper.ts @@ -1,7 +1,7 @@ import { AppErrorCode } from "../Error/AppError"; import { HObjectTypeMeta, type HObjectType, type PlainParsableHObjectType } from "../Feature"; import { ERR, type R } from "../Result"; -import { InvalidArrayElementsPlainParseIssue, InvalidHObjectPlainParseIssue as InvalidHObjectPlainParseIssue, InvalidTypePlainParseIssue, PlainParseIssue, TooBigPlainParseIssue, TooSmallPlainParseIssue } from "./PlainParseIssue"; +import { InvalidArrayElementsPlainParseIssue, InvalidHObjectPlainParseIssue as InvalidHObjectPlainParseIssue, InvalidStringPlainParseIssue, InvalidTypePlainParseIssue, PlainParseIssue, TooBigPlainParseIssue, TooSmallPlainParseIssue } from "./PlainParseIssue"; export const PlainParseError = 'core.plain.parse' as const; export type PlainParseError = typeof PlainParseError; @@ -24,7 +24,7 @@ export class PlainParseHelper { } public static parseBigInt64(plain: unknown, path?: string, issues?: PlainParseIssue[]): bigint | PlainParseIssue { - if (typeof plain === 'number' || (typeof plain === 'string' && BigInt64Regex.test(plain))) { + if (typeof plain === 'number' || typeof plain === 'bigint' || (typeof plain === 'string' && BigInt64Regex.test(plain))) { return BigInt(plain); } @@ -36,6 +36,21 @@ export class PlainParseHelper { return issue; } + public static parseBigInt64GTE(plain: unknown, min: bigint, path?: string, issues?: PlainParseIssue[]): bigint | PlainParseIssue { + if (typeof plain === 'number' || typeof plain === 'bigint' || (typeof plain === 'string' && BigInt64Regex.test(plain))) { + const parsed = BigInt(plain); + if (parsed < min) { + const issue = TooSmallPlainParseIssue.bigintGTE(min, parsed, path); + issues?.push(issue); + return issue; + } + return parsed; + } + const issue = new InvalidTypePlainParseIssue('bigint_string', typeof plain, path); + issues?.push(issue); + return issue; + } + public static parseNumber(plain: unknown, path?: string, issues?: PlainParseIssue[]): number | PlainParseIssue { if (typeof plain === 'number') { return plain; @@ -135,6 +150,21 @@ export class PlainParseHelper { return issue; } + public static parseStringRegex(plain: unknown, regex: RegExp, path?: string, issues?: PlainParseIssue[]): string | PlainParseIssue { + if (typeof plain === 'string') { + if (!regex.test(plain)) { + const issue = InvalidStringPlainParseIssue.regex(regex, path); + issues?.push(issue); + return issue; + } + return plain; + } + + const issue = new InvalidTypePlainParseIssue('string', typeof plain, path); + issues?.push(issue); + return issue; + } + public static parseStringLength(plain: unknown, length: number, path?: string, issues?: PlainParseIssue[]): string | PlainParseIssue { if (typeof plain === 'string') { if (plain.length !== length) { diff --git a/src/Util/Plain/PlainParseIssue.ts b/src/Util/Plain/PlainParseIssue.ts index 36ee9ce..079df6b 100644 --- a/src/Util/Plain/PlainParseIssue.ts +++ b/src/Util/Plain/PlainParseIssue.ts @@ -61,7 +61,7 @@ export abstract class PlainParseIssue implements JsonSerialize { } } -export type PlainParsePrimitiveType = 'string' | 'number' | 'bigint' | 'bigint_string' | 'boolean' | 'object' | 'array' | 'symbol' | 'undefined' | 'null' | 'function'; +export type PlainParsePrimitiveType = 'string' | 'number' | 'bigint' | 'bigint_string' | 'boolean' | 'object' | 'array' | 'symbol' | 'undefined' | 'null' | 'function' | 'Date'; export class InvalidTypePlainParseIssue extends PlainParseIssue { public constructor( @@ -87,12 +87,18 @@ export class InvalidTypePlainParseIssue extends PlainParseIssue { export class InvalidStringPlainParseIssue extends PlainParseIssue { public constructor( public validatorType: string, + public validatorArgs: Record, message: string, path?: string, ) { super(PlainParseIssueCode.invalid_string, message, path); } + public static regex(regex: RegExp, path?: string): InvalidStringPlainParseIssue { + const message = `String must pass pattern: ${regex}`; + return new this('regex', { regex }, message, path); + } + public get i18n(): string { return super.i18n + `.${this.validatorType}`; } @@ -103,12 +109,17 @@ export class InvalidStringPlainParseIssue extends PlainParseIssue { message: this.message, path: this.path, validatorType: this.validatorType, - i18n: this.i18n - }; + validatorArgs: this.validatorArgs, + i18n: this.i18n, + } as any; } } export enum ValueRangeSideMode { + bigint_exclusive = 'bigint_exclusive', + bigint_inclusive = 'bigint_inclusive', + bigint_exactly = 'bigint_exactly', + number_exclusive = 'number_exclusive', number_inclusive = 'number_inclusive', number_exactly = 'number_exactly', @@ -122,9 +133,9 @@ export enum ValueRangeSideMode { export class TooSmallPlainParseIssue extends PlainParseIssue { public constructor( - public minimum: number, + public minimum: number | bigint, public mode: ValueRangeSideMode, - public current: number, + public current: number | bigint, message: string, path?: string ) { @@ -166,6 +177,11 @@ export class TooSmallPlainParseIssue extends PlainParseIssue { return new this(exactly, ValueRangeSideMode.number_exactly, current, message, path); } + public static bigintGTE(minimum: bigint, current: bigint, path?: string): TooSmallPlainParseIssue { + const message = `Number must be greater than or equal to ${minimum}, current: ${current}`; + return new this(minimum, ValueRangeSideMode.bigint_inclusive, current, message, path); + } + public get i18n(): string { return super.i18n + `.${this.mode}`; } @@ -177,9 +193,9 @@ export class TooSmallPlainParseIssue extends PlainParseIssue { path: this.path, i18n: this.i18n, - minimum: this.minimum, + minimum: typeof this.minimum === 'bigint' ? this.minimum.toString() : this.minimum, mode: this.mode, - current: this.current, + current: typeof this.current === 'bigint' ? this.current.toString() : this.current, }; } } diff --git a/src/Util/Plain/types.ts b/src/Util/Plain/types.ts index 2172f4e..2b3f6df 100644 --- a/src/Util/Plain/types.ts +++ b/src/Util/Plain/types.ts @@ -8,7 +8,7 @@ type NumberValueRuleTag = number & ValueRuleTagBase; export namespace v { export namespace string { - export type pattern = string & ValueRuleTagBase<'string_pattern'>; + export type regex = string & ValueRuleTagBase<'string_regex'>; export type length = string & ValueRuleTagBase<'string_length_exactly'>; export namespace length { diff --git a/test/helper/TestDto.ts b/test/helper/TestDto.ts index 3ed2c32..cd395ec 100644 --- a/test/helper/TestDto.ts +++ b/test/helper/TestDto.ts @@ -1,5 +1,4 @@ import { - ValueObject, UIntValue, Dto, type JsonObjectType, type PlainParsableHObjectType, @@ -14,7 +13,6 @@ import { } from "@"; import type { v } from "@/Util/Plain/types"; -@ValueObject('Test') export class TestValueObject extends UIntValue { } class OtherTestDto extends Dto { diff --git a/test/unit/Domain/ValueObject/DateTime.test.ts b/test/unit/Domain/ValueObject/DateTime.test.ts index a1bcaa2..34c9847 100644 --- a/test/unit/Domain/ValueObject/DateTime.test.ts +++ b/test/unit/Domain/ValueObject/DateTime.test.ts @@ -2,7 +2,7 @@ * @group unit */ -import { DateTime } from '@'; +import { DateTime, InvalidHObjectPlainParseIssue, PlainParseError, TooSmallPlainParseIssue } from '@'; import { Duration, Period } from '@js-joda/core'; describe('DateTime', () => { @@ -10,7 +10,7 @@ describe('DateTime', () => { test('when input is Date', () => { const raw = new Date("2024-01-01 10:30:30"); - const result = DateTime.c(raw); + const result = DateTime.parse(raw); expect(result.v.formatDateTime()).toBe(raw.toISOString().replace(/\..+/, "")); }); @@ -22,7 +22,7 @@ describe('DateTime', () => { {name: "onlyDate", raw: "2024-01-01", expected: "2024-01-01T00:00:00"} ])('when input is string($name)', (data) => { - const result = DateTime.c(data.raw); + const result = DateTime.parse(data.raw); expect(result.isSuccess()).toBe(true); expect(result.v.formatDateTime()).toBe(data.expected); @@ -45,7 +45,7 @@ describe('DateTime', () => { {name: "onlyDate", raw: "2024-01-01", expected: "2024-01-01T00:00:00"} ])('when input is string($name)', (data) => { - const result = DateTime.c(data.raw); + const result = DateTime.parse(data.raw); expect(result.isSuccess()).toBe(true); expect(result.v.formatDateTime()).toBe(data.expected); @@ -61,8 +61,10 @@ describe('DateTime', () => { const result = DateTime.fromTimestamp(-10); expect(result.isError()).toBe(true); - expect(result.e.type).toEqual('core.domain.value_object.date_time.invalid_raw_value'); - expect(result.e.data).toEqual({ msg: 'invalid timestamp', raw: -10 }); + expect(result.e.type).toEqual(PlainParseError); + expect(result.e.data).toEqual(new InvalidHObjectPlainParseIssue(DateTime.HOBJ_META, [ + TooSmallPlainParseIssue.numberGTE(0, -10) + ])); }); test('plus()', () => { diff --git a/test/unit/Domain/ValueObject/Email.test.ts b/test/unit/Domain/ValueObject/Email.test.ts index b82b489..bb5dae1 100644 --- a/test/unit/Domain/ValueObject/Email.test.ts +++ b/test/unit/Domain/ValueObject/Email.test.ts @@ -2,31 +2,31 @@ * @group unit */ -import { Email } from '@'; +import { Email, PlainParseError } from '@'; import { EmailHash } from '../../../../src/Domain/ValueObject/EmailHash'; describe('Email', () => { test('create', () => { - const result = Email.c('test@test.com'); + const result = Email.parse('test@test.com'); expect(result.isSuccess()).toBe(true); expect(result.v.v).toEqual('test@test.com'); }); test('create when invalid raw value', () => { - const result = Email.c('test@test'); + const result = Email.parse('test@test'); expect(result.isError()).toBe(true); - expect(result.e.type).toEqual('core.domain.value_object.email.invalid_raw_value'); + expect(result.e.type).toEqual(PlainParseError); }); test('get local', () => { - const result = Email.c('test@test.com'); + const result = Email.parse('test@test.com'); expect(result.v.local).toEqual('test'); }); test('get domain', () => { - const result = Email.c('test@test.com'); + const result = Email.parse('test@test.com'); expect(result.v.domain).toEqual('test.com'); }); test('get hash', () => { - const result = Email.c('test@test.com'); + const result = Email.parse('test@test.com'); expect(result.v.hash).toEqual(EmailHash.cs('a6ad00ac113a19d953efb91820d8788e2263b28a')); }); }); diff --git a/test/unit/Domain/ValueObject/UBigIntValue.test.ts b/test/unit/Domain/ValueObject/UBigIntValue.test.ts index f839beb..d31aff2 100644 --- a/test/unit/Domain/ValueObject/UBigIntValue.test.ts +++ b/test/unit/Domain/ValueObject/UBigIntValue.test.ts @@ -2,9 +2,9 @@ * @group unit/core */ -import { UBigIntValue } from '../../../../src'; +import { PlainParseError, UBigIntValue } from '../../../../src'; -class CustomUBigInt extends UBigIntValue { +class CustomUBigInt extends UBigIntValue { public customMethod() { return 'test'; } @@ -12,24 +12,24 @@ class CustomUBigInt extends UBigIntValue { describe('UBigIntValue', () => { test('create from string', () => { - const result = UBigIntValue.c('10'); + const result = UBigIntValue.parse('10'); expect(result.v.v).toBe(10n); }); test('create when invalid string raw value', () => { - const result = UBigIntValue.c('-10'); + const result = UBigIntValue.parse('-10'); expect(result.isError()).toBe(true); - expect(result.e.type).toEqual('core.domain.value_object.u_big_int_value.invalid_raw_value'); + expect(result.e.type).toEqual(PlainParseError); }); test('create from bigint', () => { - const result = UBigIntValue.c(10n); + const result = UBigIntValue.parse(10n); expect(result.v.v).toBe(10n); }); test('create custom', () => { - const result = CustomUBigInt.c(10n); + const result = CustomUBigInt.parse(10n); expect(result.v).toBeInstanceOf(CustomUBigInt); expect(result.v.v).toBe(10n); }); diff --git a/test/unit/Domain/ValueObject/UIntValue.test.ts b/test/unit/Domain/ValueObject/UIntValue.test.ts index cf195ed..a6c3536 100644 --- a/test/unit/Domain/ValueObject/UIntValue.test.ts +++ b/test/unit/Domain/ValueObject/UIntValue.test.ts @@ -2,18 +2,19 @@ * @group unit/core */ -import { UIntValue } from '../../../../src/Domain/ValueObject/UIntValue'; +import { PlainParseError } from "@"; +import { UIntValue } from '@/Domain/ValueObject/UIntValue'; describe('UIntValue', () => { test('create', () => { - const result = UIntValue.c(10); + const result = UIntValue.parse(10); expect(result.v.v).toBe(10); }); test('create when invalid raw value', () => { - const result = UIntValue.c(-10); + const result = UIntValue.parse(-10); expect(result.isError()).toBe(true); - expect(result.e.type).toEqual('core.domain.value_object.u_int_value.invalid_raw_value'); + expect(result.e.type).toEqual(PlainParseError); }); }); diff --git a/test/unit/Domain/ValueObject/ValueObject.test.ts b/test/unit/Domain/ValueObject/ValueObject.test.ts deleted file mode 100644 index eb376f9..0000000 --- a/test/unit/Domain/ValueObject/ValueObject.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @group unit/core - */ - -import { checkEnumValueObject } from '@'; - -enum Test { - a = 1, - b = 2, -} - -const TestEnumMeta = { - module: 'Test', - class: 'Test', -}; - -describe('ValueObject', () => { - test('checkEnumValueObject when is valid ', () => { - const result = checkEnumValueObject(Test.a, Test, TestEnumMeta); - expect(result).toBeNull(); - }); - - test('checkEnumValueObject when is invalid ', () => { - const result = checkEnumValueObject(10, Test, TestEnumMeta); - expect(result).toMatchObject({}); - }); -}); diff --git a/test/unit/Util/Dto.test.ts b/test/unit/Util/Dto.test.ts index a076d79..3eb9031 100644 --- a/test/unit/Util/Dto.test.ts +++ b/test/unit/Util/Dto.test.ts @@ -1,11 +1,10 @@ -import { Dto, UIntValue, ValueObject, type JsonObjectType } from '@'; +import { Dto, UIntValue, type JsonObjectType } from '@'; import path from 'path'; /** * @group unit */ -@ValueObject('Test') class TestValueObject extends UIntValue { } class OtherTestDto extends Dto { diff --git a/test/unit/Util/Plain/ObjectPropertyTypes.test.ts b/test/unit/Util/Plain/ObjectPropertyTypes.test.ts index f1cc18f..8dcf4eb 100644 --- a/test/unit/Util/Plain/ObjectPropertyTypes.test.ts +++ b/test/unit/Util/Plain/ObjectPropertyTypes.test.ts @@ -3,7 +3,7 @@ import path from 'node:path'; class Super { public constructor( - public stringPatternField: v.string.pattern<'^[A-Z][a-zA-Z]{4}$'>, + public stringPatternField: v.string.regex<'^[A-Z][a-zA-Z]{4}$'>, public stringLengthField: v.string.length<10>, public stringMinField: v.string.length.min<10>,