@matheuspuel/state-machine 0.5.0 → 0.7.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.
Files changed (62) hide show
  1. package/dist/definition.js +5 -2
  2. package/dist/definition.js.map +1 -1
  3. package/dist/dts/definition.d.ts +4 -1
  4. package/dist/dts/definition.d.ts.map +1 -1
  5. package/dist/dts/machines/Form/Error.d.ts +9 -0
  6. package/dist/dts/machines/Form/Error.d.ts.map +1 -0
  7. package/dist/dts/machines/Form/Field/index.d.ts +56 -0
  8. package/dist/dts/machines/Form/Field/index.d.ts.map +1 -0
  9. package/dist/dts/machines/{Form.d.ts → Form/index.d.ts} +6 -42
  10. package/dist/dts/machines/Form/index.d.ts.map +1 -0
  11. package/dist/dts/machines/QueryState.d.ts +39 -0
  12. package/dist/dts/machines/QueryState.d.ts.map +1 -0
  13. package/dist/dts/machines/basic.d.ts +8 -0
  14. package/dist/dts/machines/basic.d.ts.map +1 -0
  15. package/dist/dts/machines/index copy.d.ts +10 -0
  16. package/dist/dts/machines/index copy.d.ts.map +1 -0
  17. package/dist/dts/machines/index.d.ts +3 -3
  18. package/dist/dts/machines/index.d.ts.map +1 -1
  19. package/dist/dts/machines/index.test copy.d.ts +2 -0
  20. package/dist/dts/machines/index.test copy.d.ts.map +1 -0
  21. package/dist/machines/Form/Error.js +4 -0
  22. package/dist/machines/Form/Error.js.map +1 -0
  23. package/dist/machines/Form/Field/index.js +102 -0
  24. package/dist/machines/Form/Field/index.js.map +1 -0
  25. package/dist/machines/{Form.js → Form/index.js} +10 -55
  26. package/dist/machines/Form/index.js.map +1 -0
  27. package/dist/machines/QueryState.js +28 -0
  28. package/dist/machines/QueryState.js.map +1 -0
  29. package/dist/machines/basic.js +37 -0
  30. package/dist/machines/basic.js.map +1 -0
  31. package/dist/machines/index copy.js +39 -0
  32. package/dist/machines/index copy.js.map +1 -0
  33. package/dist/machines/index.js +3 -3
  34. package/dist/machines/index.js.map +1 -1
  35. package/dist/machines/index.test copy.js +37 -0
  36. package/dist/machines/index.test copy.js.map +1 -0
  37. package/package.json +25 -32
  38. package/src/definition.ts +9 -5
  39. package/src/machines/Form/Error.ts +5 -0
  40. package/src/machines/Form/Field/index.ts +234 -0
  41. package/src/machines/{Form.test.ts → Form/index.test.ts} +10 -11
  42. package/src/machines/{Form.ts → Form/index.ts} +11 -146
  43. package/src/machines/QueryState.ts +89 -0
  44. package/src/machines/{Struct.ts → basic.ts} +23 -7
  45. package/src/machines/{Struct.test.ts → index.test.ts} +13 -1
  46. package/src/machines/index.ts +3 -3
  47. package/dist/dts/form/definition.d.ts +0 -20
  48. package/dist/dts/form/definition.d.ts.map +0 -1
  49. package/dist/dts/form/index.d.ts +0 -2
  50. package/dist/dts/form/index.d.ts.map +0 -1
  51. package/dist/dts/machines/Form.d.ts.map +0 -1
  52. package/dist/dts/machines/FormValue.d.ts +0 -12
  53. package/dist/dts/machines/FormValue.d.ts.map +0 -1
  54. package/dist/form/definition.js +0 -14
  55. package/dist/form/definition.js.map +0 -1
  56. package/dist/form/index.js +0 -2
  57. package/dist/form/index.js.map +0 -1
  58. package/dist/machines/Form.js.map +0 -1
  59. package/dist/machines/FormValue.js +0 -12
  60. package/dist/machines/FormValue.js.map +0 -1
  61. package/src/machines/of.test.ts +0 -15
  62. package/src/machines/of.ts +0 -11
@@ -0,0 +1,39 @@
1
+ import { Record } from 'effect';
2
+ import { make, makeStore, } from '../definition.js';
3
+ export * as Form from './Form/index.js';
4
+ export { QueryState, trackEffect } from './QueryState.js';
5
+ export const of = (initialState) => make()({
6
+ initialState,
7
+ actions: ({ Store }) => ({
8
+ get: Store.get,
9
+ update: (f) => Store.update(f),
10
+ set: (value) => Store.update(() => value),
11
+ }),
12
+ });
13
+ export const Struct = (fields) => make()({
14
+ initialState: Record.map(fields, _ => _.initialState),
15
+ actions: ({ Store }) => ({
16
+ ...Record.map(fields, (_, key) => _.actions({
17
+ Store: makeStore({
18
+ get: () => Store.get()[key],
19
+ update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
20
+ }),
21
+ })),
22
+ }),
23
+ start: ({ Store }) => Promise.all(Object.keys(fields).map(async (key) => {
24
+ const field = fields[key];
25
+ return field.start?.({
26
+ Store: makeStore({
27
+ get: () => Store.get()[key],
28
+ update: f => Store.update(_ => ({ ..._, [key]: f(_[key]) })),
29
+ }),
30
+ });
31
+ })),
32
+ onUpdate: async (state) => {
33
+ await Promise.all(Object.keys(fields).map(async (key) => {
34
+ const field = fields[key];
35
+ return field.onUpdate?.(state[key]);
36
+ }));
37
+ },
38
+ });
39
+ //# sourceMappingURL=index%20copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index copy.js","sourceRoot":"","sources":["../../src/machines/index copy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAEL,IAAI,EACJ,SAAS,GAEV,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAEzD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAI,YAAe,EAAE,EAAE,CACvC,IAAI,EAAK,CAAC;IACR,YAAY;IACZ,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACvB,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,MAAM,EAAE,CAAC,CAAqB,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QAClD,GAAG,EAAE,CAAC,KAAQ,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC;KAC7C,CAAC;CACH,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,MAAM,GAAG,CAGpB,MAAS,EACT,EAAE,CACF,IAAI,EAEA,CAID;IACD,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAQ;IAC5D,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACvB,GAAI,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAChC,CAAC,CAAC,OAAO,CAAC;YACR,KAAK,EAAE,SAAS,CAAC;gBACf,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC;gBAC3B,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;aAC7D,CAAQ;SACV,CAAC,CACK;KACV,CAAC;IACF,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CACnB,OAAO,CAAC,GAAG,CACT,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,KAAK,EAAC,GAAG,EAAC,EAAE;QAClC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAE,CAAA;QAC1B,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;YACnB,KAAK,EAAE,SAAS,CAAC;gBACf,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC;gBAC3B,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;aAC7D,CAAQ;SACV,CAAC,CAAA;IACJ,CAAC,CAAC,CACH;IACH,QAAQ,EAAE,KAAK,EAAC,KAAK,EAAC,EAAE;QACtB,MAAM,OAAO,CAAC,GAAG,CACf,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,KAAK,EAAC,GAAG,EAAC,EAAE;YAClC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAE,CAAA;YAC1B,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QACrC,CAAC,CAAC,CACH,CAAA;IACH,CAAC;CACF,CAAC,CAAA"}
@@ -1,4 +1,4 @@
1
- export * as Form from './Form.js';
2
- export { of } from './of.js';
3
- export { Struct } from './Struct.js';
1
+ export * from './basic.js';
2
+ export * as Form from './Form/index.js';
3
+ export { QueryState, trackEffect } from './QueryState.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/machines/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/machines/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,OAAO,KAAK,IAAI,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from '@effect/vitest';
2
+ import { StateMachine } from '@matheuspuel/state-machine';
3
+ import { Effect, pipe } from 'effect';
4
+ describe('of', () => {
5
+ it('should work', () => {
6
+ const machine = StateMachine.of('');
7
+ const instance = StateMachine.run(machine);
8
+ instance.actions.set('a');
9
+ const state = instance.ref.get.pipe(Effect.runSync);
10
+ expect(state).toStrictEqual('a');
11
+ const data = instance.actions.get();
12
+ expect(data).toStrictEqual('a');
13
+ });
14
+ });
15
+ describe('Struct', () => {
16
+ it('should work', () => {
17
+ const machine = pipe(StateMachine.Struct({
18
+ a: StateMachine.of(0),
19
+ b: StateMachine.of(''),
20
+ }), base => StateMachine.make()({
21
+ ...base,
22
+ actions: ({ Store }) => ({
23
+ ...base.actions({ Store }),
24
+ get: () => Store.get(),
25
+ }),
26
+ }));
27
+ const instance = StateMachine.run(machine);
28
+ const getState = () => instance.ref.get.pipe(Effect.runSync);
29
+ expect(getState()).toStrictEqual({ a: 0, b: '' });
30
+ instance.actions.a.set(1);
31
+ expect(getState()).toStrictEqual({ a: 1, b: '' });
32
+ instance.actions.b.set('a');
33
+ expect(getState()).toStrictEqual({ a: 1, b: 'a' });
34
+ expect(instance.actions.get()).toStrictEqual({ a: 1, b: 'a' });
35
+ });
36
+ });
37
+ //# sourceMappingURL=index.test%20copy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test copy.js","sourceRoot":"","sources":["../../src/machines/index.test copy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAErC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE;IAClB,EAAE,CAAC,aAAa,EAAE,GAAG,EAAE;QACrB,MAAM,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;QACnC,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC1C,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACzB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QACnD,MAAM,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;QAChC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,CAAA;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;IACjC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,QAAQ,EAAE,GAAG,EAAE;IACtB,EAAE,CAAC,aAAa,EAAE,GAAG,EAAE;QACrB,MAAM,OAAO,GAAG,IAAI,CAClB,YAAY,CAAC,MAAM,CAAC;YAClB,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC;YACrB,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC;SACvB,CAAC,EACF,IAAI,CAAC,EAAE,CACL,YAAY,CAAC,IAAI,EAAiC,CAAC;YACjD,GAAG,IAAI;YACP,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;gBACvB,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC;gBAC1B,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;aACvB,CAAC;SACH,CAAC,CACL,CAAA;QACD,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC1C,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC5D,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;QACjD,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACzB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;QACjD,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3B,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;QAClD,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matheuspuel/state-machine",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "/dist",
@@ -12,54 +12,47 @@
12
12
  "types": "./dist/dts/index.d.ts",
13
13
  "default": "./dist/index.js"
14
14
  },
15
- "./definition": {
16
- "types": "./dist/dts/definition.d.ts",
17
- "default": "./dist/definition.js"
18
- },
19
- "./machines": {
20
- "types": "./dist/dts/machines/index.d.ts",
21
- "default": "./dist/machines/index.js"
22
- },
23
15
  "./react": {
24
16
  "types": "./dist/dts/react/index.d.ts",
25
17
  "default": "./dist/react/index.js"
26
- },
27
- "./runtime": {
28
- "types": "./dist/dts/runtime/index.d.ts",
29
- "default": "./dist/runtime/index.js"
30
18
  }
31
19
  },
32
- "scripts": {
33
- "build": "tsc -b tsconfig.build.json",
34
- "build:watch": "tsc -b tsconfig.build.json --watch",
35
- "lint": "eslint . --max-warnings 0 --config .eslintrc.production.js",
36
- "lint:fix": "eslint . --fix --config .eslintrc.production.js",
37
- "typecheck": "tsc",
38
- "test": "vitest"
39
- },
40
20
  "dependencies": {
41
- "@matheuspuel/optic": "^0.2.0",
42
- "effect": "^3.18.1",
21
+ "@matheuspuel/optic": "^0.3.0",
22
+ "@matheuspuel/query-state": "^0.3.0",
23
+ "effect": "^3.19.16",
43
24
  "react": "19.1.0",
44
25
  "use-sync-external-store": "^1.5.0"
45
26
  },
46
27
  "devDependencies": {
47
- "@effect/language-service": "^0.41.1",
48
- "@effect/vitest": "^0.26.0",
28
+ "@effect/language-service": "^0.73.1",
29
+ "@effect/vitest": "^0.27.0",
49
30
  "@matheuspuel/state-machine": "file:.",
50
31
  "@types/node": "^25.0.8",
51
32
  "@types/react": "~19.1.17",
52
33
  "@types/use-sync-external-store": "^1.5.0",
53
- "@typescript-eslint/parser": "^8.29.1",
54
- "@vitest/coverage-v8": "^3.2.4",
34
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
35
+ "@typescript-eslint/parser": "^8.55.0",
36
+ "@vitest/coverage-v8": "^4.0.18",
55
37
  "cross-env": "^7.0.3",
56
38
  "eslint": "^8.57.1",
57
39
  "eslint-config-prettier": "^10.1.1",
58
40
  "eslint-plugin-prettier": "^5.2.6",
59
- "prettier": "^3.6.2",
60
- "tsx": "^4.20.6",
41
+ "eslint-plugin-react-hooks": "^7.0.1",
42
+ "prettier": "^3.8.1",
43
+ "tsx": "^4.21.0",
61
44
  "typescript": "^5.9.3",
62
- "vite": "^6.3.5",
63
- "vitest": "^3.2.4"
45
+ "vite": "^7.3.1",
46
+ "vitest": "^4.0.18"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc --project tsconfig.build.json",
50
+ "build:watch": "tsc --project tsconfig.build.json --watch",
51
+ "lint": "eslint . --max-warnings 0 --config .eslintrc.production.cjs",
52
+ "lint:fix": "eslint . --fix --config .eslintrc.production.cjs",
53
+ "typecheck": "tsc",
54
+ "test": "vitest",
55
+ "check": "pnpm build && pnpm typecheck && pnpm lint && pnpm test --run",
56
+ "upload": "pnpm check && pnpm publish"
64
57
  }
65
- }
58
+ }
package/src/definition.ts CHANGED
@@ -43,10 +43,14 @@ export type AnyStateMachineWithActions<Actions extends AnyStateActions> = {
43
43
  onUpdate?: (state: any) => void | Promise<void>
44
44
  }
45
45
 
46
- export const make =
47
- <State>() =>
48
- <Actions extends AnyStateActions>(args: StateMachine<State, Actions>) =>
49
- args
46
+ export const make = <State, Actions extends AnyStateActions>(
47
+ args: StateMachine<State, Actions>,
48
+ ) => args
49
+
50
+ export const withState = <State>() => ({
51
+ make: <Actions extends AnyStateActions>(args: StateMachine<State, Actions>) =>
52
+ args,
53
+ })
50
54
 
51
55
  export type PreparedStateActions<Actions extends AnyStateActions> = Actions
52
56
 
@@ -63,7 +67,7 @@ export const mapActions = <
63
67
  self: StateMachine<State, Actions>,
64
68
  f: (actions: Actions, machine: { Store: Store<State> }) => NextActions,
65
69
  ): StateMachine<State, NextActions> =>
66
- make<State>()({
70
+ make({
67
71
  ...self,
68
72
  actions: machine => ({ ...f(self.actions(machine), machine) }),
69
73
  })
@@ -0,0 +1,5 @@
1
+ import { Data } from 'effect'
2
+
3
+ export class ValidationError<E> extends Data.TaggedError(
4
+ 'FormValidationError',
5
+ )<{ error: E }> {}
@@ -0,0 +1,234 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+
3
+ import { Effect, Either, flow, Option, pipe, Schema } from 'effect'
4
+ import { NoSuchElementException } from 'effect/Cause'
5
+ import { ParseError } from 'effect/ParseResult'
6
+ import * as StateMachine from '../../../definition.js'
7
+ import { ValidationError } from '../Error.js'
8
+
9
+ type FormFieldBase<A, I, E> = StateMachine.StateMachine<
10
+ { value: I; error: E | null },
11
+ {
12
+ set: (value: I) => void
13
+ update: (f: (previous: I) => I) => void
14
+ error: { set: (error: E | null) => void }
15
+ validate: () => Effect.Effect<A, ValidationError<E>>
16
+ check: () => Promise<Either.Either<A, ValidationError<E>>>
17
+ setStateFromData: (data: A) => void
18
+ }
19
+ >
20
+
21
+ export type FormField<A, I, E> = FormFieldBase<A, I, E> & {
22
+ withError: <E2>() => FormField<A, I, E2 | E>
23
+ parse: {
24
+ <A2 extends A, E2>(
25
+ to: (value: NoInfer<A>) => Effect.Effect<A2, E2>,
26
+ ): FormField<A2, I, E2 | E>
27
+ <A2, E2>(args: {
28
+ to: (value: NoInfer<A>) => Effect.Effect<A2, E2>
29
+ from: (data: A2) => NoInfer<A>
30
+ }): FormField<A2, I, E2 | E>
31
+ }
32
+ transform: {
33
+ <A2 extends A>(to: (value: NoInfer<A>) => A2): FormField<A2, I, E>
34
+ <A2>(args: {
35
+ to: (value: NoInfer<A>) => A2
36
+ from: (data: A2) => NoInfer<A>
37
+ }): FormField<A2, I, E>
38
+ <A2>(schema: Schema.Schema<A2, A>): FormField<A2, I, E | ParseError>
39
+ <A2, E2>(
40
+ schema: Schema.Schema<A2, A>,
41
+ makeError: (error: ParseError) => E2,
42
+ ): FormField<A2, I, E | E2>
43
+ }
44
+ filter: <E2>(
45
+ predicate: (value: A) => boolean,
46
+ onFail: (value: A) => E2,
47
+ ) => FormField<A, I, E | E2>
48
+ required: () => FormField<NonNullable<A>, I, E | NoSuchElementException>
49
+ }
50
+
51
+ export type AnyFormField = FormField<any, any, any>
52
+
53
+ const addExtraProperties = <A, I, E>(
54
+ self: FormFieldBase<A, I, E>,
55
+ ): FormField<A, I, E> => {
56
+ const withError = <E2>(): FormField<A, I, E2 | E> =>
57
+ self as FormField<A, I, E2 | E>
58
+
59
+ const parse: FormField<A, I, E>['parse'] = <A2, E2>(
60
+ args:
61
+ | ((value: NoInfer<A>) => Effect.Effect<A2, E2>)
62
+ | {
63
+ to: (value: NoInfer<A>) => Effect.Effect<A2, E2>
64
+ from: (data: A2) => NoInfer<A>
65
+ },
66
+ ): FormField<A2, I, E2 | E> => {
67
+ const to = typeof args === 'function' ? args : args.to
68
+ const from =
69
+ typeof args === 'function' ? (_: A2) => _ as unknown as A : args.from
70
+ return addExtraProperties<A2, I, E2 | E>(
71
+ StateMachine.mapActions(withError<E2>(), (actions, { Store }) => {
72
+ const validate = (): Effect.Effect<
73
+ A2,
74
+ ValidationError<E2 | E>,
75
+ never
76
+ > =>
77
+ actions.validate().pipe(
78
+ Effect.flatMap(
79
+ flow(
80
+ to,
81
+ Effect.tap(() =>
82
+ Effect.sync(() =>
83
+ Store.update(_ => ({ value: _.value, error: null })),
84
+ ),
85
+ ),
86
+ Effect.tapError(error =>
87
+ Effect.sync(() =>
88
+ Store.update(_ => ({ value: _.value, error })),
89
+ ),
90
+ ),
91
+ Effect.mapError(error => new ValidationError({ error })),
92
+ ),
93
+ ),
94
+ )
95
+ return {
96
+ ...actions,
97
+ validate,
98
+ check: () => validate().pipe(Effect.either, Effect.runPromise),
99
+ setStateFromData: data => actions.setStateFromData(from(data)),
100
+ }
101
+ }),
102
+ )
103
+ }
104
+
105
+ const transform: FormField<A, I, E>['transform'] = <A2, E2>(
106
+ args:
107
+ | ((value: NoInfer<A>) => A2)
108
+ | {
109
+ to: (value: NoInfer<A>) => A2
110
+ from: (data: A2) => NoInfer<A>
111
+ }
112
+ | Schema.Schema<A2, A>,
113
+ makeError?: (error: ParseError) => E2,
114
+ ): FormField<A2, I, any> => {
115
+ if (Schema.isSchema(args)) {
116
+ const schema = args as Schema.Schema<A2, A>
117
+ return parse({
118
+ to: value =>
119
+ Effect.mapError(Schema.decode(schema)(value), parseError =>
120
+ (makeError ?? (_ => _))(parseError),
121
+ ),
122
+ from: Schema.encodeSync(schema),
123
+ })
124
+ } else if (typeof args === 'function') {
125
+ const to = args
126
+ return parse({
127
+ to: _ => Effect.succeed(to(_)),
128
+ from: (_: A2) => _ as unknown as A,
129
+ })
130
+ } else if ('to' in args) {
131
+ const { to, from } = args
132
+ return parse({
133
+ to: _ => Effect.succeed(to(_)),
134
+ from: from,
135
+ })
136
+ } else {
137
+ throw new Error('Invalid arguments')
138
+ }
139
+ }
140
+
141
+ const filter: FormField<A, I, E>['filter'] = (predicate, onFail) =>
142
+ parse(value =>
143
+ predicate(value) ? Effect.succeed(value) : Effect.fail(onFail(value)),
144
+ )
145
+
146
+ return {
147
+ ...self,
148
+ withError,
149
+ parse,
150
+ transform,
151
+ filter,
152
+ required: () => parse(Option.fromNullable),
153
+ }
154
+ }
155
+
156
+ export const make = <A, I, E>(args: {
157
+ initial: I
158
+ validate: (value: I) => Effect.Effect<A, E>
159
+ fromData: (data: A) => I
160
+ }): FormField<A, I, E> =>
161
+ addExtraProperties(
162
+ StateMachine.withState<{ value: I; error: E | null }>().make({
163
+ initialState: { value: args.initial, error: null },
164
+ actions: ({ Store }) => {
165
+ const validate = () =>
166
+ pipe(
167
+ Store.get().value,
168
+ args.validate,
169
+ Effect.tap(() =>
170
+ Effect.sync(() =>
171
+ Store.update(_ => ({ value: _.value, error: null })),
172
+ ),
173
+ ),
174
+ Effect.tapError(error =>
175
+ Effect.sync(() => Store.update(_ => ({ value: _.value, error }))),
176
+ ),
177
+ Effect.mapError(error => new ValidationError({ error })),
178
+ )
179
+ return {
180
+ set: value => Store.update(() => ({ value, error: null })),
181
+ update: f => Store.update(_ => ({ value: f(_.value), error: null })),
182
+ error: {
183
+ set: error => Store.update(_ => ({ value: _.value, error })),
184
+ },
185
+ validate,
186
+ check: () => validate().pipe(Effect.either, Effect.runPromise),
187
+ setStateFromData: data =>
188
+ Store.update(_ => ({
189
+ ..._,
190
+ value: args.fromData(data),
191
+ error: null,
192
+ })),
193
+ }
194
+ },
195
+ }),
196
+ )
197
+
198
+ export const of = <A>(initial: A): FormField<A, A, never> =>
199
+ make<A, A, never>({
200
+ initial: initial,
201
+ validate: _ => Effect.succeed(_),
202
+ fromData: _ => _,
203
+ })
204
+
205
+ export const nullOr = <A>(initial?: A | null) => of<A | null>(initial ?? null)
206
+
207
+ // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
208
+ export const undefinedOr = <A>(initial?: A | undefined) =>
209
+ of<A | undefined>(initial)
210
+
211
+ export const String = of('')
212
+
213
+ export const TrimString = String.transform(_ => _.trim())
214
+
215
+ export const TrimStringOrNull = TrimString.transform({
216
+ to: _ => _ || null,
217
+ from: _ => _ ?? '',
218
+ })
219
+
220
+ export const TrimStringOrUndefined = TrimString.transform({
221
+ to: _ => _ || undefined,
222
+ from: _ => _ ?? '',
223
+ })
224
+
225
+ export const NonEmptyString = String.parse({
226
+ to: _ => Schema.decodeOption(Schema.NonEmptyString)(_),
227
+ from: _ => _,
228
+ })
229
+
230
+ export const TrimNonEmptyString = String.parse({
231
+ to: _ =>
232
+ Schema.decodeOption(Schema.compose(Schema.Trim, Schema.NonEmptyString))(_),
233
+ from: _ => _,
234
+ })
@@ -2,14 +2,13 @@ import { describe, expect, it } from '@effect/vitest'
2
2
  import { Form, StateMachine } from '@matheuspuel/state-machine'
3
3
  import { Effect } from 'effect'
4
4
  import { NoSuchElementException } from 'effect/Cause'
5
- import { ValidationError } from './Form.js'
5
+ import { ValidationError } from './index.js'
6
6
 
7
7
  describe('Form', () => {
8
8
  it('should work', () => {
9
- const formField = Form.transformField(Form.field(0), {
10
- validate: _ =>
11
- _ > 1 ? Effect.succeed({ n: _ }) : Effect.fail('low' as const),
12
- fromData: (data: { n: number }) => data.n,
9
+ const formField = Form.Field.of(0).parse({
10
+ to: _ => (_ > 1 ? Effect.succeed({ n: _ }) : Effect.fail('low' as const)),
11
+ from: (data: { n: number }) => data.n,
13
12
  })
14
13
  const machine = Form.Struct({
15
14
  a: formField,
@@ -66,8 +65,8 @@ describe('Form', () => {
66
65
 
67
66
  it('TaggedUnion', () => {
68
67
  const stateMachine = Form.TaggedUnion('_tag', {
69
- A: Form.Struct({ a: Form.field(0), x: Form.field('') }),
70
- B: Form.Struct({ b: Form.field(0), x: Form.field('') }),
68
+ A: Form.Struct({ a: Form.Field.of(0), x: Form.Field.of('') }),
69
+ B: Form.Struct({ b: Form.Field.of(0), x: Form.Field.of('') }),
71
70
  })
72
71
  const form = StateMachine.run(stateMachine)
73
72
  const getState = () => form.ref.get.pipe(Effect.runSync)
@@ -96,7 +95,7 @@ describe('Form', () => {
96
95
 
97
96
  it('Array', () => {
98
97
  const stateMachine = Form.Struct({
99
- list: Form.Array(Form.Struct({ a: Form.NonEmptyString })),
98
+ list: Form.Array(Form.Struct({ a: Form.Field.NonEmptyString })),
100
99
  })
101
100
  const form = StateMachine.run(stateMachine)
102
101
  const getState = () => form.ref.get.pipe(Effect.runSync)
@@ -137,11 +136,11 @@ describe('Form', () => {
137
136
 
138
137
  it('change password example', () => {
139
138
  const stateMachine = Form.Struct({
140
- oldPassword: Form.field(''),
139
+ oldPassword: Form.Field.of(''),
141
140
  newPassword: StateMachine.mapActions(
142
141
  Form.Struct({
143
- password: Form.field(''),
144
- confirmation: Form.fieldWithError<'not-match'>()(Form.field('')),
142
+ password: Form.Field.of(''),
143
+ confirmation: Form.Field.of('').withError<'not-match'>(),
145
144
  }),
146
145
  actions => ({
147
146
  ...actions,