-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
748ec93
commit 1b8e93b
Showing
4 changed files
with
325 additions
and
0 deletions.
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,61 @@ | ||
import { assertEquals } from "../deps.ts"; | ||
import { compose, newEther, newEtherSymbol, runEther } from "./ether.ts"; | ||
|
||
type Article = { | ||
createdAt: string; | ||
updatedAt: string; | ||
body: string; | ||
}; | ||
|
||
interface ArticleRepository { | ||
has: (id: string) => Promise<boolean>; | ||
insert: (id: string, article: Partial<Article>) => Promise<void>; | ||
} | ||
const repoSymbol = newEtherSymbol<ArticleRepository>(); | ||
|
||
type Req = { | ||
id: string; | ||
timestamp: string; | ||
body: string; | ||
}; | ||
const serviceSymbol = newEtherSymbol<(req: Req) => Promise<void>>(); | ||
const service = newEther( | ||
serviceSymbol, | ||
({ repo }) => async ({ id, timestamp, body }: Req) => { | ||
if (!await repo.has(id)) { | ||
return; | ||
} | ||
await repo.insert(id, { updatedAt: timestamp, body }); | ||
return; | ||
}, | ||
{ | ||
repo: repoSymbol, | ||
}, | ||
); | ||
|
||
Deno.test("runs an Ether", async () => { | ||
const mockRepository = newEther( | ||
repoSymbol, | ||
() => ({ | ||
has: (id) => { | ||
assertEquals(id, "foo"); | ||
return Promise.resolve(true); | ||
}, | ||
insert: (id, article) => { | ||
assertEquals(id, "foo"); | ||
assertEquals(article, { | ||
updatedAt: "2020-01-01T13:17:00Z", | ||
body: "Hello, World!", | ||
}); | ||
return Promise.resolve(); | ||
}, | ||
}), | ||
); | ||
const injecting = compose(mockRepository); | ||
const ether = injecting(service); | ||
await runEther(ether)({ | ||
id: "foo", | ||
timestamp: "2020-01-01T13:17:00Z", | ||
body: "Hello, World!", | ||
}); | ||
}); |
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,192 @@ | ||
/** | ||
* This package provides a dependency container combinator `EtherT`/`Ether` and associated functions. | ||
* | ||
* @packageDocumentation | ||
*/ | ||
|
||
import type { Get1, Hkt2, Hkt3 } from "./hkt.ts"; | ||
import type { Monad } from "./type-class/monad.ts"; | ||
import { IdentityHkt } from "./identity.ts"; | ||
import { doT } from "./cat.ts"; | ||
|
||
/** | ||
* A symbol among Ether values, which resolves into the actual producing type. | ||
*/ | ||
export type EtherSymbol<T> = symbol & { | ||
readonly etherValue: T; | ||
}; | ||
/** | ||
* Gets the value type from an `EtherSymbol`. | ||
*/ | ||
export type EtherValue<S> = S extends EtherSymbol<infer T> ? T : never; | ||
|
||
/** | ||
* Makes a new `EtherSymbol` for `newEther` or `newEtherT`. It should be created with an interface definition. | ||
* | ||
* @returns The unique `EtherSymbol`. | ||
*/ | ||
export const newEtherSymbol = <T>(): EtherSymbol<T> => | ||
Symbol() as EtherSymbol<T>; | ||
|
||
/** | ||
* A dependencies object which will be passed to `Ether`'s handler. | ||
*/ | ||
export type EtherDeps<D> = { | ||
[K in keyof D]: EtherValue<D[K]>; | ||
}; | ||
|
||
/** | ||
* Omits fields of type `T` from a dependencies object type `D`. | ||
*/ | ||
export type OmitType<D, T> = { | ||
[K in keyof D as D[K] extends EtherSymbol<T> ? never : K]: D[K]; | ||
}; | ||
|
||
/** | ||
* EtherT is a function which needs dependencies `D` and returns `M<T>`. | ||
*/ | ||
export interface EtherT<D, M, T> { | ||
readonly selfSymbol: EtherSymbol<T>; | ||
readonly handler: (resolved: EtherDeps<D>) => Get1<M, T>; | ||
readonly depSymbols: D; | ||
} | ||
|
||
/** | ||
* Executes an `EtherT` which requires no dependencies. | ||
* | ||
* @param ether Execution target. | ||
* @returns The return value of `ether`'s handler. | ||
*/ | ||
export const runEtherT = <M, T>( | ||
ether: EtherT<Record<string, never>, M, T>, | ||
): Get1<M, T> => ether.handler({}); | ||
|
||
/** | ||
* Creates a new `EtherT`. | ||
* | ||
* @param selfSymbol The unique symbol corresponding to the return type `T`. | ||
* @param handler The function which creates an object of type `T` from the dependencies object on `M`. | ||
* @param depSymbols The declaration of symbols by key, for `handler` which requires dependencies. | ||
* @returns A new `EtherT` having parameters as fields. | ||
*/ | ||
export const newEtherT = <M>() => | ||
< | ||
T, | ||
const D extends Record<string, symbol> = Record<string, never>, | ||
>( | ||
selfSymbol: EtherSymbol<T>, | ||
handler: (deps: EtherDeps<D>) => Get1<M, T>, | ||
depSymbols: D = {} as D, | ||
): EtherT<D, M, T> => ({ selfSymbol, handler, depSymbols }); | ||
|
||
/** | ||
* Composes two `EtherT`s into a new one. Injects `lower` into `upper`. | ||
* | ||
* @param monad The monad implementation for `M`. | ||
* @param lower The lower dependency, which will be injected, such as a database adaptor. | ||
* @param upper The upper dependency, which will injected `lower`, such as a service function. | ||
* @returns The injected `EtherT`, which will be resolved the matching dependencies on `upper` with `lower`'s handler. | ||
*/ | ||
export const composeT = | ||
<M>(monad: Monad<M>) => | ||
<const D extends Record<string, symbol>, T>(lower: EtherT<D, M, T>) => | ||
<const E extends Record<string, symbol>, U>( | ||
upper: EtherT<E, M, U>, | ||
): EtherT<OmitType<E, T> & D, M, U> => { | ||
const targetKeys = Object.entries(upper.depSymbols).filter(([, sym]) => | ||
sym === lower.selfSymbol | ||
).map(([key]) => key); | ||
return { | ||
selfSymbol: upper.selfSymbol, | ||
handler: (deps) => | ||
doT(monad).addM("resolved", lower.handler(deps)) | ||
.addMWith("ret", ({ resolved }) => { | ||
const depsForUpper: Record<string, unknown> = { | ||
...deps, | ||
}; | ||
for (const targetKey of targetKeys) { | ||
depsForUpper[targetKey] = resolved; | ||
} | ||
return upper.handler(depsForUpper as EtherDeps<E>); | ||
}).finish(({ ret }) => ret), | ||
depSymbols: Object.fromEntries( | ||
Object.entries({ ...upper.depSymbols, ...lower.depSymbols }) | ||
.filter( | ||
([, depSym]) => depSym === lower.selfSymbol, | ||
), | ||
) as OmitType<E, T> & D, | ||
}; | ||
}; | ||
|
||
export interface EtherTHkt extends Hkt3 { | ||
readonly type: EtherT<this["arg3"], this["arg2"], this["arg1"]>; | ||
} | ||
|
||
/** | ||
* Ether is a function which needs dependencies `D` and returns `M<T>`. | ||
*/ | ||
export type Ether<D, T> = EtherT<D, IdentityHkt, T>; | ||
|
||
/** | ||
* Executes an `Ether` which requires no dependencies. | ||
* | ||
* @param ether Execution target. | ||
* @returns The return value of `ether`'s handler. | ||
*/ | ||
export const runEther = <T>(ether: Ether<Record<string, never>, T>): T => | ||
ether.handler({}); | ||
|
||
/** | ||
* Creates a new `Ether`. | ||
* | ||
* @param selfSymbol The unique symbol corresponding to the return type `T`. | ||
* @param handler The function which creates an object of type `T` from the dependencies object. | ||
* @param depSymbols The declaration of symbols by key, for `handler` which requires dependencies. | ||
* @returns A new `Ether` having parameters as fields. | ||
*/ | ||
export const newEther = < | ||
T, | ||
const D extends Record<string, symbol> = Record<string, never>, | ||
>( | ||
selfSymbol: EtherSymbol<T>, | ||
handler: (deps: EtherDeps<D>) => T, | ||
depSymbols: D = {} as D, | ||
): Ether<D, T> => ({ selfSymbol, handler, depSymbols }); | ||
|
||
/** | ||
* Composes two `Ether`s into a new one. Injects `lower` into `upper`. | ||
* | ||
* @param lower The lower dependency, which will be injected, such as a database adaptor. | ||
* @param upper The upper dependency, which will injected `lower`, such as a service function. | ||
* @returns The injected `Ether`, which will be resolved the matching dependencies on `upper` with `lower`'s handler. | ||
*/ | ||
export const compose = | ||
<const D extends Record<string, symbol>, T>(lower: Ether<D, T>) => | ||
<const E extends Record<string, symbol>, U>( | ||
upper: Ether<E, U>, | ||
): Ether<OmitType<E, T> & D, U> => { | ||
const targetKeys = Object.entries(upper.depSymbols).filter(([, sym]) => | ||
sym === lower.selfSymbol | ||
).map(([key]) => key); | ||
return { | ||
selfSymbol: upper.selfSymbol, | ||
handler: (deps) => { | ||
const resolved = lower.handler(deps); | ||
const depsForUpper: Record<string, unknown> = { ...deps }; | ||
for (const targetKey of targetKeys) { | ||
depsForUpper[targetKey] = resolved; | ||
} | ||
return upper.handler(depsForUpper as EtherDeps<E>); | ||
}, | ||
depSymbols: Object.fromEntries( | ||
Object.entries({ ...upper.depSymbols, ...lower.depSymbols }) | ||
.filter( | ||
([, depSym]) => depSym === lower.selfSymbol, | ||
), | ||
) as OmitType<E, T> & D, | ||
}; | ||
}; | ||
|
||
export interface EtherHkt extends Hkt2 { | ||
readonly type: Ether<this["arg2"], this["arg1"]>; | ||
} |
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,71 @@ | ||
import { assertEquals } from "../deps.ts"; | ||
import { catT } from "./cat.ts"; | ||
import { mapOr, none, Option, some } from "./option.ts"; | ||
import { local } from "./reader.ts"; | ||
import { run } from "./reader.ts"; | ||
import { ask } from "./reader.ts"; | ||
import { monad } from "./reader.ts"; | ||
import { Reader } from "./reader.ts"; | ||
|
||
Deno.test("ask", () => { | ||
interface User { | ||
name: string; | ||
} | ||
const userCat = catT(monad<User>()); | ||
|
||
const message = (): Reader<User, string> => | ||
userCat(ask<User>()).finish( | ||
({ name }) => `Hello, ${name}!`, | ||
); | ||
const box = (): Reader<User, string> => | ||
userCat(message()).finish( | ||
(mes) => `<div class="message-box">${mes}</div>`, | ||
); | ||
|
||
assertEquals( | ||
run(box())({ name: "John" }), | ||
'<div class="message-box">Hello, John!</div>', | ||
); | ||
assertEquals( | ||
run(box())({ name: "Alice" }), | ||
'<div class="message-box">Hello, Alice!</div>', | ||
); | ||
}); | ||
|
||
Deno.test("local", () => { | ||
interface User { | ||
name: string; | ||
id: string; | ||
score: number; | ||
} | ||
interface Bulk { | ||
users: readonly User[]; | ||
} | ||
|
||
const extractFromBulk = (id: string) => | ||
local((bulk: Bulk): Option<User> => { | ||
const found = bulk.users.find((elem) => elem.id === id); | ||
if (!found) { | ||
return none(); | ||
} | ||
return some(found); | ||
}); | ||
const scoreReport = (id: string): Reader<Bulk, string> => | ||
extractFromBulk(id)( | ||
catT(monad<Option<User>>())(ask<Option<User>>()) | ||
.finish( | ||
mapOr("user not found")(({ name, score }) => | ||
`${name}'s score is ${score}!` | ||
), | ||
), | ||
); | ||
|
||
const bulk: Bulk = { | ||
users: [ | ||
{ name: "John", id: "1321", score: 12130 }, | ||
{ name: "Alice", id: "4209", score: 320123 }, | ||
], | ||
}; | ||
assertEquals(run(scoreReport("1321"))(bulk), "John's score is 12130!"); | ||
assertEquals(run(scoreReport("4209"))(bulk), "Alice's score is 320123!"); | ||
}); |