effect-machine 0.2.4 → 0.3.0

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/src/schema.ts CHANGED
@@ -74,11 +74,30 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends n
74
74
  * Constructor functions for each variant.
75
75
  * Empty structs: plain values with `_tag`: `State.Idle`
76
76
  * Non-empty structs require args: `State.Loading({ url })`
77
+ *
78
+ * Each variant also has a `derive` method for constructing from a source object.
79
+ */
80
+ /**
81
+ * Constructor functions for each variant.
82
+ * Empty structs: plain values with `_tag`: `State.Idle`
83
+ * Non-empty structs require args: `State.Loading({ url })`
84
+ *
85
+ * Each variant also has a `derive` method for constructing from a source object.
86
+ * The source type uses `object` to accept branded state types without index signature issues.
77
87
  */
78
88
  type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = {
79
89
  readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true
80
- ? TaggedStructType<K, D[K]> & Brand
81
- : (args: Schema.Struct.Constructor<D[K]>) => TaggedStructType<K, D[K]> & Brand;
90
+ ? TaggedStructType<K, D[K]> &
91
+ Brand & {
92
+ readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
93
+ }
94
+ : ((args: Schema.Struct.Constructor<D[K]>) => TaggedStructType<K, D[K]> & Brand) & {
95
+ readonly derive: (
96
+ source: object,
97
+ partial?: Partial<Schema.Struct.Constructor<D[K]>>,
98
+ ) => TaggedStructType<K, D[K]> & Brand;
99
+ readonly _tag: K;
100
+ };
82
101
  };
83
102
 
84
103
  /**
@@ -92,6 +111,11 @@ type MatchCases<D extends Record<string, Schema.Struct.Fields>, R> = {
92
111
  * Base schema interface with pattern matching helpers
93
112
  */
94
113
  interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Brand> {
114
+ /**
115
+ * Raw definition record for introspection
116
+ */
117
+ readonly _definition: D;
118
+
95
119
  /**
96
120
  * Per-variant schemas for fine-grained operations
97
121
  */
@@ -164,6 +188,7 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
164
188
  schema: Schema.Schema<VariantsUnion<D>, VariantsUnion<D>, never>;
165
189
  variants: VariantSchemas<D>;
166
190
  constructors: Record<string, (args: Record<string, unknown>) => Record<string, unknown>>;
191
+ _definition: D;
167
192
  $is: <Tag extends string>(tag: Tag) => (u: unknown) => boolean;
168
193
  $match: (valueOrCases: unknown, maybeCases?: unknown) => unknown;
169
194
  } => {
@@ -184,16 +209,29 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
184
209
  // Create constructor that builds tagged struct directly
185
210
  // Like Data.taggedEnum, this doesn't validate at construction time
186
211
  // Use Schema.decode for validation when needed
187
- const hasFields = Object.keys(fields).length > 0;
212
+ const fieldNames = new Set(Object.keys(fields));
213
+ const hasFields = fieldNames.size > 0;
188
214
 
189
215
  if (hasFields) {
190
216
  // Non-empty: constructor function requiring args
191
217
  const constructor = (args: Record<string, unknown>) => ({ ...args, _tag: tag });
192
218
  constructor._tag = tag;
219
+ constructor.derive = (source: Record<string, unknown>, partial?: Record<string, unknown>) => {
220
+ const result: Record<string, unknown> = { _tag: tag };
221
+ for (const key of fieldNames) {
222
+ if (key in source) result[key] = source[key];
223
+ }
224
+ if (partial !== undefined) {
225
+ for (const [key, value] of Object.entries(partial)) {
226
+ result[key] = value;
227
+ }
228
+ }
229
+ return result;
230
+ };
193
231
  constructors[tag] = constructor;
194
232
  } else {
195
233
  // Empty: plain value, not callable
196
- constructors[tag] = { _tag: tag } as never;
234
+ constructors[tag] = { _tag: tag, derive: () => ({ _tag: tag }) } as never;
197
235
  }
198
236
  }
199
237
 
@@ -244,6 +282,7 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
244
282
  schema: unionSchema as unknown as Schema.Schema<VariantsUnion<D>, VariantsUnion<D>, never>,
245
283
  variants: variants as unknown as VariantSchemas<D>,
246
284
  constructors,
285
+ _definition: definition,
247
286
  $is,
248
287
  $match,
249
288
  };
@@ -254,8 +293,15 @@ const buildMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(
254
293
  * Builds the schema object with variants, constructors, $is, and $match.
255
294
  */
256
295
  const createMachineSchema = <D extends Record<string, Schema.Struct.Fields>>(definition: D) => {
257
- const { schema, variants, constructors, $is, $match } = buildMachineSchema(definition);
258
- return Object.assign(Object.create(schema), { variants, $is, $match, ...constructors });
296
+ const { schema, variants, constructors, _definition, $is, $match } =
297
+ buildMachineSchema(definition);
298
+ return Object.assign(Object.create(schema), {
299
+ variants,
300
+ _definition,
301
+ $is,
302
+ $match,
303
+ ...constructors,
304
+ });
259
305
  };
260
306
 
261
307
  /**
package/src/testing.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { Effect, SubscriptionRef } from "effect";
2
2
 
3
3
  import type { Machine, MachineRef } from "./machine.js";
4
+ import { BuiltMachine } from "./machine.js";
4
5
  import { AssertionError } from "./errors.js";
5
6
  import type { GuardsDef, EffectsDef } from "./slot.js";
6
7
  import { executeTransition } from "./internal/transition.js";
7
8
 
9
+ /** Accept either Machine or BuiltMachine for testing utilities. */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> =
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
14
+
8
15
  /**
9
16
  * Result of simulating events through a machine
10
17
  */
@@ -39,11 +46,17 @@ export const simulate = Effect.fn("effect-machine.simulate")(function* <
39
46
  R,
40
47
  GD extends GuardsDef = Record<string, never>,
41
48
  EFD extends EffectsDef = Record<string, never>,
42
- >(
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
44
- machine: Machine<S, E, R, any, any, GD, EFD>,
45
- events: ReadonlyArray<E>,
46
- ) {
49
+ >(input: MachineInput<S, E, R, GD, EFD>, events: ReadonlyArray<E>) {
50
+ const machine = (input instanceof BuiltMachine ? input._inner : input) as Machine<
51
+ S,
52
+ E,
53
+ R,
54
+ Record<string, never>,
55
+ Record<string, never>,
56
+ GD,
57
+ EFD
58
+ >;
59
+
47
60
  // Create a dummy self for slot accessors
48
61
  const dummySelf: MachineRef<E> = {
49
62
  send: Effect.fn("effect-machine.testing.simulate.send")((_event: E) => Effect.void),
@@ -83,13 +96,8 @@ export const assertReaches = Effect.fn("effect-machine.assertReaches")(function*
83
96
  R,
84
97
  GD extends GuardsDef = Record<string, never>,
85
98
  EFD extends EffectsDef = Record<string, never>,
86
- >(
87
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
88
- machine: Machine<S, E, R, any, any, GD, EFD>,
89
- events: ReadonlyArray<E>,
90
- expectedTag: string,
91
- ) {
92
- const result = yield* simulate(machine, events);
99
+ >(input: MachineInput<S, E, R, GD, EFD>, events: ReadonlyArray<E>, expectedTag: string) {
100
+ const result = yield* simulate(input, events);
93
101
  if (result.finalState._tag !== expectedTag) {
94
102
  return yield* new AssertionError({
95
103
  message:
@@ -119,12 +127,11 @@ export const assertPath = Effect.fn("effect-machine.assertPath")(function* <
119
127
  GD extends GuardsDef = Record<string, never>,
120
128
  EFD extends EffectsDef = Record<string, never>,
121
129
  >(
122
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
123
- machine: Machine<S, E, R, any, any, GD, EFD>,
130
+ input: MachineInput<S, E, R, GD, EFD>,
124
131
  events: ReadonlyArray<E>,
125
132
  expectedPath: ReadonlyArray<string>,
126
133
  ) {
127
- const result = yield* simulate(machine, events);
134
+ const result = yield* simulate(input, events);
128
135
  const actualPath = result.states.map((s) => s._tag);
129
136
 
130
137
  if (actualPath.length !== expectedPath.length) {
@@ -169,13 +176,8 @@ export const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")
169
176
  R,
170
177
  GD extends GuardsDef = Record<string, never>,
171
178
  EFD extends EffectsDef = Record<string, never>,
172
- >(
173
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
174
- machine: Machine<S, E, R, any, any, GD, EFD>,
175
- events: ReadonlyArray<E>,
176
- forbiddenTag: string,
177
- ) {
178
- const result = yield* simulate(machine, events);
179
+ >(input: MachineInput<S, E, R, GD, EFD>, events: ReadonlyArray<E>, forbiddenTag: string) {
180
+ const result = yield* simulate(input, events);
179
181
 
180
182
  const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
181
183
  if (visitedIndex !== -1) {
@@ -236,11 +238,17 @@ export const createTestHarness = Effect.fn("effect-machine.createTestHarness")(f
236
238
  R,
237
239
  GD extends GuardsDef = Record<string, never>,
238
240
  EFD extends EffectsDef = Record<string, never>,
239
- >(
240
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Schema fields need wide acceptance
241
- machine: Machine<S, E, R, any, any, GD, EFD>,
242
- options?: TestHarnessOptions<S, E>,
243
- ) {
241
+ >(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E>) {
242
+ const machine = (input instanceof BuiltMachine ? input._inner : input) as Machine<
243
+ S,
244
+ E,
245
+ R,
246
+ Record<string, never>,
247
+ Record<string, never>,
248
+ GD,
249
+ EFD
250
+ >;
251
+
244
252
  // Create a dummy self for slot accessors
245
253
  const dummySelf: MachineRef<E> = {
246
254
  send: Effect.fn("effect-machine.testing.harness.send")((_event: E) => Effect.void),