@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 +2 -1
- package/src/form/definition.ts +33 -0
- package/src/form/index.ts +1 -0
- package/src/machines/Form.test.ts +68 -0
- package/src/machines/Form.ts +132 -0
- package/src/machines/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matheuspuel/state-machine",
|
|
3
|
-
"version": "0.2.
|
|
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
|
+
}
|
package/src/machines/index.ts
CHANGED