diff --git a/package.json b/package.json index 77e275b..7ae9b88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cosmicmind/domainjs", - "version": "0.0.1-rc-072224-4", + "version": "0.0.1-rc-073024-4", "description": "A domain-driven design framework for scalable systems.", "keywords": [], "author": { diff --git a/src/Aggregate.ts b/src/Aggregate.ts index e37f88b..289792e 100644 --- a/src/Aggregate.ts +++ b/src/Aggregate.ts @@ -41,11 +41,11 @@ import { } from '@/Entity' import { - EventTopics, - EventObservable, -} from '@/Event' + Topics, + ObservableTopics, +} from '@/Topic' -export abstract class Aggregate extends EventObservable { +export abstract class Aggregate extends ObservableTopics { protected root: E constructor(root: E) { diff --git a/src/Message.ts b/src/Message.ts new file mode 100644 index 0000000..0ea6559 --- /dev/null +++ b/src/Message.ts @@ -0,0 +1,189 @@ +/** + * BSD 3-Clause License + * + * Copyright © 2023, Daniel Jonathan + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES LOSS OF USE, DATA, OR PROFITS OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @module Message + */ + +import { + guard, + FoundationError, +} from '@cosmicmind/foundationjs' + +import { + Observable, + ObservableTopics, +} from '@cosmicmind/patternjs' + +/** + * Represents an Message. + * + * @example + * const message: Message = { + * name: 'John Doe', + * age: 25 + * } + */ +export type Message = Record + +/** + * Represents a collection of message topics. + * + * @extends {ObservableTopics} + * + * @property {Message} [K] - The message topic. + */ +export type MessageTopics = ObservableTopics & { + readonly [K: string]: Message +} + +/** + * An observable class for handling messages of specific types. + * + * @template T The message topic type. + */ +export class MessageObservable extends Observable {} + +/** + * Represents the lifecycle hooks for an message property. + * + * @template E - The type of the message. + * @template V - The type of the property value. + */ +export type MessagePropertyLifecycle = { + required?: boolean + validator?(value: V, message: E): boolean | never +} + +/** + * Represents a map that defines the lifecycle of message properties. + * + * @template E - The type of the message. + */ +export type MessagePropertyLifecycleMap = { + [K in keyof E]?: MessagePropertyLifecycle +} + +export class MessageError extends FoundationError {} + +/** + * Represents the lifecycle methods for an message. + * + * @template E - The type of message. + */ +export type MessageLifecycle = { + created?(message: E): void + error?(error: MessageError): void + properties?: MessagePropertyLifecycleMap +} + +/** + * Defines an message with an optional message lifecycle handler. + * + * @template E The type of the message. + * @param {MessageLifecycle} [handler={}] The optional message lifecycle handler. + * @returns {(message: E) => E} A function that creates an message with the given lifecycle handler. + */ +export const defineMessage = (handler: MessageLifecycle = {}): (message: E) => E => + (message: E): E => makeMessage(message, handler) + +/** + * Creates a proxy message handler that prmessages the modification of message properties. + * + * @template E - The message type. + * @param {MessageLifecycle} handler - The message lifecycle handler. + * @returns {ProxyHandler} - The proxy message handler. + */ +function makeMessageHandler(handler: MessageLifecycle): ProxyHandler { + return { + set(): never { + throwError('cannot modify message properties', handler) + }, + } +} + +/** + * Throws an MessageError with a specified message and invokes the error handler. + * + * @template E - The type of Message. + * @param {string} message - The error message. + * @param {MessageLifecycle} handler - The message lifecycle handler. + * @throws {MessageError} - The MessageError instance. + * @return {never} - This method never returns. + */ +function throwError(message: string, handler: MessageLifecycle): never { + const error = new MessageError(message) + handler.error?.(error) + throw error +} + +/** + * Creates an message of type `E`. + * + * @template E - The type of the message to create. + * @param {E} target - The target object to create the message from. + * @param {MessageLifecycle} [handler={}] - The lifecycle handler for the message. + * @returns {E} - The created message object. + * @throws {MessageError} - If the target object is invalid. + */ +function makeMessage(target: E, handler: MessageLifecycle = {}): E | never { + if (guard(target)) { + const properties = handler.properties + + if (guard>(properties)) { + const message = new Proxy(target, makeMessageHandler(handler)) + + for (const [ key, property ] of Object.entries(properties) as [string, MessagePropertyLifecycle][]) { + if (property.required) { + if (!(key in target)) { + throwError(`${JSON.stringify(target)} ${key} is required`, handler) + } + + if (false === property.validator?.(target[key], message)) { + throwError(`${JSON.stringify(target)} ${key} is invalid`, handler) + } + } + else if (key in target && 'undefined' !== typeof target[key]) { + if (guard(property, 'validator') && false === property.validator?.(target[key], message)) { + throwError(`${JSON.stringify(target)} ${key} is invalid`, handler) + } + } + } + + handler.created?.(message) + + return message + } + } + + throw new MessageError(`${String(target)} is invalid`) +} \ No newline at end of file diff --git a/src/Topic.ts b/src/Topic.ts index 90b219e..b6d6806 100644 --- a/src/Topic.ts +++ b/src/Topic.ts @@ -35,28 +35,10 @@ */ import { - Observable, - ObservableTopics, + Observable as O, + ObservableTopics as OT, } from '@cosmicmind/patternjs' -import { - Event, -} from '@/Event' +export type Topics = OT -/** - * Represents a collection of Event topics. - * - * @extends {ObservableTopics} - * - * @property {Event} [K] - A event topic. - */ -export type EventTopics = ObservableTopics & { - readonly [K: string]: Event -} - -/** - * An observable class for handling topics of specific types. - * - * @template T The event topic type. - */ -export class TopicObservable extends Observable {} +export class ObservableTopics extends O {} diff --git a/src/index.ts b/src/index.ts index 0a15afc..61aabf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,4 +33,6 @@ export * from '@/Aggregate' export * from '@/Event' export * from '@/Entity' +export * from '@/Message' +export * from '@/Topic' export * from '@/Value' \ No newline at end of file