From ec2a92860a7b8755fdcb229cbd6a7a8f9cba7ec2 Mon Sep 17 00:00:00 2001 From: mccraig mccraig of the clan mccraig Date: Tue, 14 Nov 2023 16:49:18 +0000 Subject: [PATCH] handler tests --- src/handler.ts | 52 +++++++++++++------- src/handler_test.ts | 112 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 142 insertions(+), 22 deletions(-) diff --git a/src/handler.ts b/src/handler.ts index 8e350bd..0b98033 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -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 @@ -35,32 +35,46 @@ export type X = IndexPureWrapperProgramTuple<[ { tagStr: "foo", program: (ev: number) => Effect.Effect }, { tagStr: "bar", program: (ev: number) => Effect.Effect }]> -// 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 = ReturnType extends Effect.Effect + ? R + : never + +export type ProgramsDepsU = UnionFromTuple<{ + +readonly [Index in keyof Tuple]: ProgramDeps +} & { length: Tuple['length'] }> -// use a conditional type to distribute the result type over a union of Tagged -export type DistributeEventResultTypes = - IndexPureWrapperProgramTuple[I['tag']] extends PureWrapperProgram - ? ReturnType[I['tag']]['program']> +export type ProgramErrors = ReturnType extends Effect.Effect + ? E + : never + export type ProgramsErrorsU = UnionFromTuple<{ + +readonly [Index in keyof Tuple]: ProgramErrors + } & { length: Tuple['length'] }> + +export type ProgramValue = ReturnType extends Effect.Effect + ? 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 = + IndexPureWrapperProgramTuple[I['tag']] extends UPureWrapperProgram + ? ProgramValue[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: [...EventHandlerPrograms]): - (i: Inputs) => DistributeEventResultTypes => { + >> + (eventHandlerPrograms: [...Programs]): + (i: Inputs) => Effect.Effect, + ProgramsErrorsU, + DistributeProgramValueTypes> => { 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) => { @@ -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 + return prog.program(i) as Effect.Effect, + ProgramsErrorsU, + DistributeProgramValueTypes> } else throw "NoProgram for tag: " + i.tag } diff --git a/src/handler_test.ts b/src/handler_test.ts index 1245cc3..cd55302 100644 --- a/src/handler_test.ts +++ b/src/handler_test.ts @@ -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("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()( + 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("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()(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"}} + }) }) \ No newline at end of file