@colisweb/rescript-toolkit 2.67.0 → 2.68.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 (81) hide show
  1. package/.gitlab-ci.yml +3 -2
  2. package/.yarn/cache/boolbase-npm-1.0.0-965fe9af6d-3e25c80ef6.zip +0 -0
  3. package/.yarn/cache/camelcase-npm-3.0.0-0c65af0c7f-ae4fe1c17c.zip +0 -0
  4. package/.yarn/cache/comma-separated-tokens-npm-1.0.8-00dbbf3418-0adcb07174.zip +0 -0
  5. package/.yarn/cache/css-selector-parser-npm-1.4.1-b8c642c4c5-31948754e5.zip +0 -0
  6. package/.yarn/cache/fault-npm-2.0.1-c462630f58-c9b30f47d9.zip +0 -0
  7. package/.yarn/cache/format-npm-0.2.2-679f3acc64-646a60e133.zip +0 -0
  8. package/.yarn/cache/github-slugger-npm-2.0.0-3afba76e6c-250375cde2.zip +0 -0
  9. package/.yarn/cache/hast-util-has-property-npm-1.0.4-a09b607810-23025cee66.zip +0 -0
  10. package/.yarn/cache/hast-util-has-property-npm-2.0.1-aa6919669c-cc909b7e29.zip +0 -0
  11. package/.yarn/cache/hast-util-heading-rank-npm-2.1.1-0d71da5801-a49233e9ac.zip +0 -0
  12. package/.yarn/cache/hast-util-is-element-npm-1.1.0-be10e62fa7-30fad3f65e.zip +0 -0
  13. package/.yarn/cache/hast-util-is-element-npm-2.1.3-3051d610ff-9d988f6839.zip +0 -0
  14. package/.yarn/cache/hast-util-select-npm-1.0.1-115974f390-9cdb20850f.zip +0 -0
  15. package/.yarn/cache/hast-util-to-string-npm-2.0.0-c6108aa2b8-0c087f8dee.zip +0 -0
  16. package/.yarn/cache/hast-util-to-text-npm-3.1.2-922eb1f623-d17cf3344c.zip +0 -0
  17. package/.yarn/cache/hast-util-whitespace-npm-1.0.4-43bb1ff3d0-b7f4a1942b.zip +0 -0
  18. package/.yarn/cache/{husky-npm-8.0.2-46c70b41ed-e101656fcb.zip → husky-npm-8.0.3-b0b59c5127-837bc7e441.zip} +0 -0
  19. package/.yarn/cache/lowlight-npm-2.8.0-c65abb6cac-c45a91e715.zip +0 -0
  20. package/.yarn/cache/not-npm-0.1.0-a1712708cd-8043bb53bc.zip +0 -0
  21. package/.yarn/cache/nth-check-npm-1.0.2-3f6d0d22eb-59e115fdd7.zip +0 -0
  22. package/.yarn/cache/property-information-npm-3.2.0-ae434c241d-ed2614520d.zip +0 -0
  23. package/.yarn/cache/rehype-add-classes-npm-1.0.0-ddf6b4e74d-25c0e2dbf5.zip +0 -0
  24. package/.yarn/cache/rehype-autolink-headings-npm-6.1.1-fe8058cc11-60782fb2e5.zip +0 -0
  25. package/.yarn/cache/rehype-highlight-npm-6.0.0-5179257139-5a70e7ad45.zip +0 -0
  26. package/.yarn/cache/rehype-slug-npm-5.1.0-ae08840ba8-2a7c17fd74.zip +0 -0
  27. package/.yarn/cache/space-separated-tokens-npm-1.1.5-2352c83473-8ef68f1cfa.zip +0 -0
  28. package/.yarn/cache/unist-util-find-after-npm-4.0.0-11b3b7fb4f-8381ef0bad.zip +0 -0
  29. package/.yarn/cache/zwitch-npm-1.0.5-5911cef6ce-28a1bebaca.zip +0 -0
  30. package/.yarn/install-state.gz +0 -0
  31. package/package.json +8 -5
  32. package/playground/{App.res → PlaygroundApp.res} +14 -7
  33. package/playground/PlaygroundComponents.res +114 -0
  34. package/playground/PlaygroundLocales.res +1 -0
  35. package/playground/PlaygroundRouter.res +23 -0
  36. package/playground/{CodeBlock.res → Playground_CodeBlock.res} +12 -16
  37. package/playground/components/Playground_Alert.res +25 -0
  38. package/playground/components/Playground_Button.res +75 -0
  39. package/playground/components/Playground_Label.res +16 -0
  40. package/playground/components/Playground_Modal.res +53 -0
  41. package/playground/components/Playground_MultiSelect.res +91 -0
  42. package/playground/components/Playground_Switch.res +2 -2
  43. package/playground/custom.css +4 -1
  44. package/playground/{stories/IntroductionColors.stories.mdx → design/DesignSystem_Colors.mdx} +0 -0
  45. package/playground/{stories/IntroductionFonts.stories.mdx → design/DesignSystem_Fonts.mdx} +0 -0
  46. package/playground/{stories/IntroductionMediaQueries.stories.mdx → design/DesignSystem_MediaQueries.mdx} +0 -0
  47. package/playground/design/Playground_DesignSystem.res +12 -0
  48. package/playground/{stories/TailwindConfigBreakpoints.js → design/TailwindConfigBreakpoints.jsx} +1 -1
  49. package/playground/{stories/TailwindConfigColorsPreview.js → design/TailwindConfigColorsPreview.jsx} +1 -1
  50. package/playground/{stories/TailwindConfigFontsPreview.js → design/TailwindConfigFontsPreview.jsx} +1 -1
  51. package/playground/docs/ApiDecoding.mdx +70 -0
  52. package/playground/docs/Form.mdx +109 -0
  53. package/playground/docs/Identifiers.mdx +38 -0
  54. package/playground/{Doc.res → docs/Playground_Docs.res} +8 -8
  55. package/playground/docs/Primitives.mdx +53 -0
  56. package/playground/docs/Request.mdx +281 -0
  57. package/playground/docs/Router.mdx +43 -0
  58. package/playground/docs/Setup.mdx +51 -0
  59. package/playground/docs/Unleash.mdx +53 -0
  60. package/playground/main.jsx +1 -1
  61. package/src/intl/Toolkit__Intl.res +5 -9
  62. package/src/intl/Toolkit__Intl.resi +1 -0
  63. package/src/logger/Toolkit__BrowserLogger.res +15 -15
  64. package/src/router/Toolkit__Router.res +2 -1
  65. package/src/tailwind/tailwind.config.cjs +3 -5
  66. package/src/ui/Toolkit__Ui_MultiSelect.res +21 -0
  67. package/src/ui/styles.css +6 -3
  68. package/vite.config.js +44 -1
  69. package/.yarn/cache/cosmiconfig-npm-7.0.0-b9d0d7d156-6801feaa02.zip +0 -0
  70. package/.yarn/cache/import-fresh-npm-3.3.0-3e34265ca9-2cacfad06e.zip +0 -0
  71. package/.yarn/cache/klona-npm-2.0.4-6bc4e7cd86-abc6690882.zip +0 -0
  72. package/.yarn/cache/postcss-loader-npm-4.2.0-f01fec2503-c45ec1ca1b.zip +0 -0
  73. package/.yarn/cache/semver-npm-7.3.4-4c3baf0ead-96451bfd7c.zip +0 -0
  74. package/playground/Playground_Component.res +0 -33
  75. package/playground/Playground_ComponentDetails.res +0 -25
  76. package/playground/Playground_ComponentsList.res +0 -43
  77. package/playground/stories/Toolkit__UI_AlertStory.res +0 -111
  78. package/playground/stories/Toolkit__UI_LabelStory.res +0 -39
  79. package/playground/stories/Toolkit__UI_ModalStory.res +0 -211
  80. package/playground/stories/Toolkit__UI_MultiSelectStory.res +0 -132
  81. package/playground/stories/Toolkit__Ui_ButtonStory.res +0 -324
@@ -155,9 +155,12 @@
155
155
  }
156
156
 
157
157
  h2 {
158
- @apply text-2xl font-display font-bold;
158
+ @apply text-3xl font-display font-bold;
159
159
  }
160
160
  h3 {
161
+ @apply text-2xl font-display font-bold;
162
+ }
163
+ h4 {
161
164
  @apply text-xl font-display font-bold;
162
165
  }
163
166
 
@@ -0,0 +1,12 @@
1
+ module Fonts = {
2
+ @module("../../../../playground/design/DesignSystem_Fonts.mdx") @react.component
3
+ external make: unit => React.element = "default"
4
+ }
5
+ module Colors = {
6
+ @module("../../../../playground/design/DesignSystem_Colors.mdx") @react.component
7
+ external make: unit => React.element = "default"
8
+ }
9
+ module MediaQueries = {
10
+ @module("../../../../playground/design/DesignSystem_MediaQueries.mdx") @react.component
11
+ external make: unit => React.element = "default"
12
+ }
@@ -1,4 +1,4 @@
1
- import tailwindConfig from "../src/tailwind/tailwind.config";
1
+ import * as tailwindConfig from "../../src/tailwind/tailwind.config.js";
2
2
 
3
3
  const screens = tailwindConfig.theme.screens;
4
4
 
@@ -1,4 +1,4 @@
1
- import tailwindConfig from "../../src/tailwind/tailwind.config";
1
+ import * as tailwindConfig from "../../src/tailwind/tailwind.config.js";
2
2
 
3
3
  const colors = tailwindConfig.theme.colors;
4
4
 
@@ -1,4 +1,4 @@
1
- import tailwindConfig from "../src/tailwind/tailwind.config";
1
+ import * as tailwindConfig from "../../src/tailwind/tailwind.config.js";
2
2
 
3
3
  const fontFamily = tailwindConfig.theme.fontFamily;
4
4
  const fontSize = tailwindConfig.theme.fontSize;
@@ -0,0 +1,70 @@
1
+ # Decode content from API result
2
+
3
+ ## API
4
+
5
+ ### Decode an enumeration
6
+
7
+ ```rescript
8
+ module LiftEnum = {
9
+ @deriving(jsConverter)
10
+ type enum = [
11
+ | #withLift
12
+ | #withoutLift
13
+ | #unknown
14
+ ]
15
+ }
16
+
17
+ module Lift = Toolkit.Decoder.Enum(LiftEnum)
18
+
19
+ @decco
20
+ type response = {lift: Lift.t}
21
+ ```
22
+
23
+ ### Decode a date
24
+
25
+ ```rescript
26
+ @decco
27
+ type response = {
28
+ createdAt: Toolkit.Decoder.Date.t
29
+ }
30
+ ```
31
+
32
+ ### Decode unit measure
33
+
34
+ There are 2 options :
35
+
36
+ - the field has the unit like : `19 mm`
37
+ - the field has only the value
38
+
39
+ #### Automatic unit handling
40
+
41
+ ```rescript
42
+ [@decco]
43
+ type response = {
44
+ length: Toolkit.Decoder.UnitMeasure.Dimension.WithUnit.t
45
+ }
46
+ ```
47
+
48
+ #### Known unit
49
+
50
+ ```rescript
51
+ [@decco]
52
+ type response = {
53
+ weight: Toolkit.Decoder.UnitMeasure.Weight.Kg.t
54
+ }
55
+ ```
56
+
57
+ ### Decode an Array written as a String
58
+
59
+ This codec is intended for encoding the array params of API requests of scala services.
60
+
61
+ ```rescript
62
+ @decco
63
+ type params = {
64
+ lengths: Toolkit.Decoder.StringArray<Toolkit.Decoder.UnitMeasure.Dimension.WithUnit.t>
65
+ };
66
+
67
+ let params = {lengths:[#cm(1),#m(40), #km(3)]};
68
+ let encodedParams = params->params_encode; // "1.00 cm,40.00 m,3.00 km"
69
+ let decodedParams = encodedParams->params_decode; // Ok([#cm(1),#m(40),#km(3)])
70
+ ```
@@ -0,0 +1,109 @@
1
+ # Handling forms
2
+
3
+ We have created a fork of [reform@9.8.0](https://github.com/Astrocoders/reform) we had some features :
4
+
5
+ - `onSubmit` must return a `Promise.promise(result('a, 'e))`
6
+ - `SubmitFailed(option('error'))` to the `formState('error)` variant
7
+
8
+ ## Usage
9
+
10
+ ### Define a FormState
11
+
12
+ The module must contains an `error` type and an include of `state` type with `%lenses`
13
+
14
+ ```rescript
15
+ module FormState = {
16
+ type error = ColiswebApi.V5.Store.Contact.CreateContact.Request.error;
17
+ include [%lenses
18
+ type state = {
19
+ firstName: string,
20
+ lastName: string,
21
+ email: string,
22
+ phone1: string,
23
+ phone2: string,
24
+ }
25
+ ];
26
+ };
27
+
28
+ module FormApi = Toolkit.Form.Make(FormState);
29
+ ```
30
+
31
+ ### Use the form in React
32
+
33
+ ```rescript
34
+ // ...
35
+ @react.component
36
+ let make = () => {
37
+
38
+ let initialState =
39
+ FormState.{
40
+ firstName: "",
41
+ lastName: "",
42
+ phone1: "",
43
+ phone2: "",
44
+ email: "",
45
+ };
46
+
47
+ let schema =
48
+ FormApi.Form.Validation.Schema([
49
+ StringNonEmpty(FirstName),
50
+ StringNonEmpty(LastName),
51
+ Email(Email),
52
+ ]);
53
+
54
+ let onSubmit = (form: FormApi.Form.onSubmitAPI) => {
55
+ let {values}: FormApi.Form.state = form.state;
56
+
57
+ ColiswebApi.V5.Store.Contact.CreateContact.Config.exec((
58
+ clientId,
59
+ storeId,
60
+ {
61
+ firstName: values.firstName->OptionUtils.fromString,
62
+ lastName: values.lastName->OptionUtils.fromString,
63
+ email: values.email,
64
+ phone1: values.phone1->OptionUtils.fromString,
65
+ phone2: values.phone2->OptionUtils.fromString,
66
+ },
67
+ ))
68
+ ->Promise.flatMapOk(result =>
69
+ reloadContact()->Promise.map(_ => Ok(result))
70
+ )
71
+ ->Promise.tapOk(_ => hide())
72
+ };
73
+
74
+ let form = FormApi.Form.use(~initialState, ~schema, ~onSubmit, ());
75
+
76
+ <FormApi.Form.Provider value=form>
77
+ <form onSubmit={event => {
78
+ ReactEvent.Synthetic.preventDefault(event);
79
+ form.submit();
80
+ }}>
81
+
82
+ <Form.Field
83
+ field
84
+ render={({handleChange, error, value, validate}) => {
85
+ let isInvalid = error->Option.isSome;
86
+ <>
87
+ <input value onChange={BsReform.Helpers.handleChange(handleChange)} />
88
+
89
+ {
90
+ error->Option.mapWithDefault(React.null, e => <p>e->React.string</p>)
91
+ }
92
+ </>;
93
+ }}
94
+ />;
95
+
96
+ <Button
97
+ type_="submit"
98
+ color=#primary
99
+ isLoading={form.state.formState === Submitting}
100
+ onClick={_ => form.submit()}>
101
+ <FormattedMessage
102
+ id="forms.controls.confirm"
103
+ defaultMessage="Confirm"
104
+ />
105
+ </Button>
106
+ </form>
107
+ </FormApi.Form.Provider>;
108
+ }
109
+ ```
@@ -0,0 +1,38 @@
1
+ # Avoid string type with unique identifiers
2
+
3
+ Convert a string or an int in an opaque type with a decco decoder.
4
+
5
+ ### `string` identifier
6
+
7
+ ```rescript
8
+ module DeliveryId = Toolkit.Identifier.MakeString({});
9
+
10
+ /**
11
+ somewhere
12
+ **/
13
+ @decco
14
+ type response = {
15
+ deliveryId: DeliveryId.t
16
+ };
17
+
18
+ let deliveryId = DeliveryId.make("some-id");
19
+ deliveryId->DeliveryId.toString // "some-id"
20
+ ```
21
+
22
+ ### `int` identifier
23
+
24
+ ```rescript
25
+ module DeliveryId = Toolkit.Identifier.MakeInt({});
26
+
27
+ /**
28
+ somewhere
29
+ **/
30
+ @decco
31
+ type response = {
32
+ deliveryId: DeliveryId.t
33
+ };
34
+
35
+ let deliveryId = DeliveryId.make(12);
36
+ deliveryId->DeliveryId.toString; // "12"
37
+ deliveryId->DeliveryId.toInt; // 12
38
+ ```
@@ -1,32 +1,32 @@
1
1
  module ApiDecoding = {
2
- @module("../../../docs/ApiDecoding.mdx") @react.component
2
+ @module("../../../../playground/docs/ApiDecoding.mdx") @react.component
3
3
  external make: unit => React.element = "default"
4
4
  }
5
5
  module Form = {
6
- @module("../../../docs/Form.mdx") @react.component
6
+ @module("../../../../playground/docs/Form.mdx") @react.component
7
7
  external make: unit => React.element = "default"
8
8
  }
9
9
  module Identifiers = {
10
- @module("../../../docs/Identifiers.mdx") @react.component
10
+ @module("../../../../playground/docs/Identifiers.mdx") @react.component
11
11
  external make: unit => React.element = "default"
12
12
  }
13
13
  module Primitives = {
14
- @module("../../../docs/Primitives.mdx") @react.component
14
+ @module("../../../../playground/docs/Primitives.mdx") @react.component
15
15
  external make: unit => React.element = "default"
16
16
  }
17
17
  module Requests = {
18
- @module("../../../docs/Request.mdx") @react.component
18
+ @module("../../../../playground/docs/Request.mdx") @react.component
19
19
  external make: unit => React.element = "default"
20
20
  }
21
21
  module Setup = {
22
- @module("../../../docs/Setup.mdx") @react.component
22
+ @module("../../../../playground/docs/Setup.mdx") @react.component
23
23
  external make: unit => React.element = "default"
24
24
  }
25
25
  module Router = {
26
- @module("../../../docs/Router.mdx") @react.component
26
+ @module("../../../../playground/docs/Router.mdx") @react.component
27
27
  external make: unit => React.element = "default"
28
28
  }
29
29
  module Unleash = {
30
- @module("../../../docs/Unleash.mdx") @react.component
30
+ @module("../../../../playground/docs/Unleash.mdx") @react.component
31
31
  external make: unit => React.element = "default"
32
32
  }
@@ -0,0 +1,53 @@
1
+ # Primitives
2
+
3
+ Utililties functions for primitives types :
4
+
5
+ - `option`
6
+ - `string`
7
+ - `array`
8
+
9
+ ## Option
10
+
11
+ ### string
12
+
13
+ - `fromString` : `string => option(string)`
14
+ - `toString` : `option(string) => string`
15
+
16
+ ```rescript
17
+ let anEmptyString = "";
18
+
19
+ Toolkit.Primitives.Option.fromString(anEmptyString)
20
+ ->Option.map(v => v ++ "test");
21
+
22
+ Toolkit.Primitives.Option.toString(Some("test")); // "test"
23
+ Toolkit.Primitives.Option.toString(None); // ""
24
+ ```
25
+
26
+ ### boolean
27
+
28
+ - `fromBool` : `bool => option(bool)`
29
+
30
+ ```rescript
31
+ Toolkit.Primitives.Option.fromBool(true); // Some(true)
32
+ Toolkit.Primitives.Option.fromBool(false); // None
33
+ ```
34
+
35
+ ## Array
36
+
37
+ ```rescript
38
+ let variable = ["abc", "dce", ""];
39
+
40
+ Js.log(Primitives.Array.joinNonEmpty(anEmptyString)); // "abc, dce"
41
+ Js.log(Primitives.Array.joinNonEmpty(~separator="+", anEmptyString)); // "abc+dce"
42
+ Js.log(variable->Primitives.Array.tail); // ""
43
+ ```
44
+
45
+ ## Result
46
+
47
+ ```rescript
48
+ let result = Ok("test");
49
+ let test = Error("error");
50
+
51
+ Js.log(Primitives.Result.get(result)); // Some("test")
52
+ Js.log(Primitives.Result.get(test)); // None
53
+ ```
@@ -0,0 +1,281 @@
1
+ # How to manage an API request
2
+
3
+ ## 1. Create the request config
4
+
5
+ You have to declare 3 types :
6
+
7
+ - `argument` is what you give to the function
8
+ - `response` is what your API answer
9
+ - `error` is the potential custom errors that can occur (⚠️ this has to be handled manually)
10
+
11
+ And the `exec` method.
12
+
13
+ We use [decco](https://github.com/reasonml-labs/decco) to serialize / deserialize the data.
14
+
15
+ ```rescript
16
+ let request = Axios.Instance.create(
17
+ Axios.makeConfig(~baseURL=envState.apiUrl,()),
18
+ );
19
+
20
+ module FetchUsers = {
21
+ module Config = {
22
+ type argument = unit;
23
+
24
+ @decco
25
+ type response = array<user>
26
+ @decco
27
+ and user = {
28
+ name: string
29
+ };
30
+
31
+ type error = string;
32
+
33
+ let exec = () => {
34
+ request->Request.get("/", response_decode);
35
+ };
36
+ };
37
+ module Request = Toolkit.Request.Make(Config);
38
+ };
39
+
40
+ FetchUsers.Request.exec();
41
+ ```
42
+
43
+ ### 1.2 Send data
44
+
45
+ We create a decodable `body` type that we give to the `argument` type.
46
+ This decodable `body` will be used in the exec method.
47
+
48
+ ```rescript
49
+ module UpdateUser = {
50
+ module Config = {
51
+ type argument = body
52
+ @decco
53
+ and body = {
54
+ name: string,
55
+ };
56
+
57
+ @decco
58
+ type response = unit;
59
+
60
+ type error = unit;
61
+
62
+ let exec = body => {
63
+ request->Request.postData("/update-user", response_decode, body->body_encode);
64
+ };
65
+ };
66
+ module Request = Toolkit_Request.Make(Config);
67
+ };
68
+
69
+ UpdateUser.Request.exec({ name: "test" })
70
+ ```
71
+
72
+ ### 1.3 Handle errors
73
+
74
+ In order to handle special errors comming from the API, you can use the `mapError` parameter.
75
+
76
+ This parameter requires a callback which contains the **response** as argument and needs to return this type :
77
+
78
+ ```rescript
79
+ option<result<'data, error<'a>>>
80
+ ```
81
+
82
+ The `result` is useful for some cases when an API error should not break the flow so you can handle the specific error and just return `Ok()`.
83
+
84
+ #### Request error type
85
+
86
+ ```rescript
87
+ type undecodedResponse = {
88
+ data: undecodedData,
89
+ headers,
90
+ config,
91
+ status: int,
92
+ };
93
+
94
+ type noResponse = {
95
+ message: string,
96
+ config,
97
+ };
98
+
99
+ // We get this type in mapError callback argument
100
+ type failedResponse = {
101
+ message: string,
102
+ config,
103
+ response,
104
+ }
105
+ and response = {
106
+ data: failedResponseData,
107
+ headers,
108
+ status: int,
109
+ };
110
+
111
+ type error<'a> = [
112
+ | #noResponse(noResponse)
113
+ | #invalidResponse(failedResponse)
114
+ | #invalidResponseData(undecodedResponse, string)
115
+ | #invalidErrorData
116
+ | #unknown(Js.Promise.error)
117
+ | #custom('a)
118
+ ]
119
+ ```
120
+
121
+ ```rescript
122
+ let exec = token => {
123
+ request->Request.get(
124
+ "/test",
125
+ ~mapError=
126
+ error => {
127
+ // have to return option(Toolkit.Request.error('a'))
128
+ },
129
+ response_decode,
130
+ );
131
+ };
132
+ ```
133
+
134
+ `failedResponseData` can be anything depending on the API behavior. So you have to use `Obj.magic` to cast and use the correct field.
135
+
136
+ ```rescript
137
+ let handleError = (error: failedResponse) => {
138
+ // the API return an object { code: string }
139
+ let code = error.response.data->Obj.magic##code;
140
+
141
+ // If code can be undefined
142
+ code
143
+ ->Option.map(code => {
144
+ switch (code) {
145
+ | "some_code" => Error(#custom("message"))
146
+ | "other_code" => Error(#invalidErrorData)
147
+ | "its_ok" => Ok()
148
+ | _ => Error(#custom("unknown"))
149
+ }
150
+ })
151
+ };
152
+ ```
153
+
154
+ Often, we use decco in order to handle complex error (error with data). We create a custom Decoder for this kind of case
155
+
156
+ ```rescript
157
+ module UpdateUser = {
158
+ module Config = {
159
+ type argument = body
160
+ @decco
161
+ and body = {name: string};
162
+
163
+ @decco
164
+ type response = unit;
165
+
166
+ module Error =
167
+ Decoders.Enum({
168
+ @deriving(jsConverter)
169
+ type enum = [
170
+ | #"unknown-user"
171
+ | #invalidData
172
+ | #unknownError
173
+ ];
174
+ });
175
+
176
+ @decco
177
+ type error = Error.t;
178
+
179
+ let exec = body => {
180
+ request->Request.postData(
181
+ "/update-user",
182
+ response_decode,
183
+ body->body_encode,
184
+ ~mapError=
185
+ ({response}) => {
186
+ switch (response.data->Obj.magic->error_decode) {
187
+ | Ok(error) => Some(Error(`custom(error)))
188
+ | Error(err) =>
189
+ Js.log(err);
190
+ Some(Error(`custom(`unknownError)));
191
+ }
192
+ }
193
+ );
194
+ };
195
+ };
196
+ module Request = Toolkit_Request.Make(Config);
197
+ };
198
+
199
+ UpdateUser.Request.exec({ name: "test" })
200
+ ->Promise.tapError(err => {
201
+ switch (err) {
202
+ | #custom(#"unknown-user") => Js.log("user not found")
203
+ | #custom(#unknownError) => Js.log("alert sentry")
204
+ | _ => Js.log("generic handler")
205
+ }
206
+ })
207
+ ```
208
+
209
+ ## 2. Use request in React
210
+
211
+ ### 2.1 Fetching data with swr
212
+
213
+ There are several ways to manage a request in React, we chose to use [swr](https://swr.vercel.app/) with [react-suspense](https://reactjs.org/docs/concurrent-mode-suspense.html) enabled.
214
+ We have a functor to generate automaticatly a swr fetcher based on the request created before :
215
+
216
+ ```rescript
217
+ module GetUsers = {
218
+ module Config = {
219
+ type argument = unit;
220
+
221
+ @decco
222
+ type response = array<user>
223
+ @decco
224
+ and user = {
225
+ name: string
226
+ };
227
+
228
+ type error = string;
229
+
230
+ let exec = () => {
231
+ request->Request.get("/users", response_decode);
232
+ };
233
+ };
234
+ module Request = Toolkit.Request.Make();
235
+ };
236
+
237
+
238
+ module GetUsersFetcher =
239
+ Fetcher.Make({
240
+ module Request = GetUsers.Request;
241
+ let key = () => [|
242
+ "GetUsers",
243
+ |];
244
+ });
245
+ ```
246
+
247
+ Later in the react app
248
+
249
+ ```rescript
250
+ module PageUsers = {
251
+ [@react.component]
252
+ let make = () => {
253
+ let (users, _, _) = GetUsersFetcher.use();
254
+
255
+ <div>
256
+ {
257
+ users
258
+ ->Belt.Array.mapWithIndex((i, user) => {
259
+ <div key={i->Belt.Int.toString}>
260
+ {user.name->React.String}
261
+ </div>
262
+ })
263
+ }
264
+ </div>
265
+ };
266
+ };
267
+
268
+ [@react.component]
269
+ let make = () => {
270
+ <ErrorBoundary>
271
+ <React.Suspense fallback={<Spinner />}>
272
+ <PageUsers />
273
+ </React.Suspense>
274
+ </ErrorBoundary>
275
+ };
276
+ ```
277
+
278
+ There are 2 functions generated by the fetcher functor :
279
+
280
+ - `use` can be used when the API will always return something
281
+ - `useOptional` return an `option<'a>` so it's used when the API can return nothing
@@ -0,0 +1,43 @@
1
+ # Router
2
+
3
+ Type-safe navigation
4
+
5
+ ## Usage
6
+
7
+ ```rescript
8
+ open Toolkit
9
+
10
+ module StoreId = Identifiers.MakeString()
11
+
12
+ module RouterConfig = {
13
+ type admin = Home
14
+
15
+ type t =
16
+ | Home
17
+ | StoreDetails(StoreId.t)
18
+ | Admin(admin)
19
+ | NotFound
20
+
21
+ let make = url =>
22
+ switch url.path {
23
+ | list{} => Home
24
+ | list{"stores", storeId} => StoreDetails(StoreId.make(storeId))
25
+ | list{"admin", ...rest} =>
26
+ switch rest {
27
+ | list{} => Admin(Home)
28
+ | _ => NotFound
29
+ }
30
+ | _ => NotFound
31
+ }
32
+
33
+ let toString = route =>
34
+ switch route {
35
+ | Home => ""
36
+ | StoreDetails(storeId) => "/stores/" ++ storeId->StoreId.toString
37
+ | Admin(Home) => "/admin/"
38
+ | NotFound => "/404"
39
+ }
40
+ }
41
+
42
+ module Navigation = Router.Make(RouterConfig)
43
+ ```