@playfast/reform-proof 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -12
- package/package.json +18 -18
- package/src/{assertions.ts → assert.ts} +3 -3
- package/src/engine.ts +285 -8
- package/src/engineMounted.ts +271 -0
- package/src/engineMounts.ts +165 -0
- package/src/engineRender.ts +136 -0
- package/src/engineSink.ts +195 -0
- package/src/engineTree.ts +298 -0
- package/src/engineTreeDriver.ts +247 -0
- package/src/engineTreeFacade.ts +183 -0
- package/src/{runtimeTreeTypes.ts → engineTreeTypes.ts} +26 -22
- package/src/errors.ts +19 -0
- package/src/facade.ts +71 -251
- package/src/fingerprint.test.ts +24 -0
- package/src/fingerprint.ts +73 -0
- package/src/index.ts +165 -115
- package/src/nested-feature.test.ts +174 -0
- package/src/product.ts +50 -19
- package/src/proofCase.ts +46 -0
- package/src/runner.ts +5 -1
- package/src/structure-events.test.ts +9 -3
- package/src/structure-locators.test.ts +19 -9
- package/src/test-clock.test.ts +135 -0
- package/src/typed-seams.test.ts +6 -3
- package/src/driverTypes.ts +0 -19
- package/src/execution.ts +0 -100
- package/src/runtimeFacade.ts +0 -269
- package/src/runtimeTreeDispose.ts +0 -21
- package/src/runtimeTreeDriver.ts +0 -285
- package/src/runtimeTreeFacade.ts +0 -134
- package/src/runtimeTreeSettle.ts +0 -34
- package/src/sink.ts +0 -73
- package/src/treeBindings.ts +0 -121
package/src/index.ts
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { Effect, type
|
|
2
|
-
import type { YieldWrap } from 'effect/Utils'
|
|
1
|
+
import { Effect, Layer, TestContext, type TestServices } from 'effect'
|
|
3
2
|
import { ProofRunner, type ProofRunnerApi, proofRunnerLayer, withProofRunner } from './runner'
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
3
|
+
import { isRecord } from './assert'
|
|
4
|
+
import type { ContractOf, Facade, SlotsOf } from './facade'
|
|
6
5
|
import type {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
AnyRequirementClass,
|
|
7
|
+
ProductClass,
|
|
8
|
+
ProductComposition,
|
|
9
|
+
RequirementClass,
|
|
10
|
+
} from './product'
|
|
11
|
+
import type { ProofCapture, ProofCase, ProofGenerator, SuiteResult } from './proofCase'
|
|
12
|
+
import type {
|
|
13
|
+
CapturedScene,
|
|
9
14
|
Scene,
|
|
10
|
-
|
|
11
|
-
Trigger,
|
|
15
|
+
SlotComposition as ReformSlotComposition,
|
|
12
16
|
UiContract,
|
|
13
17
|
} from '@playfast/reform'
|
|
14
|
-
|
|
15
|
-
type AnyValue = Schema.Schema.Type<Schema.Schema.Any>
|
|
16
|
-
type AnyComposition = CompositionClass<AnyValue, AnyValue>
|
|
18
|
+
import type { AnyScene, CapturedRender } from '@playfast/reform/internal'
|
|
17
19
|
|
|
18
20
|
export { ProofRunner, proofRunnerLayer, withProofRunner }
|
|
19
21
|
export type { ProofRunnerApi }
|
|
20
22
|
|
|
21
23
|
export {
|
|
22
24
|
makeFacade,
|
|
25
|
+
makeProofFacade,
|
|
23
26
|
makeRuntimeHandleFacade,
|
|
24
27
|
makeRuntimeTreeFacade,
|
|
25
28
|
makeSink,
|
|
@@ -28,72 +31,37 @@ export {
|
|
|
28
31
|
export type {
|
|
29
32
|
MountedFacade,
|
|
30
33
|
MountedSlotFacade,
|
|
31
|
-
|
|
34
|
+
MountedSlotFacadesOf,
|
|
32
35
|
RuntimeTreeFacade,
|
|
33
36
|
Sink,
|
|
34
37
|
} from './engine'
|
|
35
38
|
|
|
36
|
-
export {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
export { Product, ProductRequirement } from './product'
|
|
40
|
+
export type {
|
|
41
|
+
AnyRequirementClass,
|
|
42
|
+
ProductClass,
|
|
43
|
+
ProductComposition,
|
|
44
|
+
ProductManifest,
|
|
45
|
+
RequirementCapture,
|
|
46
|
+
RequirementClass,
|
|
47
|
+
RequirementManifest,
|
|
43
48
|
} from './product'
|
|
44
|
-
|
|
45
|
-
export {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
payload: PayloadOf<EventsOf<C>[K]>,
|
|
56
|
-
) => Effect.Effect<C['props'], never, CompositionService>
|
|
57
|
-
}
|
|
58
|
-
export type SlotFacadesOf<C extends UiContract> = {
|
|
59
|
-
readonly [K in keyof SlotsOf<C>]: SlotFacade<ContractOfSlot<SlotsOf<C>[K]>>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export type Action = (payload: unknown) => Effect.Effect<unknown, never, CompositionService>
|
|
63
|
-
|
|
64
|
-
export interface Facade<C extends UiContract> {
|
|
65
|
-
readonly props: Effect.Effect<C['props'], never, CompositionService>
|
|
66
|
-
readonly expectProps: (partial: Partial<C['props']>) => Effect.Effect<void, never, CompositionService>
|
|
67
|
-
readonly actions: ActionsOf<C>
|
|
68
|
-
readonly slots: SlotFacadesOf<C>
|
|
69
|
-
readonly frame: Effect.Effect<void, never, CompositionService>
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface SlotFacade<C extends UiContract> extends Facade<C> {
|
|
73
|
-
readonly first: Effect.Effect<Facade<C>, never, CompositionService>
|
|
74
|
-
readonly at: (index: number) => Effect.Effect<Facade<C>, never, CompositionService>
|
|
75
|
-
readonly all: Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
|
|
76
|
-
readonly byKey: (key: string) => Effect.Effect<Facade<C>, never, CompositionService>
|
|
77
|
-
readonly where: (
|
|
78
|
-
predicate: (props: C['props']) => boolean,
|
|
79
|
-
) => Effect.Effect<ReadonlyArray<Facade<C>>, never, CompositionService>
|
|
80
|
-
readonly count: Effect.Effect<number, never, CompositionService>
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
type ProofGenerator = Generator<
|
|
84
|
-
YieldWrap<Effect.Effect<unknown, unknown, CompositionService>>,
|
|
85
|
-
void,
|
|
86
|
-
unknown
|
|
87
|
-
>
|
|
88
|
-
|
|
89
|
-
type AnyFacade = Facade<UiContract>
|
|
90
|
-
|
|
91
|
-
export type ProofBody = (app: AnyFacade) => ProofGenerator
|
|
49
|
+
export { expect, matchProps } from './assert'
|
|
50
|
+
export type {
|
|
51
|
+
Action,
|
|
52
|
+
ActionsOf,
|
|
53
|
+
ContractOf,
|
|
54
|
+
EventsOf,
|
|
55
|
+
Facade,
|
|
56
|
+
SlotFacade,
|
|
57
|
+
SlotFacadesOf,
|
|
58
|
+
} from './facade'
|
|
59
|
+
export type { ProofCapture, ProofCase, ProofResult, SuiteResult } from './proofCase'
|
|
92
60
|
|
|
93
61
|
export interface Proof {
|
|
94
|
-
readonly requirement:
|
|
95
|
-
readonly scene:
|
|
96
|
-
readonly
|
|
62
|
+
readonly requirement: AnyRequirementClass
|
|
63
|
+
readonly scene: AnyScene
|
|
64
|
+
readonly capture: <Result>(visit: ProofCapture<Result>) => Result
|
|
97
65
|
}
|
|
98
66
|
|
|
99
67
|
export interface ProofSuite {
|
|
@@ -102,45 +70,78 @@ export interface ProofSuite {
|
|
|
102
70
|
readonly proofs: ReadonlyArray<Proof>
|
|
103
71
|
}
|
|
104
72
|
|
|
105
|
-
const
|
|
106
|
-
typeof candidate === 'object' && candidate !== null
|
|
107
|
-
|
|
108
|
-
export interface ProofResult {
|
|
109
|
-
readonly requirement: string
|
|
110
|
-
readonly ok: boolean
|
|
111
|
-
// oxlint-disable-next-line reform-rules/no-optional-fields -- serialized result DTO read as `error ?? …` by the vitest adapter (external); optional kept for wire compat
|
|
112
|
-
readonly error?: string
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface SuiteResult {
|
|
116
|
-
readonly product: string
|
|
117
|
-
readonly results: ReadonlyArray<ProofResult>
|
|
118
|
-
readonly ok: boolean
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const implement = <Comp extends AnyComposition>(
|
|
73
|
+
const implement = <Comp extends ProductComposition>(
|
|
122
74
|
requirement: RequirementClass<Comp, string>,
|
|
123
|
-
scene: Scene<ContractOf<Comp>>,
|
|
124
|
-
body: (app: Facade<ContractOf<Comp
|
|
125
|
-
): Proof =>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
75
|
+
scene: Scene<ContractOf<NoInfer<Comp>>, NoInfer<Comp>['States']>,
|
|
76
|
+
body: (app: Facade<ContractOf<Comp>, never>) => ProofGenerator,
|
|
77
|
+
): Proof =>
|
|
78
|
+
scene.capture(
|
|
79
|
+
<Services, P, N extends string, Identity>(
|
|
80
|
+
exactScene: CapturedScene<ContractOf<Comp>, Comp['States'], Services, P, N, Identity>,
|
|
81
|
+
): Proof => {
|
|
82
|
+
const proofCase: ProofCase<
|
|
83
|
+
Comp,
|
|
84
|
+
ContractOf<Comp>,
|
|
85
|
+
Comp['States'],
|
|
86
|
+
Services,
|
|
87
|
+
P,
|
|
88
|
+
N,
|
|
89
|
+
Identity
|
|
90
|
+
> = {
|
|
91
|
+
requirement,
|
|
92
|
+
scene: exactScene,
|
|
93
|
+
placement: 'Root',
|
|
94
|
+
body,
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
requirement,
|
|
98
|
+
scene: exactScene,
|
|
99
|
+
capture: (visit) =>
|
|
100
|
+
visit<Comp, ContractOf<Comp>, Comp['States'], Services, P, N, Identity>(proofCase),
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
type SlotsHolding<C extends UiContract, Target extends ProductComposition> = C extends {
|
|
106
|
+
readonly slots: NonNullable<UiContract['slots']>
|
|
129
107
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
108
|
+
? {
|
|
109
|
+
[K in keyof SlotsOf<C>]: [ReformSlotComposition<SlotsOf<C>[K]>] extends [Target] ? K : never
|
|
110
|
+
}[keyof SlotsOf<C>]
|
|
111
|
+
: never
|
|
112
|
+
|
|
113
|
+
type SceneHolding<
|
|
114
|
+
Comp extends ProductComposition,
|
|
115
|
+
C extends UiContract,
|
|
116
|
+
S extends ReadonlyArray<unknown>,
|
|
117
|
+
> = [SlotsHolding<C, Comp>] extends [never] ? never : Scene<C, S>
|
|
118
|
+
|
|
119
|
+
const implementVia = <
|
|
120
|
+
Comp extends ProductComposition,
|
|
121
|
+
C extends UiContract,
|
|
122
|
+
S extends ReadonlyArray<unknown>,
|
|
123
|
+
>(
|
|
136
124
|
requirement: RequirementClass<Comp, string>,
|
|
137
|
-
scene:
|
|
138
|
-
body: (app: Facade<C>) => ProofGenerator,
|
|
139
|
-
): Proof =>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
125
|
+
scene: SceneHolding<NoInfer<Comp>, C, S>,
|
|
126
|
+
body: (app: Facade<C, never>) => ProofGenerator,
|
|
127
|
+
): Proof =>
|
|
128
|
+
scene.capture(
|
|
129
|
+
<Services, P, N extends string, Identity>(
|
|
130
|
+
exactScene: CapturedScene<C, S, Services, P, N, Identity>,
|
|
131
|
+
): Proof => {
|
|
132
|
+
const proofCase: ProofCase<Comp, C, S, Services, P, N, Identity> = {
|
|
133
|
+
requirement,
|
|
134
|
+
scene: exactScene,
|
|
135
|
+
placement: 'Slot',
|
|
136
|
+
body,
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
requirement,
|
|
140
|
+
scene: exactScene,
|
|
141
|
+
capture: (visit) => visit<Comp, C, S, Services, P, N, Identity>(proofCase),
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
)
|
|
144
145
|
|
|
145
146
|
interface MakeSuiteConfig {
|
|
146
147
|
readonly proofs: ReadonlyArray<Proof>
|
|
@@ -156,28 +157,77 @@ export const isProofSuite = (candidate: unknown): candidate is ProofSuite =>
|
|
|
156
157
|
isRecord(candidate) && candidate['kind'] === 'ProofSuite' && Array.isArray(candidate['proofs'])
|
|
157
158
|
|
|
158
159
|
const run = (proofSuite: ProofSuite): Promise<SuiteResult> =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
Effect.runPromise(runEffect(proofSuite))
|
|
161
|
+
|
|
162
|
+
const runEffect = (proofSuite: ProofSuite): Effect.Effect<SuiteResult, never, never> =>
|
|
163
|
+
ProofRunner.pipe(
|
|
164
|
+
Effect.flatMap((runner) =>
|
|
165
|
+
Effect.forEach(proofSuite.proofs, (proof) => runner.executeProofEffect(proof)),
|
|
166
|
+
),
|
|
167
|
+
Effect.map((reports) => ({
|
|
164
168
|
product: proofSuite.product.manifest.name,
|
|
165
169
|
results: reports,
|
|
166
170
|
ok: reports.every((report) => report.ok),
|
|
167
|
-
}
|
|
168
|
-
|
|
171
|
+
})),
|
|
172
|
+
Effect.provide(proofRunnerLayer),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
export interface StepFrame {
|
|
176
|
+
readonly index: number
|
|
177
|
+
readonly captures: ReadonlyArray<CapturedRender>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface DriveResult {
|
|
181
|
+
readonly requirement: string
|
|
182
|
+
readonly frames: ReadonlyArray<StepFrame>
|
|
183
|
+
readonly ok: boolean
|
|
184
|
+
readonly error: string | undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface ProofDriver {
|
|
188
|
+
readonly requirement: string
|
|
189
|
+
readonly run: () => Promise<DriveResult>
|
|
190
|
+
}
|
|
169
191
|
|
|
170
|
-
export type { DriveResult, ProofDriver, StepFrame } from './driverTypes'
|
|
171
192
|
const driver = (proofSuite: ProofSuite): ReadonlyArray<ProofDriver> =>
|
|
172
193
|
proofSuite.proofs.map((proof) => ({
|
|
173
194
|
requirement: proof.requirement.manifest.statement,
|
|
174
195
|
run: () => withProofRunner((runner: ProofRunnerApi) => runner.driveProof(proof)),
|
|
175
196
|
}))
|
|
176
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Wrap a scene's layers in a virtual clock, so every `Effect.sleep`, `Schedule` and
|
|
200
|
+
* `Clock` read its procedures make elapses only when the proof says so via
|
|
201
|
+
* `app.advance(...)`:
|
|
202
|
+
*
|
|
203
|
+
* ```ts
|
|
204
|
+
* const MyScene = scene(MyComposition, { provide: [Proof.withTestClock(App)] })
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* It has to WRAP the scene's layers, never sit beside them in `provide`. The engine
|
|
208
|
+
* forks its loop while its layer is being built, and a forked fiber inherits the
|
|
209
|
+
* clock in scope at that moment — so a clock merged as a sibling reaches the proof
|
|
210
|
+
* body but not the procedures whose timers it is meant to control. That mis-wiring
|
|
211
|
+
* is silent (`app.advance(...)` finds a clock, adjusts it, and no product timer
|
|
212
|
+
* moves), which is why the clock is exposed as this combinator, not a bare layer.
|
|
213
|
+
*
|
|
214
|
+
* Opt-in per scene rather than on by default: a scene without it keeps the real
|
|
215
|
+
* clock, so proofs that observe product timers by waiting stay correct. Once a scene
|
|
216
|
+
* has it, a product timer NEVER fires on its own — an unadvanced proof sees the
|
|
217
|
+
* pre-timer state, which is the point.
|
|
218
|
+
*/
|
|
219
|
+
const withTestClock = <ROut, E, RIn>(
|
|
220
|
+
layer: Layer.Layer<ROut, E, RIn>,
|
|
221
|
+
): Layer.Layer<ROut | TestServices.TestServices, E, RIn> =>
|
|
222
|
+
layer.pipe(Layer.provideMerge(TestContext.TestContext))
|
|
223
|
+
|
|
177
224
|
export const Proof: {
|
|
178
225
|
readonly implement: typeof implement
|
|
179
226
|
readonly implementVia: typeof implementVia
|
|
180
227
|
readonly suite: typeof suite
|
|
181
228
|
readonly run: typeof run
|
|
229
|
+
readonly runEffect: typeof runEffect
|
|
182
230
|
readonly driver: typeof driver
|
|
183
|
-
|
|
231
|
+
readonly withTestClock: typeof withTestClock
|
|
232
|
+
} = { implement, implementVia, suite, run, runEffect, driver, withTestClock }
|
|
233
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { expect, it } from '@effect/vitest'
|
|
2
|
+
import { Effect, Layer, Schema as S } from 'effect'
|
|
3
|
+
import {
|
|
4
|
+
Composition,
|
|
5
|
+
Engine,
|
|
6
|
+
Feature,
|
|
7
|
+
type FeatureSlotTarget,
|
|
8
|
+
featureModule,
|
|
9
|
+
mount,
|
|
10
|
+
one,
|
|
11
|
+
provide,
|
|
12
|
+
scene,
|
|
13
|
+
slot,
|
|
14
|
+
State,
|
|
15
|
+
ui,
|
|
16
|
+
} from '@playfast/reform'
|
|
17
|
+
import { FeatureInput, featureModuleFromInput } from '@playfast/reform/internal'
|
|
18
|
+
import { Product, Proof, ProductRequirement } from './index'
|
|
19
|
+
|
|
20
|
+
const NestedFeatureInput = S.Struct({ value: S.Number })
|
|
21
|
+
|
|
22
|
+
class NestedFeatureValue extends State.make('proof.nestedFeature.value', S.Number) {}
|
|
23
|
+
|
|
24
|
+
class NestedUi extends ui('ProofNestedFeature')<{
|
|
25
|
+
props: { mountValue: number; featureValue: number }
|
|
26
|
+
}>() {}
|
|
27
|
+
|
|
28
|
+
class Nested extends Composition.make('ProofNestedFeature', {
|
|
29
|
+
title: 'Nested feature',
|
|
30
|
+
props: NestedFeatureInput,
|
|
31
|
+
states: [NestedFeatureValue],
|
|
32
|
+
ui: NestedUi,
|
|
33
|
+
})<Nested>() {}
|
|
34
|
+
|
|
35
|
+
const NestedLive = Composition.live(Nested, function* () {
|
|
36
|
+
const props = yield* Nested.props
|
|
37
|
+
const featureValue = yield* NestedFeatureValue
|
|
38
|
+
return mount({
|
|
39
|
+
props: { mountValue: props.value, featureValue },
|
|
40
|
+
slots: {},
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const NestedModule = featureModuleFromInput(
|
|
45
|
+
NestedFeatureInput,
|
|
46
|
+
[],
|
|
47
|
+
NestedLive.pipe(
|
|
48
|
+
Layer.provideMerge(
|
|
49
|
+
Layer.unwrapEffect(
|
|
50
|
+
Effect.map(FeatureInput, (input) =>
|
|
51
|
+
State.live(NestedFeatureValue, S.decodeUnknownSync(NestedFeatureInput)(input).value),
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
class PlaceholderUi extends ui('ProofNestedFeaturePlaceholder')<{
|
|
59
|
+
props: { phase: string }
|
|
60
|
+
}>() {}
|
|
61
|
+
|
|
62
|
+
class Placeholder extends Composition.make('ProofNestedFeaturePlaceholder', {
|
|
63
|
+
title: 'Nested feature placeholder',
|
|
64
|
+
ui: PlaceholderUi,
|
|
65
|
+
})<Placeholder>() {}
|
|
66
|
+
|
|
67
|
+
const PlaceholderLive = Composition.live(Placeholder, function* () {
|
|
68
|
+
return mount({ props: { phase: 'loading' }, slots: {} })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
class NestedFeature extends Feature.make('proof.nestedFeature', {
|
|
72
|
+
loadingStrategy: 'lazy',
|
|
73
|
+
input: NestedFeatureInput,
|
|
74
|
+
composition: Nested,
|
|
75
|
+
load: Effect.succeed(NestedModule),
|
|
76
|
+
placeholder: { loading: Placeholder, failed: Placeholder },
|
|
77
|
+
}) {}
|
|
78
|
+
|
|
79
|
+
class NestedSlot extends slot('ProofNestedFeatureSlot')<
|
|
80
|
+
NestedSlot,
|
|
81
|
+
FeatureSlotTarget<typeof NestedFeature>
|
|
82
|
+
>() {}
|
|
83
|
+
|
|
84
|
+
class RootUi extends ui('ProofNestedFeatureRoot')<{
|
|
85
|
+
props: { ready: boolean }
|
|
86
|
+
slots: { Nested: NestedSlot }
|
|
87
|
+
}>() {}
|
|
88
|
+
|
|
89
|
+
class Root extends Composition.make('ProofNestedFeatureRoot', {
|
|
90
|
+
title: 'Nested feature root',
|
|
91
|
+
slots: { Nested: NestedSlot },
|
|
92
|
+
ui: RootUi,
|
|
93
|
+
})<Root>() {}
|
|
94
|
+
|
|
95
|
+
const RootLive = Composition.live(Root, function* () {
|
|
96
|
+
return mount({
|
|
97
|
+
props: { ready: true },
|
|
98
|
+
slots: { Nested: one({ value: 42 }) },
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const RootModule = featureModule(
|
|
103
|
+
[],
|
|
104
|
+
Layer.mergeAll(RootLive, PlaceholderLive, provide(NestedSlot, NestedFeature)),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
class RootFeature extends Feature.make('proof.rootFeature', {
|
|
108
|
+
composition: Root,
|
|
109
|
+
module: RootModule,
|
|
110
|
+
}) {}
|
|
111
|
+
|
|
112
|
+
class HarnessRootSlot extends slot('ProofHarnessRootSlot')<
|
|
113
|
+
HarnessRootSlot,
|
|
114
|
+
FeatureSlotTarget<typeof RootFeature>
|
|
115
|
+
>() {}
|
|
116
|
+
|
|
117
|
+
class HarnessUi extends ui('ProofNestedFeatureHarness')<{
|
|
118
|
+
props: { ready: boolean }
|
|
119
|
+
slots: { Root: HarnessRootSlot }
|
|
120
|
+
}>() {}
|
|
121
|
+
|
|
122
|
+
class Harness extends Composition.make('ProofNestedFeatureHarness', {
|
|
123
|
+
title: 'Nested feature harness',
|
|
124
|
+
slots: { Root: HarnessRootSlot },
|
|
125
|
+
ui: HarnessUi,
|
|
126
|
+
})<Harness>() {}
|
|
127
|
+
|
|
128
|
+
const HarnessLive = Composition.live(Harness, function* () {
|
|
129
|
+
return mount({
|
|
130
|
+
props: { ready: true },
|
|
131
|
+
slots: { Root: one({}) },
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const RootScene = scene(Harness, {
|
|
136
|
+
provide: [
|
|
137
|
+
Layer.mergeAll(HarnessLive, provide(HarnessRootSlot, RootFeature)).pipe(
|
|
138
|
+
Layer.provideMerge(Engine),
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
class MountsNestedFeature extends ProductRequirement.make(
|
|
144
|
+
Root,
|
|
145
|
+
'mounts a nested native Feature with its slot input',
|
|
146
|
+
) {}
|
|
147
|
+
|
|
148
|
+
class RootProduct extends Product.make(Root, {
|
|
149
|
+
requirements: [MountsNestedFeature],
|
|
150
|
+
}) {}
|
|
151
|
+
|
|
152
|
+
const mountsNestedFeature = Proof.implementVia(MountsNestedFeature, RootScene, function* (app) {
|
|
153
|
+
const root = yield* app.slots.Root.first
|
|
154
|
+
const nested = yield* root.slots.Nested.first
|
|
155
|
+
yield* nested.expectProps({ mountValue: 42, featureValue: 42 })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const NestedFeatureProofs = Proof.suite(RootProduct, {
|
|
159
|
+
proofs: [mountsNestedFeature],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it.live('proves a Feature that mounts a nested native Feature', () =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const result = yield* Proof.runEffect(NestedFeatureProofs)
|
|
165
|
+
expect(result.results).toEqual([
|
|
166
|
+
{
|
|
167
|
+
requirement: 'mounts a nested native Feature with its slot input',
|
|
168
|
+
ok: true,
|
|
169
|
+
error: undefined,
|
|
170
|
+
},
|
|
171
|
+
])
|
|
172
|
+
expect(result.ok).toBe(true)
|
|
173
|
+
}),
|
|
174
|
+
)
|
package/src/product.ts
CHANGED
|
@@ -1,40 +1,65 @@
|
|
|
1
|
-
import { Option
|
|
2
|
-
import type {
|
|
3
|
-
import type { ContractOf,
|
|
1
|
+
import { Option } from 'effect'
|
|
2
|
+
import type { AnyComposition } from '@playfast/reform/internal'
|
|
3
|
+
import type { ContractOf, EventNamesOf } from './facade'
|
|
4
4
|
|
|
5
|
-
type
|
|
6
|
-
type AnyComposition = CompositionClass<AnyValue, AnyValue>
|
|
5
|
+
export type ProductComposition = AnyComposition & { readonly identity: symbol }
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
const RequirementCompositionTypeId: unique symbol = Symbol.for(
|
|
8
|
+
'reform-proof/RequirementComposition',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export interface AnyRequirementClass {
|
|
12
|
+
new (): {}
|
|
13
|
+
readonly manifest: {
|
|
14
|
+
readonly kind: 'ProductRequirement'
|
|
15
|
+
readonly name: string
|
|
16
|
+
readonly statement: string
|
|
17
|
+
readonly composition: ProductComposition
|
|
18
|
+
readonly events: Option.Option<ReadonlyArray<PropertyKey>>
|
|
19
|
+
}
|
|
20
|
+
readonly capture: <Result>(visit: RequirementCapture<Result>) => Result
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type RequirementCapture<Result> = <
|
|
24
|
+
Comp extends ProductComposition,
|
|
25
|
+
Statement extends string,
|
|
26
|
+
>(
|
|
27
|
+
requirement: RequirementClass<Comp, Statement>,
|
|
28
|
+
) => Result
|
|
29
|
+
|
|
30
|
+
export interface RequirementManifest<Comp extends ProductComposition, Statement extends string> {
|
|
9
31
|
readonly kind: 'ProductRequirement'
|
|
10
32
|
readonly name: Statement
|
|
11
33
|
readonly statement: Statement
|
|
12
34
|
readonly composition: Comp
|
|
13
|
-
readonly events: Option.Option<ReadonlyArray<
|
|
35
|
+
readonly events: Option.Option<ReadonlyArray<EventNamesOf<ContractOf<Comp>>>>
|
|
14
36
|
}
|
|
15
37
|
|
|
16
|
-
export interface RequirementClass<
|
|
17
|
-
|
|
38
|
+
export interface RequirementClass<
|
|
39
|
+
Comp extends ProductComposition,
|
|
40
|
+
Statement extends string,
|
|
41
|
+
> extends AnyRequirementClass {
|
|
18
42
|
readonly manifest: RequirementManifest<Comp, Statement>
|
|
43
|
+
readonly [RequirementCompositionTypeId]: (composition: Comp) => Comp
|
|
19
44
|
}
|
|
20
45
|
|
|
21
|
-
export interface ProductManifest<Comp extends
|
|
46
|
+
export interface ProductManifest<Comp extends ProductComposition> {
|
|
22
47
|
readonly kind: 'Product'
|
|
23
48
|
readonly name: string
|
|
24
49
|
readonly composition: Comp
|
|
25
|
-
readonly requirements: ReadonlyArray<
|
|
50
|
+
readonly requirements: ReadonlyArray<AnyRequirementClass>
|
|
26
51
|
}
|
|
27
52
|
|
|
28
|
-
export interface ProductClass<Comp extends
|
|
53
|
+
export interface ProductClass<Comp extends ProductComposition = ProductComposition> {
|
|
29
54
|
new (): {}
|
|
30
55
|
readonly manifest: ProductManifest<Comp>
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
interface MakeRequirementOptionsExternalApi<Comp extends
|
|
34
|
-
readonly events?: ReadonlyArray<
|
|
58
|
+
interface MakeRequirementOptionsExternalApi<Comp extends ProductComposition> {
|
|
59
|
+
readonly events?: ReadonlyArray<EventNamesOf<ContractOf<Comp>>>
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
const makeRequirement = <Comp extends
|
|
62
|
+
const makeRequirement = <Comp extends ProductComposition, const Statement extends string>(
|
|
38
63
|
composition: Comp,
|
|
39
64
|
statement: Statement,
|
|
40
65
|
options?: MakeRequirementOptionsExternalApi<Comp>,
|
|
@@ -46,20 +71,24 @@ const makeRequirement = <Comp extends AnyComposition, const Statement extends st
|
|
|
46
71
|
composition,
|
|
47
72
|
events: Option.fromNullable(options?.events),
|
|
48
73
|
}
|
|
49
|
-
|
|
74
|
+
class Requirement {
|
|
50
75
|
static readonly manifest = manifest
|
|
76
|
+
static readonly [RequirementCompositionTypeId] = (exact: Comp): Comp => exact
|
|
77
|
+
static readonly capture = <Result>(visit: RequirementCapture<Result>): Result =>
|
|
78
|
+
visit(Requirement)
|
|
51
79
|
}
|
|
80
|
+
return Requirement
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
export const ProductRequirement: { readonly make: typeof makeRequirement } = {
|
|
55
84
|
make: makeRequirement,
|
|
56
85
|
}
|
|
57
86
|
|
|
58
|
-
interface MakeProductConfig<Comp extends
|
|
87
|
+
interface MakeProductConfig<Comp extends ProductComposition> {
|
|
59
88
|
readonly requirements: ReadonlyArray<RequirementClass<Comp, string>>
|
|
60
89
|
}
|
|
61
90
|
|
|
62
|
-
const makeProduct = <Comp extends
|
|
91
|
+
const makeProduct = <Comp extends ProductComposition>(
|
|
63
92
|
composition: Comp,
|
|
64
93
|
config: MakeProductConfig<Comp>,
|
|
65
94
|
): ProductClass<Comp> => {
|
|
@@ -74,4 +103,6 @@ const makeProduct = <Comp extends AnyComposition>(
|
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
105
|
|
|
77
|
-
export const Product: { readonly make: typeof makeProduct } = {
|
|
106
|
+
export const Product: { readonly make: typeof makeProduct } = {
|
|
107
|
+
make: makeProduct,
|
|
108
|
+
}
|
package/src/proofCase.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Effect } from 'effect'
|
|
2
|
+
import type { YieldWrap } from 'effect/Utils'
|
|
3
|
+
import type { CapturedScene, UiContract } from '@playfast/reform'
|
|
4
|
+
import type { Facade } from './facade'
|
|
5
|
+
import type { ProductComposition, RequirementClass } from './product'
|
|
6
|
+
|
|
7
|
+
export type ProofGenerator = Generator<YieldWrap<Effect.Effect<unknown, unknown, never>>, void, unknown>
|
|
8
|
+
|
|
9
|
+
export interface ProofCase<
|
|
10
|
+
Comp extends ProductComposition,
|
|
11
|
+
C extends UiContract,
|
|
12
|
+
S extends ReadonlyArray<unknown>,
|
|
13
|
+
Services,
|
|
14
|
+
P,
|
|
15
|
+
N extends string,
|
|
16
|
+
Identity,
|
|
17
|
+
> {
|
|
18
|
+
readonly requirement: RequirementClass<Comp, string>
|
|
19
|
+
readonly scene: CapturedScene<C, S, Services, P, N, Identity>
|
|
20
|
+
readonly placement: 'Root' | 'Slot'
|
|
21
|
+
readonly body: (app: Facade<C, never>) => ProofGenerator
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ProofCapture<Result> = <
|
|
25
|
+
Comp extends ProductComposition,
|
|
26
|
+
C extends UiContract,
|
|
27
|
+
S extends ReadonlyArray<unknown>,
|
|
28
|
+
Services,
|
|
29
|
+
P,
|
|
30
|
+
N extends string,
|
|
31
|
+
Identity,
|
|
32
|
+
>(
|
|
33
|
+
proof: ProofCase<Comp, C, S, Services, P, N, Identity>,
|
|
34
|
+
) => Result
|
|
35
|
+
|
|
36
|
+
export interface ProofResult {
|
|
37
|
+
readonly requirement: string
|
|
38
|
+
readonly ok: boolean
|
|
39
|
+
readonly error: string | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SuiteResult {
|
|
43
|
+
readonly product: string
|
|
44
|
+
readonly results: ReadonlyArray<ProofResult>
|
|
45
|
+
readonly ok: boolean
|
|
46
|
+
}
|