Skip to content

Commit

Permalink
Added Tasks!
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanburke committed Oct 31, 2024
1 parent 2b537f4 commit b4da104
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 17 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "functype",
"version": "0.8.30",
"version": "0.8.31",
"description": "A smallish functional library for TypeScript",
"author": "[email protected]",
"license": "MIT",
Expand Down
15 changes: 15 additions & 0 deletions src/core/base/Base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Typeable } from "../../typeable/Typeable"

/**
* Base Object from which most other objects inherit
* @param type
* @constructor
*/
export function Base(type: string) {
return {
...Typeable(type),
toString() {
return `${type}()`
},
}
}
31 changes: 31 additions & 0 deletions src/core/error/Throwable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Typeable } from "../../typeable/Typeable"

const NAME = "Throwable" as const

export type Throwable = Error &
Typeable<typeof NAME> & {
readonly data?: unknown // Optional readonly data field for extra info
}

// AppError factory function
export const Throwable = (srcError: unknown, data?: unknown): Throwable => {
const message: string =
srcError instanceof Error ? srcError.message : typeof srcError === "string" ? srcError : "An unknown error occurred" // Fallback message if srcError is neither Error nor string

const stack: string | undefined = srcError instanceof Error ? srcError.stack : undefined

// Create a new Error object
const error = new Error(message)
error.name = NAME

// Return the custom error with readonly properties for message, stack, and data
return {
_tag: NAME, // Use const for the _tag to make it immutable
name: error.name,
message: error.message,
stack: stack ?? error.stack, // Use original stack if available, otherwise fallback to new error's stack
data, // Optional readonly data
}
}

Throwable.NAME = NAME
59 changes: 59 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Either, Left, Right } from "../../either/Either"
import { Base } from "../base/Base"
import { Throwable } from "../error/Throwable"

export type AppException<T> = Either<Throwable, T>

/**
* AppException factory function
* @param error
* @param data
* @constructor
*/
export const AppException = <T>(error: unknown, data?: unknown): AppException<T> => {
const appError = Throwable(error, data)
return {
...Base("AppException"),
...Left(appError),
}
}

export type AppResult<T> = Either<Throwable, T>

export const AppResult = <T>(data: T): AppResult<T> => {
return {
...Base("AppResult"),
...Right(data),
}
}

export type Task<T> = Either<Throwable, T>

export function Task<T>(f: () => T, e: (error: unknown) => unknown = (error: unknown) => error): Task<T> {
try {
return AppResult<T>(f())
} catch (error) {
return AppException<T>(e(error))
}
}

Task.success = <T>(data: T) => AppResult<T>(data)
Task.fail = <T>(error: unknown) => AppException<T>(error)

export type AsyncTask<T> = Promise<Task<T>>

export async function AsyncTask<T>(
f: () => T,
e: (error: unknown) => unknown = (error: unknown) => error,
): AsyncTask<T> {
try {
const result = await f()
return AppResult<T>(result)
} catch (error) {
const result = await e(error)
return AppException<T>(result)
}
}

AsyncTask.success = <T>(data: T) => AppResult<T>(data)
AsyncTask.fail = <T>(error: unknown) => AppException<T>(error)
11 changes: 0 additions & 11 deletions src/either/Either.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,6 @@ export const tryCatchAsync = async <L extends Type, R extends Type>(
}
}

export const tryCatchSync = <L extends Type, R extends Type>(
f: () => R,
onError: (error: unknown) => L,
): Either<L, R> => {
try {
return Right<L, R>(f())
} catch (error: unknown) {
return Left<L, R>(onError(error))
}
}

export const Either = {
sequence: <L extends Type, R extends Type>(eithers: Either<L, R>[]): Either<L, R[]> => {
const rights: R[] = []
Expand Down
8 changes: 4 additions & 4 deletions src/option/Option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import stringify from "safe-stable-stringify"

import { Functor, Type } from "../functor"
import { Either, Left, List, Right, Traversable } from "../index"
import { _Iterable_, Seq } from "../iterable"
import { Typeable } from "../typeable/Typeable"
import { Valuable } from "../valuable/Valuable"

export type Option<T extends Type> = {
readonly _tag: "Some" | "None"
Expand All @@ -25,7 +25,7 @@ export type Option<T extends Type> = {
toEither<E>(left: E): Either<E, T>
toString(): string
toValue(): { _tag: "Some" | "None"; value: T }
} & (Traversable<T> & Functor<T> & Typeable<"Some" | "None">)
} & (Traversable<T> & Functor<T> & Typeable<"Some" | "None"> & Valuable<T>)

export const Some = <T extends Type>(value: T): Option<T> => ({
_tag: "Some",
Expand Down Expand Up @@ -63,15 +63,15 @@ export const Some = <T extends Type>(value: T): Option<T> => ({

const NONE: Option<never> = {
_tag: "None",
value: undefined,
value: undefined as never,
isEmpty: true,
get: () => {
throw new Error("Cannot call get() on None")
},
getOrElse: <T>(defaultValue: T) => defaultValue,
orElse: <T>(alternative: Option<T>) => alternative,
map: <U extends Type>(f: (value: never) => U) => NONE as unknown as Option<U>,
filter(predicate: (value: never) => boolean): Option<never> {
filter(_predicate: (value: never) => boolean): Option<never> {
return NONE
},
flatMap: <U extends Type>(f: (value: never) => Option<U>) => NONE as unknown as Option<U>,
Expand Down
2 changes: 1 addition & 1 deletion src/valuable/Valuable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export function Valuable<T>(value: T) {
}

// Extract the return type of the EncodedType function
export type Valuable<T extends string> = ReturnType<typeof Valuable<T>>
export type Valuable<T> = ReturnType<typeof Valuable<T>>
157 changes: 157 additions & 0 deletions test/core/task/task.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { isLeft, isRight } from "../../../src"
import { Throwable } from "../../../src/core/error/Throwable"
import { AppException, AppResult, AsyncTask, Task } from "../../../src/core/task/Task"

describe("AppException", () => {
test("should create an AppException with error", () => {
const error = new Error("Test error")
const result = AppException<string>(error)

expect(isLeft(result)).toBe(true)
expect(result._tag).toBe("Left")
expect((result.value as Throwable)._tag).toBe("Throwable")
expect((result.value as Error).message).toBe("Test error")
})

test("should create an AppException with error and additional data", () => {
const error = new Error("Test error")
const data = { additionalInfo: "extra data" }
const result = AppException<string>(error, data)

expect(isLeft(result)).toBe(true)
expect(result._tag).toBe("Left")
expect(result.value instanceof Error).toBe(false)
expect((result.value as unknown as Throwable).data).toEqual(data)
})
})

describe("AppResult", () => {
test("should create a successful AppResult", () => {
const data = "test data"
const result = AppResult(data)

expect(isRight(result)).toBe(true)
expect(result._tag).toBe("Right")
expect(result.value).toBe(data)
})

test("should work with different data types", () => {
const numberResult = AppResult(42)
const objectResult = AppResult({ key: "value" })
const arrayResult = AppResult([1, 2, 3])

expect(isRight(numberResult)).toBe(true)
expect(numberResult.value).toBe(42)
expect(isRight(objectResult)).toBe(true)
expect(objectResult.value).toEqual({ key: "value" })
expect(isRight(arrayResult)).toBe(true)
expect(arrayResult.value).toEqual([1, 2, 3])
})
})

describe("Task", () => {
test("should handle successful operations", () => {
const result = Task(() => "success")

expect(isRight(result)).toBe(true)
expect(result.value).toBe("success")
})

test("should handle errors", () => {
const error = new Error("Task failed")
const result = Task(() => {
throw error
})

expect(isLeft(result)).toBe(true)
expect(result.value._tag).toBe("Throwable")
expect((result.value as Error).message).toBe("Task failed")
})

test("should use custom error handler", () => {
const error = new Error("Original error")
const customError = "Custom error message"
const result = Task(
() => {
throw error
},
() => customError,
)

expect(isLeft(result)).toBe(true)
expect(result.value.message).toBe(customError)
})

test("Task.success should create successful result", () => {
const result = Task.success("data")

expect(isRight(result)).toBe(true)
expect(result.value).toBe("data")
})

test("Task.fail should create failure result", () => {
const error = new Error("Failed")
const result = Task.fail(error)

expect(isLeft(result)).toBe(true)
expect((result.value as Throwable)._tag).toBe("Throwable")
expect((result.value as Error).message).toBe("Failed")
})
})

describe("AsyncTask", () => {
test("should handle successful async operations", async () => {
const result = await AsyncTask(async () => "success")

expect(result).toBe("success")
})

test("should handle async errors", async () => {
const error = new Error("Async task failed")
try {
await AsyncTask(async () => {
throw error
})
} catch (error) {
expect((error as unknown as Throwable).message).toBe("Async task failed")
}
})

test("should use custom async error handler", async () => {
const error = new Error("Original error")
const customError = "Custom async error message"
try {
await AsyncTask(
async () => {
throw error
},
async () => customError,
)
} catch (error) {
expect((error as unknown as Throwable).message).toBe(customError)
}
})

test("AsyncTask.success should create successful result", () => {
const result = AsyncTask.success("data")

expect(isRight(result)).toBe(true)
expect(result.value).toBe("data")
})

test("AsyncTask.fail should create failure result", () => {
const error = new Error("Failed")
const result = AsyncTask.fail(error)

expect(isLeft(result)).toBe(true)
expect((result.value as unknown as Throwable)._tag).toBe("Throwable")
expect((result.value as Error).message).toBe("Failed")
})

test("should handle promises", async () => {
const successPromise = Promise.resolve("success")
const result = await AsyncTask(async () => successPromise)

expect(result).toBe("success")
})
})

0 comments on commit b4da104

Please sign in to comment.