Skip to content

Commit

Permalink
feat(core): Result model (#18)
Browse files Browse the repository at this point in the history
* feat(core): Result model
* feat(core, commands): execute returns Result
* feat(models): Builder model
  • Loading branch information
akdasa authored Nov 26, 2022
1 parent fd25724 commit c500abb
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 12 deletions.
8 changes: 6 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
flowchart LR
Processor-->|executes|Command
Command-->|modifies|Context
Command-->|returns|Result
```

# Commands
Expand Down Expand Up @@ -39,11 +40,14 @@ Command is an object that represents a single action. It is used to encapsulate

```ts
import { ICommand } from '@akdasa-studios/framework/commands'
import { Result } from '@akdasa-studios/framework/core'

class AddCommand implements ICommand<CalculatorContext, number> {

class AddCommand implements ICommand<CalculatorContext, Result<void, string>> {
constructor(private value: number) {}
public execute(context: CalculatorContext) {
context.set(context.value + this.value);
context.set(context.value + this.value)
return Result.ok()
}
public revert(context: CalculatorContext) {
context.set(context.value - this.value);
Expand Down
9 changes: 7 additions & 2 deletions lib/commands/command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { AnyResult } from '@lib/core'

/**
* Command is the base class for all commands.
*/
export interface ICommand<TContext, TResult> {
export interface ICommand<
TContext,
TResult extends AnyResult
> {
/**
* Executes the command.
* @param context The context in which the command is executed.
Expand All @@ -14,4 +19,4 @@ export interface ICommand<TContext, TResult> {
/**
* Any command that can be executed.
*/
export type AnyCommand = ICommand<unknown, unknown>
export type AnyCommand = ICommand<unknown, AnyResult>
4 changes: 2 additions & 2 deletions lib/commands/processor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnyResult } from '@lib/core'
import { ICommand, AnyCommand } from './command'


export class Processor {
private stack: AnyCommand[] = []

Expand All @@ -15,7 +15,7 @@ export class Processor {
* @param command Command to process.
* @returns {TResult} Returns the result of the command.
*/
execute<TContext, TResult>(command: ICommand<TContext, TResult>): TResult {
execute<TContext, TResult extends AnyResult>(command: ICommand<TContext, TResult>): TResult {
if (this.stack.includes(command)) {
throw new Error('Command is already executed.')
}
Expand Down
1 change: 1 addition & 0 deletions lib/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './result'
83 changes: 83 additions & 0 deletions lib/core/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

/**
* Any result
*/
export type AnyResult = Result<unknown, unknown>

/**
* Result is a discriminated union type that represents either a success or a failure.
*/
export class Result<
TSuccess = void,
TFail = string
> implements Result<TSuccess, TFail> {
private readonly _success: boolean;
private readonly _payload: TSuccess | TFail | undefined

/**
* Initializes a new instance of the Result class.
* @param success Indicates if the result is a success or a failure.
* @param payload The payload of the result.
*/
private constructor(
success: boolean,
payload: TSuccess | TFail | undefined,
) {
this._success = success
this._payload = payload
}

/**
* Creates a new result indicating a success with the specified payload.
* @param payload The payload of the result.
* @returns A new instance of the Result class.
*/
public static ok<TSuccessPayload, TFailPayload>(
payload?: TSuccessPayload
): Result<TSuccessPayload, TFailPayload> {
return new Result<TSuccessPayload, TFailPayload>(true, payload)
}

/**
* Creates a new result indicating a failure with the specified payload.
* @param payload The payload of the result.
* @returns A new instance of the Result class.
*/
public static fail<TSuccessPayload, TFailPayload>(
payload?: TFailPayload
): Result<TSuccessPayload, TFailPayload> {
return new Result<TSuccessPayload, TFailPayload>(false, payload)
}

/**
* Indicates if the result is a success.
* @returns True if the result is a success, otherwise false.
*/
get isSuccess(): boolean {
return this._success
}

/**
* Indicates if the result is a failure.
* @returns True if the result is a failure, otherwise false.
*/
get isFailure(): boolean {
return !this._success
}

/**
* Gets the payload of the result.
* @returns The payload of the result.
*/
get value(): TSuccess {
return this._payload as TSuccess
}

/**
* Gets the payload of the result.
* @returns The payload of the result.
*/
get error(): TFail {
return this._payload as TFail
}
}
5 changes: 5 additions & 0 deletions lib/domain/models/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Result } from '@lib/core'

export abstract class Builder<TModel, TError> {
public abstract build(): Result<TModel, TError>
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './domain'
export * from './commands'
export * from './core'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@akdasa-studios/framework",
"version": "0.0.5",
"version": "0.0.6",
"description": "Framework to build every app",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down
15 changes: 10 additions & 5 deletions tests/commands/processor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Result } from '@lib/core'
import { Processor, ICommand } from '@lib/commands'


class CalculatorContext {
public value = 0
}

class AddCommand implements ICommand<CalculatorContext, number> {
execute(context: CalculatorContext): number { return ++context.value }
revert(context: CalculatorContext): number { return context.value-- }
class AddCommand implements ICommand<CalculatorContext, Result<number, string>> {
execute(context: CalculatorContext): Result<number, string> {
return Result.ok(++context.value)
}
revert(context: CalculatorContext): Result<number, string> {
return Result.ok(context.value--)
}
}


Expand All @@ -32,7 +36,8 @@ describe('Processor', () => {

it('returns value of execution', () => {
const result = processor.execute(new AddCommand())
expect(result).toBe(1)
expect(result.isSuccess).toBeTruthy()
expect(result.value).toBe(1)
})

it('throws error if command is already executed', () => {
Expand Down
45 changes: 45 additions & 0 deletions tests/core/result.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Result } from '@lib/core'

describe('Result', () => {

/* -------------------------------------------------------------------------- */
/* ok */
/* -------------------------------------------------------------------------- */

describe('.ok()', () => {
it('should create a new result indicating a success with the specified payload', () => {
const result = Result.ok('success')
expect(result.isSuccess).toBe(true)
expect(result.isFailure).toBe(false)
expect(result.value).toBe('success')
})

it('should create a new result indicating a success with no payload', () => {
const result = Result.ok()
expect(result.isSuccess).toBe(true)
expect(result.isFailure).toBe(false)
expect(result.value).toBeUndefined()
})
})


/* -------------------------------------------------------------------------- */
/* fail */
/* -------------------------------------------------------------------------- */

describe('.fail()', () => {
it('should create a new result indicating a failure with the specified payload', () => {
const result = Result.fail('failure')
expect(result.isSuccess).toBe(false)
expect(result.isFailure).toBe(true)
expect(result.error).toBe('failure')
})

it('should create a new result indicating a failure with no payload', () => {
const result = Result.fail()
expect(result.isSuccess).toBe(false)
expect(result.isFailure).toBe(true)
expect(result.error).toBeUndefined()
})
})
})

0 comments on commit c500abb

Please sign in to comment.