This repository has been archived by the owner on Sep 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add core data handling module with tests
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
1 parent
65da7b0
commit 293f6d9
Showing
5 changed files
with
306 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}))) | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./data" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}) | ||
}) |