Skip to content
This repository has been archived by the owner on Sep 15, 2024. It is now read-only.

Commit

Permalink
Add core data handling module with tests
Browse files Browse the repository at this point in the history
Introduced a new core module for data handling in the `src/core` directory. This includes defining types for data-driven components and functions for retrieving and managing game data. Comprehensive tests were added to ensure the validity and functionality of the new data handling mechanisms.
  • Loading branch information
mysticfall committed Aug 26, 2024
1 parent 65da7b0 commit 293f6d9
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ programming paradigms, it's purely experimental at this point and not suitable f

| Statements | Branches | Functions | Lines |
| --------------------------- | ----------------------- | ------------------------- | ----------------- |
| ![Statements](https://img.shields.io/badge/statements-17.91%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-96.23%25-brightgreen.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-86.52%25-yellow.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-17.91%25-red.svg?style=flat) |
| ![Statements](https://img.shields.io/badge/statements-18.94%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-96.42%25-brightgreen.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-87.16%25-yellow.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-18.94%25-red.svg?style=flat) |

## Motivation

Expand Down
171 changes: 171 additions & 0 deletions src/core/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Definitions of common types related to handling game data.
* @module
*/
import {Optional} from "@fp-ts/optic"
import * as E from "fp-ts/Either"
import {Either} from "fp-ts/Either"
import {pipe} from "fp-ts/function"
import * as O from "fp-ts/Option"
import {Option} from "fp-ts/Option"
import {Show} from "fp-ts/Show"
import * as T from "io-ts"
import {Type} from "io-ts"
import {PathReporter} from "io-ts/PathReporter"
import {BaseError, BaseErrorT} from "../common"

/**
* Represents a data-driven subject with an optic and a codec.
*
* The `DataDriven` interface is a generic interface that defines a data-driven subject.
* It consists of two readonly properties: `optic` and `codec`.
*
* The `optic` property represents an optic that allows for traversal and manipulation of data within a given context.
* It is of type `Optional<TContext, TData>`, indicating that it is an optional traversal that might not always succeed.
*
* The `codec` property represents a type that specifies the encoding/decoding rules for the data.
* It is of type `Type<TData>`, indicating that it is a type that describes the structure and behaviour of the data.
*
* @template TData The type of the data.
* @template TContext The type of the context in which the data resides.
*/
export interface DataDriven<TData, TContext> {

/**
* Represents an optic that can be used for accessing the data associated with this subject.
*
* @readonly
*/
readonly optic: Optional<TContext, TData>

/**
* Represents a codec for handling data of type `TData`.
*
* @readonly
*/
readonly codec: Type<TData>
}

/**
* Represents the validation rules for {@link MissingDataError}.
*/
export const MissingDataErrorT = T.intersection([
T.readonly(T.type({
type: T.literal("MissingData")
})),
BaseErrorT
], "MissingDataError")

/**
* Represents an error that occurs when the associated data cannot be accessed.
*/
export type MissingDataError = {
readonly type: "MissingData"
} & BaseError

/**
* Represents the validation rules for {@link InvalidDataError}.
*/
export const InvalidDataErrorT = T.intersection([
T.readonly(T.type({
type: T.literal("InvalidData")
})),
BaseErrorT
], "InvalidDataError")

/**
* Represents an error that occurs when the associated data has an invalid type.
*/
export type InvalidDataError = {
readonly type: "InvalidData"
} & BaseError

/**
* Finds data associated with the given subject in the provided context.
*
* @param {TContext} context - The context used for finding the data.
* @param {Show<TSubject>} [show] - The show function used to describe the subject in error messages.
*
* @template TData The type of the data.
* @template TContext The type of the context.
* @template TSubject The type of the subject.
*
* @return {(subject: TSubject) => Either<InvalidDataError, Option<TData>>}
* The function that takes a subject and returns either the data or an {@link InvalidDataError}.
*/
export function findData<
TData,
TContext,
TSubject extends DataDriven<TData, TContext> = DataDriven<TData, TContext>
>(
context: TContext,
show?: Show<TSubject>
): (subject: TSubject) => Either<InvalidDataError, Option<TData>> {

const getError = (subject: TSubject) => pipe(
show,
O.fromNullable,
O.map(({show}) => show),
O.ap(O.of(subject)),
O.map(msg => `${msg} has an invalid data:`),
O.getOrElse(() => "Invalid data:")
)

return subject => pipe(
context,
subject.optic.getOptic,
O.fromEither,
O.map(subject.codec.decode),
O.sequence(E.Applicative),
E.mapLeft(e => ({
type: "InvalidData",
message: [
getError(subject),
pipe(e, E.left, PathReporter.report)
].join(" "),
details: e[0]
}))
)
}

/**
* Retrieves data associated with the given subject in the provided context.
*
* @param {TContext} context - The context used for finding the data.
* @param {Show<TSubject>} [show] - The show function used to describe the subject in error messages.
*
* @template TData The type of the data.
* @template TContext The type of the context.
* @template TSubject The type of the subject.
*
* @return {(subject: TSubject) => Either<InvalidDataError, Option<TData>>}
* The function that takes a subject and returns either the data or an error
* ({@link MissingDataError} when the data cannot be found, or {@link InvalidDataError} when invalid).
*/
export function getData<
TData,
TContext,
TSubject extends DataDriven<TData, TContext> = DataDriven<TData, TContext>
>(
context: TContext,
show?: Show<TSubject>
): (subject: TSubject) => Either<MissingDataError | InvalidDataError, TData> {

const getError = (subject: TSubject) => pipe(
show,
O.fromNullable,
O.map(({show}) => show),
O.ap(O.of(subject)),
O.map(msg => `${msg} has an invalid data:`),
O.getOrElse(() => "Invalid data:")
)

return subject => pipe(
subject,
findData<TData, TContext, TSubject>(context, show),
E.flatMap(E.fromOption<MissingDataError>(() => ({
type: "MissingData",
message: getError(subject)
})))
)
}
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./data"
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./actor/index"
export * from "./attribute/index"
export * from "./common/index"
export * from "./core/index"
export * from "./game"
export * from "./lore"
export * from "./prompt"
Expand Down
132 changes: 132 additions & 0 deletions test/core/data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as Optic from "@fp-ts/optic"
import {Optional} from "@fp-ts/optic"
import * as E from "fp-ts/Either"
import {pipe} from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as T from "io-ts"
import {describe, expect, it} from "vitest"
import {DataDriven, findData, getData, InvalidDataErrorT, MissingDataErrorT} from "../../src"

type Context = {
readonly items: ReadonlyArray<Item>
}

const ItemT = T.readonly(T.type({
name: T.string
}), "Item")

type Item = T.TypeOf<typeof ItemT>

class ItemData implements DataDriven<Item, Context> {

readonly codec = ItemT

constructor(
readonly optic: Optional<Context, Item>,
) {
}

static at(index: number): ItemData {
return new ItemData(Optic.id<Context>().at("items").index(index))
}
}

const allItems = {
items: [{
name: "item1"
}, {
title: "item2" // Invalid item.
}]
} as Context

describe("findData", () => {

it("should return Some when the specified data exists in the context.", () => {
const result = pipe(
ItemData.at(0),
findData<Item, Context>(allItems)
)

const name = pipe(
result,
O.fromEither,
O.flatten,
O.map(({name}) => name),
O.toUndefined
)

expect(name).toBe("item1")
})

it("should return None when the specified data exists in the context.", () => {
const result = pipe(
ItemData.at(2),
findData<Item, Context>(allItems)
)

expect(E.isRight(result)).toBeTruthy()

const item = pipe(
result,
O.fromEither,
O.flatten
)

expect(O.isNone(item)).toBeTruthy()
})

it("should return InvalidDataError when the specified data is invalid.", () => {
const error = pipe(
ItemData.at(1),
findData<Item, Context>(allItems),
E.swap,
O.fromEither,
O.toUndefined
)

expect(InvalidDataErrorT.is(error)).toBeTruthy()
})
})

describe("getData", () => {

it("should return the associated data when it exists.", () => {
const result = pipe(
ItemData.at(0),
getData<Item, Context>(allItems)
)

const name = pipe(
result,
E.map(({name}) => name),
O.fromEither,
O.toUndefined
)

expect(name).toBe("item1")
})

it("should return MissingDataError when the specified data doesn't exist.", () => {
const error = pipe(
ItemData.at(2),
getData<Item, Context>(allItems),
E.swap,
O.fromEither,
O.toUndefined
)

expect(MissingDataErrorT.is(error)).toBeTruthy()
})

it("should return InvalidDataError when the specified data is invalid.", () => {
const error = pipe(
ItemData.at(1),
getData<Item, Context>(allItems),
E.swap,
O.fromEither,
O.toUndefined
)

expect(InvalidDataErrorT.is(error)).toBeTruthy()
})
})

0 comments on commit 293f6d9

Please sign in to comment.