Created
July 18, 2023 07:21
-
-
Save isthatcentered/8761a5e6771a70d5b5bb7f047ca56cd8 to your computer and use it in GitHub Desktop.
Gherkin DSL
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
// Note, this is a (very) quick POC, this is far from perfect and needs improvement. But it works | |
import { pipe, identity } from "@effect/data/Function" | |
import * as T from "@effect/io/Effect" | |
import { TaggedClass } from "@effect/data/Data" | |
import * as Match from "@effect/match" | |
export class Given<R, S, A> extends TaggedClass("Given")<{ | |
fa: T.Effect<R, any, S> | |
_A: (a: never) => A | |
}> {} | |
export class AndGiven<R, S, A> extends TaggedClass("AndGiven")<{ | |
use: <X>( | |
go: <R1, R2, S1, S2>(params: { | |
fa: Gherkin<R1, S1, void> | |
fb: (s: S1) => T.Effect<R2, any, S2> | |
_R: (a: R1 | R2) => R | |
_S: (a: S2) => S | |
_A: (a: never) => A | |
}) => X, | |
) => X | |
}> {} | |
export class When<R, S, A> extends TaggedClass("When")<{ | |
use: <X>( | |
go: <R1, R2, S1, A1>(params: { | |
fa: Gherkin<R1, S1, never> | |
fb: (s: S1) => T.Effect<R2, any, A1> | |
_R: (a: R1 | R2) => R | |
_S: (a: S1) => S | |
_A: (a: A1) => A | |
}) => X, | |
) => X | |
}> {} | |
export class Then<R, S, A> extends TaggedClass("Then")<{ | |
use: <X>( | |
go: <R1, R2, S1, A1>(params: { | |
fa: Gherkin<R1, S1, A1> | |
fb: (context: [a: A1, s: S1]) => T.Effect<R2, any, any> | |
_R: (a: R1 | R2) => R | |
_S: (a: S1) => S | |
_A: (a: A1) => A | |
}) => X, | |
) => X | |
}> {} | |
type Gherkin<R, S, A> = | |
| Given<R, S, A> | |
| AndGiven<R, S, A> | |
| When<R, S, A> | |
| Then<R, S, A> | |
const given = <R, S2>(fa: T.Effect<R, any, S2>): Gherkin<R, S2, never> => | |
new Given({ fa, _A: identity }) | |
const andGiven = | |
<R1, S1, R2, S2>(fb: (s: S1) => T.Effect<R2, any, S2>) => | |
(fa: Gherkin<R1, S1, never>): Gherkin<R1 | R2, S2, never> => | |
new AndGiven({ | |
use: go => | |
go({ | |
fa, | |
fb, | |
_S: identity, | |
_R: identity, | |
_A: identity, | |
}), | |
}) | |
const when = | |
<R1, R2, S, A>(fb: (s: S) => T.Effect<R2, any, A>) => | |
(fa: Gherkin<R1, S, never>): Gherkin<R1 | R2, S, A> => | |
new When({ | |
use: go => | |
go({ | |
fa, | |
fb, | |
_S: identity, | |
_R: identity, | |
_A: identity, | |
}), | |
}) | |
const then = | |
<R1, R2, S, A>(fb: (params: [A, S]) => T.Effect<R2, any, any>) => | |
(fa: Gherkin<R1, S, A>): Gherkin<R1 | R2, S, A> => | |
new Then({ | |
use: go => | |
go({ | |
fa, | |
fb, | |
_S: identity, | |
_R: identity, | |
_A: identity, | |
}), | |
}) | |
const andThen = | |
<R1, R2, S, A>(fb: (params: [A, S]) => T.Effect<R2, any, any>) => | |
(fa: Gherkin<R1, S, A>): Gherkin<R1 | R2, S, A> => | |
new Then({ | |
use: go => | |
go({ | |
fa, | |
fb, | |
_S: identity, | |
_R: identity, | |
_A: identity, | |
}), | |
}) | |
const unsafeCoerceEnv = | |
<RA, RB>(_proof: (env: RA) => RB) => | |
<E, A>(fa: T.Effect<RA, E, A>): T.Effect<RB, E, A> => | |
fa as T.Effect<any, E, A> | |
const toEffect = <R, S, A>(test: Gherkin<R, S, A>): T.Effect<R, any, A> => { | |
const loop = <R, S, A>(test: Gherkin<R, S, A>): T.Effect<R, any, [S, A]> => | |
pipe( | |
Match.value(test), | |
Match.tag("Given", _ => | |
pipe( | |
_.fa, | |
T.map(s => [s, {}] as [S, never]), | |
), | |
), | |
Match.tag("AndGiven", _ => | |
_.use(_ => | |
pipe( | |
loop(_.fa), | |
T.flatMap(([s]) => _.fb(s)), | |
T.map(s => [_._S(s), {}] as [S, never]), | |
unsafeCoerceEnv(_._R), | |
), | |
), | |
), | |
Match.tag("When", _ => | |
_.use(_ => | |
pipe( | |
loop(_.fa), | |
T.flatMap(([s]) => | |
pipe( | |
_.fb(s), | |
T.map(a => [_._S(s), _._A(a)] as [S, A]), | |
), | |
), | |
unsafeCoerceEnv(_._R), | |
), | |
), | |
), | |
Match.tag("Then", _ => | |
_.use(_ => | |
pipe( | |
loop(_.fa), | |
T.flatMap(([s, a]) => { | |
return pipe( | |
_.fb([a, s]), | |
T.map(() => [_._S(s), _._A(a)] as [S, A]), | |
) | |
}), | |
unsafeCoerceEnv(_._R), | |
), | |
), | |
), | |
Match.exhaustive, | |
) | |
return pipe( | |
loop(test), | |
T.map(([, a]) => a), | |
) | |
} | |
test(`Blah`, async () => | |
await pipe( | |
given(T.succeed(5 as const)), | |
andGiven(s => T.succeed([s, 10] as const)), | |
when(([n1, n2]) => T.succeed(n1 + n2)), | |
then(([a, c]) => | |
T.sync(() => { | |
expect(c).toEqual<typeof c>([5, 10]) | |
expect(a).toEqual(15) | |
}), | |
), | |
andThen(([a, c]) => | |
T.sync(() => { | |
expect(c).toEqual<typeof c>([5, 10]) | |
expect(a).toEqual(15) | |
}), | |
), | |
toEffect, | |
T.runPromise, | |
)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment