@matheuspuel/state-machine 0.2.0 → 0.2.2

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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@matheuspuel/state-machine",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "module": "index.ts",
6
6
  "exports": {
7
7
  "./package.json": "./package.json",
8
8
  ".": "./src/index.ts",
9
9
  "./definition": "./src/definition.ts",
10
+ "./form": "./src/form/index.ts",
10
11
  "./machines": "./src/machines/index.ts",
11
12
  "./react": "./src/react/index.ts",
12
13
  "./runtime": "./src/runtime/index.ts"
@@ -0,0 +1,33 @@
1
+ import { Effect, Schema } from 'effect'
2
+ import { ParseError } from 'effect/ParseResult'
3
+
4
+ export type AnyForm = {
5
+ [key: string]: AnyForm | FormField<any, any, any>
6
+ }
7
+
8
+ export type FormField<A, I, E> = {
9
+ initial: I
10
+ validate: (value: I) => Effect.Effect<A, E>
11
+ fromData: (data: A) => I
12
+ }
13
+
14
+ export const field = <A, I, E>(args: FormField<A, I, E>) => args
15
+
16
+ export const fieldSchema = <A, I>(args: {
17
+ initial: I
18
+ schema: Schema.Schema<A, I>
19
+ }) =>
20
+ field<A, I, ParseError>({
21
+ initial: args.initial,
22
+ validate: Schema.decode(args.schema),
23
+ fromData: Schema.encodeSync(args.schema),
24
+ })
25
+
26
+ export const fieldSuccess = <A>(args: { initial: A }) =>
27
+ field<A, A, never>({
28
+ initial: args.initial,
29
+ validate: Effect.succeed,
30
+ fromData: _ => _,
31
+ })
32
+
33
+ export const Struct = <Fields extends AnyForm>(fields: Fields) => fields
@@ -0,0 +1 @@
1
+ export * as Form from './definition'
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from '@effect/vitest'
2
+ import { Effect } from 'effect'
3
+ import { StateMachine } from '..'
4
+ import { Form } from '../form'
5
+ import { run } from '../runtime'
6
+
7
+ describe('Form', () => {
8
+ it('should work', () => {
9
+ const formField = Form.field({
10
+ initial: 0,
11
+ validate: _ =>
12
+ _ > 1 ? Effect.succeed({ n: _ }) : Effect.fail('low' as const),
13
+ fromData: (data: { n: number }) => data.n,
14
+ })
15
+ const form = Form.Struct({
16
+ a: formField,
17
+ b: Form.Struct({ c: formField }),
18
+ })
19
+ const machine = StateMachine.Form(form)
20
+ const instance = run(machine)
21
+ const getState = () => instance.ref.get.pipe(Effect.runSync)
22
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
23
+ a: { value: 0, error: null },
24
+ b: { c: { value: 0, error: null } },
25
+ })
26
+ instance.actions.a.set(1)
27
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
28
+ a: { value: 1, error: null },
29
+ b: { c: { value: 0, error: null } },
30
+ })
31
+ instance.actions.validate().pipe(Effect.runSyncExit)
32
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
33
+ a: { value: 1, error: 'low' },
34
+ b: { c: { value: 0, error: 'low' } },
35
+ })
36
+ instance.actions.a.update(_ => _ + 1)
37
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
38
+ a: { value: 2, error: null },
39
+ b: { c: { value: 0, error: 'low' } },
40
+ })
41
+ instance.actions.b.c.set(1)
42
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
43
+ a: { value: 2, error: null },
44
+ b: { c: { value: 1, error: null } },
45
+ })
46
+ instance.actions.validate().pipe(Effect.runSyncExit)
47
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
48
+ a: { value: 2, error: null },
49
+ b: { c: { value: 1, error: 'low' } },
50
+ })
51
+ instance.actions.b.c.update(_ => _ + 1)
52
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
53
+ a: { value: 2, error: null },
54
+ b: { c: { value: 2, error: null } },
55
+ })
56
+ const data = instance.actions.validate().pipe(Effect.runSync)
57
+ expect(data).toStrictEqual<typeof data>({ a: { n: 2 }, b: { c: { n: 2 } } })
58
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
59
+ a: { value: 2, error: null },
60
+ b: { c: { value: 2, error: null } },
61
+ })
62
+ instance.actions.setStateFromData({ a: { n: 3 }, b: { c: { n: 3 } } })
63
+ expect(getState()).toStrictEqual<ReturnType<typeof getState>>({
64
+ a: { value: 3, error: null },
65
+ b: { c: { value: 3, error: null } },
66
+ })
67
+ })
68
+ })
@@ -0,0 +1,132 @@
1
+ import { Effect, Either, Option, Record } from 'effect'
2
+ import { make, makeStore, Store } from '../definition'
3
+ import { AnyForm, FormField } from '../form/definition'
4
+
5
+ export type FormState<Form extends AnyForm> = {
6
+ [K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
7
+ ? { value: I; error: E | null }
8
+ : Form[K] extends AnyForm
9
+ ? FormState<Form[K]>
10
+ : never
11
+ }
12
+
13
+ export type FormData<Form extends AnyForm> = {
14
+ [K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
15
+ ? A
16
+ : Form[K] extends AnyForm
17
+ ? FormData<Form[K]>
18
+ : never
19
+ }
20
+
21
+ export type FormError<Form extends AnyForm> = {
22
+ [K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
23
+ ? Option.Option<E>
24
+ : Form[K] extends AnyForm
25
+ ? FormError<Form[K]>
26
+ : never
27
+ }
28
+
29
+ export type FormActions<Form extends AnyForm> = {
30
+ [K in keyof Form]: Form[K] extends FormField<infer A, infer I, infer E>
31
+ ? {
32
+ set: (value: I) => void
33
+ update: (f: (previous: I) => I) => void
34
+ error: { set: (error: E | null) => void }
35
+ }
36
+ : Form[K] extends AnyForm
37
+ ? FormActions<Form[K]>
38
+ : never
39
+ }
40
+
41
+ const isField = (
42
+ value: AnyForm | FormField<any, any, any>,
43
+ ): value is FormField<any, any, any> => typeof value.validate === 'function'
44
+
45
+ export const Form = <F extends AnyForm>(form: F) => {
46
+ const getInitialState = <F extends AnyForm>(form: F): FormState<F> =>
47
+ Record.map(form, _ =>
48
+ isField(_) ? { value: _.initial, error: null } : getInitialState(_),
49
+ ) as any
50
+ const getActions = <F extends AnyForm>(
51
+ form: F,
52
+ Store: Store<FormState<F>>,
53
+ ): FormActions<F> =>
54
+ Record.map(form, (_, key) =>
55
+ isField(_)
56
+ ? {
57
+ update: (f: (previous: any) => any) =>
58
+ Store.update(_ => ({
59
+ ..._,
60
+ [key]: { value: f(_[key]!.value), error: null },
61
+ })),
62
+ set: (value: any) =>
63
+ Store.update(_ => ({ ..._, [key]: { value, error: null } })),
64
+ error: {
65
+ set: (error: any) =>
66
+ Store.update(_ => ({ ..._, [key]: { value: _.value, error } })),
67
+ },
68
+ }
69
+ : getActions(
70
+ _,
71
+ makeStore({
72
+ get: () => Store.get()[key],
73
+ update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
74
+ }) as any,
75
+ ),
76
+ ) as any
77
+ const validate = <F extends AnyForm>(
78
+ form: F,
79
+ Store: Store<FormState<F>>,
80
+ ): any =>
81
+ Effect.all(
82
+ Record.map(form, (_, key) =>
83
+ isField(_)
84
+ ? _.validate(Store.get()[key]!.value).pipe(
85
+ Effect.either,
86
+ Effect.tap(e =>
87
+ Store.update(_ => ({
88
+ ..._,
89
+ [key]: {
90
+ value: _[key]!.value,
91
+ error: Option.getOrNull(Either.getLeft(e)),
92
+ },
93
+ })),
94
+ ),
95
+ Effect.flatten,
96
+ )
97
+ : validate(
98
+ _,
99
+ makeStore({
100
+ get: () => Store.get()[key],
101
+ update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
102
+ }) as any,
103
+ ),
104
+ ),
105
+ { mode: 'validate' },
106
+ ) as any
107
+ return make<FormState<F>>()<
108
+ FormActions<F> & {
109
+ validate: () => Effect.Effect<FormData<F>, FormError<F>>
110
+ setStateFromData: (data: FormData<F>) => void
111
+ }
112
+ >({
113
+ initialState: getInitialState(form),
114
+ actions: ({ Store }) => ({
115
+ ...getActions(form, Store),
116
+ validate: () => validate(form, Store),
117
+ setStateFromData: (data: FormData<F>) =>
118
+ Store.update(() => {
119
+ const updateState = <F extends AnyForm>(
120
+ form: F,
121
+ data: FormData<F>,
122
+ ): any =>
123
+ Record.map(form, (_, key) =>
124
+ isField(_)
125
+ ? { value: _.fromData(data[key]), error: null }
126
+ : updateState(_, data[key]!),
127
+ )
128
+ return updateState(form, data)
129
+ }),
130
+ }),
131
+ })
132
+ }
@@ -1,3 +1,4 @@
1
+ export { Form } from './Form'
1
2
  export { FormValue } from './FormValue'
2
3
  export { of } from './of'
3
4
  export { Struct } from './Struct'