Skip to content

Commit

Permalink
handler tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mccraigmccraig committed Nov 14, 2023
1 parent 688b1ec commit ec2a928
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 22 deletions.
52 changes: 34 additions & 18 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Effect } from "effect"
import { UnionFromTuple } from "./object_builders.ts"
import { Tagged, UPureWrapperProgram, PureWrapperProgram, PureWrapperProgramsInputTuple } from "./pure_wrapper.ts"
import { Tagged, UPureWrapperProgram, PureWrapperProgramsInputTuple } from "./pure_wrapper.ts"

// to type the multi-chain handler, need something like
// a conditional type which will look up return types from the program map object
Expand Down Expand Up @@ -35,32 +35,46 @@ export type X = IndexPureWrapperProgramTuple<[
{ tagStr: "foo", program: (ev: number) => Effect.Effect<never,never,number> },
{ tagStr: "bar", program: (ev: number) => Effect.Effect<never,never,number> }]>

// a bit tricky ... given a union of Tagged, and a list of UPureWrapperProgram, get the
// return type for the handler function, which is the return type of the program
// whose tag matches the event
export type ProgramDeps<T extends UPureWrapperProgram> = ReturnType<T['program']> extends Effect.Effect<infer R, infer _E, infer _V>
? R
: never

export type ProgramsDepsU<Tuple extends readonly [...UPureWrapperProgram[]]> = UnionFromTuple<{
+readonly [Index in keyof Tuple]: ProgramDeps<Tuple[Index]>
} & { length: Tuple['length'] }>

// use a conditional type to distribute the result type over a union of Tagged
export type DistributeEventResultTypes<I extends Tagged, Progs extends [...UPureWrapperProgram[]]> =
IndexPureWrapperProgramTuple<Progs>[I['tag']] extends PureWrapperProgram<infer I, infer _IFxFn, infer _PFn, infer _OFxFn>
? ReturnType<IndexPureWrapperProgramTuple<Progs>[I['tag']]['program']>
export type ProgramErrors<T extends UPureWrapperProgram> = ReturnType<T['program']> extends Effect.Effect<infer _R, infer E, infer _V>
? E
: never
export type ProgramsErrorsU<Tuple extends readonly [...UPureWrapperProgram[]]> = UnionFromTuple<{
+readonly [Index in keyof Tuple]: ProgramErrors<Tuple[Index]>
} & { length: Tuple['length'] }>

export type ProgramValue<T extends UPureWrapperProgram> = ReturnType<T['program']> extends Effect.Effect<infer _R, infer _E, infer V>
? V
: never

// not obvious - the conditional type distribute the value type over a union of Taggeds, resulting in a union of values!
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
export type DistributeProgramValueTypes<I extends Tagged, Progs extends [...UPureWrapperProgram[]]> =
IndexPureWrapperProgramTuple<Progs>[I['tag']] extends UPureWrapperProgram
? ProgramValue<IndexPureWrapperProgramTuple<Progs>[I['tag']]>
: never

// return a function of the union
// of all the input types handled by the supplied UPureWrapperPrograms,
// which uses a supplied UPureWrapperProgram to handle the input,
// returning the same results as the supplied UPureWrapperProgram


// TODO - the output type needs more work - it needs to collect
// the deps and errors from all the programs
export const makeHandlerProgram =
<EventHandlerPrograms extends [...UPureWrapperProgram[]],
Inputs extends UnionFromTuple<PureWrapperProgramsInputTuple<EventHandlerPrograms>>>
(eventHandlerPrograms: [...EventHandlerPrograms]):
(i: Inputs) => DistributeEventResultTypes<Inputs, EventHandlerPrograms> => {
<Programs extends [...UPureWrapperProgram[]],
Inputs extends UnionFromTuple<PureWrapperProgramsInputTuple<Programs>>>
(eventHandlerPrograms: [...Programs]):
(i: Inputs) => Effect.Effect<ProgramsDepsU<Programs>,
ProgramsErrorsU<Programs>,
DistributeProgramValueTypes<Inputs, Programs>> => {

const progsByEventTag = eventHandlerPrograms.reduce(
(m, p) => { m[p.eventTagStr] = p; return m },
(m, p) => { m[p.tagStr] = p; return m },
{} as { [index: string]: UPureWrapperProgram })

return (i: Inputs) => {
Expand All @@ -69,7 +83,9 @@ export const makeHandlerProgram =
// so prog.program should be the resolved PureWrapperProgram - but
// the type is dependent on the actual type of the input
console.log("multiProg: ", i)
return prog.program(i) as DistributeEventResultTypes<Inputs, EventHandlerPrograms>
return prog.program(i) as Effect.Effect<ProgramsDepsU<Programs>,
ProgramsErrorsU<Programs>,
DistributeProgramValueTypes<Inputs, Programs>>
} else
throw "NoProgram for tag: " + i.tag
}
Expand Down
112 changes: 108 additions & 4 deletions src/handler_test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,112 @@
// import { assertEquals } from "assert"
// import { Effect, Context } from "effect"
// import { invokeServiceFxFn } from "./fx_fn.ts"
import { } from "./handler.ts"
import { assertEquals } from "assert"
import { Effect, Context } from "effect"
import { ObjectStepSpec } from "./object_builders.ts"
import { Org, OrgService, getOrgByNick, User, UserService, getUserByIds, PushNotificationService, sendPush } from "./test_services.ts"
import { tag, pureWrapperProgram, pureWrapperChainProgram } from "./pure_wrapper.ts"
import { makeHandlerProgram } from "./handler.ts"

//////////////////// some steps //////////////////////////////////

export const getOrgObjectStepSpec /* : ObjectStepSpec<"org", { data: { org_nick: string } }, string, OrgService, never, Org> */ =
{
k: "org" as const,
inFn: (d: { data: { org_nick: string } }) => d.data.org_nick,
svcFn: getOrgByNick
}
export const getUserObjectStepSpec /* : ObjectStepSpec<"user", { data: { user_id: string }, org: Org }, {org_id: string, user_id: string}, UserService, never, User> */ =
{
k: "user" as const,
// note that this fn depends on the output of an OrgServiceI.getBy* step
inFn: (d: { data: { user_id: string }, org: Org }) => { return { org_id: d.org.id, user_id: d.data.user_id } },
svcFn: getUserByIds
}

export const pureSendWelcomePush = (d: { org: Org, user: User }) => {
return [{
user_id: d.user.id,
message: "Welcome " + d.user.name + " of " + d.org.name
}] as const
}

export const sendPusnNotificationStepSpec =
{
k: "sendPush" as const,
inFn: (d: { user_id: string, message: string }) => d,
svcFn: sendPush
}

//////////////////////// getOrg

type GetOrgInput = { tag: "GetOrg", data: { org_nick: string } }
const GetOrgInputTag = tag<GetOrgInput>("GetOrg")

const formatOrgOutputStepSpec: ObjectStepSpec<"apiResponse", Org, Org, never, never, {org: Org}> =
{
k: "apiResponse" as const,
inFn: (d: Org) => d,
svcFn: (d: Org) => Effect.succeed({org: d})
}

const getOrgProg = pureWrapperChainProgram<GetOrgInput>()(
GetOrgInputTag,
[getOrgObjectStepSpec] as const,
(d: { org: Org }) => [d.org] as const,
[formatOrgOutputStepSpec] as const)

//////////////////////// sendWelcomePush

type SendWelcomePushInput = { tag: "SendWelcomePush", data: { org_nick: string, user_id: string } }
const SendWelcomePushInputTag = tag<SendWelcomePushInput>("SendWelcomePush")

const sendWelcomePushInputFn = (i: SendWelcomePushInput) => {
return Effect.succeed({
...i,
org: { id: "foo", name: "Foo" },
user: { id: "100", name: "Bar" } })
}

const sendWelcomePushOutputFn = (d: readonly [{ user_id: string, message: string }]) => {
return Effect.succeed({sendPush: "push sent OK: " + d[0].user_id.toString() + ", " + d[0].message})
}

const sendWelcomePushProg = pureWrapperProgram<SendWelcomePushInput>()(SendWelcomePushInputTag,
sendWelcomePushInputFn,
pureSendWelcomePush,
sendWelcomePushOutputFn)


// a simple context with an OrgService and a UserService which echo data back
const echoContext = Context.empty().pipe(
Context.add(OrgService, OrgService.of({
getById: (id: string) => Effect.succeed({ id: id, name: "Foo" }),
getByNick: (nick: string) => Effect.succeed({ id: nick, name: "Foo" })
})),
Context.add(UserService, UserService.of({
getByIds: (d: { org_id: string, user_id: string }) => Effect.succeed({ id: d.user_id, name: "Bar" })
})),
Context.add(PushNotificationService, PushNotificationService.of({
sendPush: (d: {user_id: string, message: string}) => Effect.succeed("push sent OK: " + d.message)
})))

////////////////////////// handler ///////////////////////////////////

const programs = [getOrgProg, sendWelcomePushProg]

const handlerProgram = makeHandlerProgram(programs)

Deno.test("makeHandlerProgram", () => {
const getOrgInput: GetOrgInput = { tag: "GetOrg", data: { org_nick: "foo"} }
// const sendWelcomePushInput: SendWelcomePushInput = { tag: "SendWelcomePush", data: { org_nick: "foo", user_id: "100" } }

const handlerEffect = handlerProgram(getOrgInput)
const runnable = Effect.provide(handlerEffect, echoContext)
const r = Effect.runSync(runnable)

assertEquals(r,
{
...getOrgInput,
org: { id: "foo", name: "Foo" },
GetOrg: [{id: "foo", name: "Foo"}],
apiResponse: {org: {id: "foo", name: "Foo"}}
})
})

0 comments on commit ec2a928

Please sign in to comment.