You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+34-25Lines changed: 34 additions & 25 deletions
Original file line number
Diff line number
Diff line change
@@ -1,14 +1,16 @@
1
1
Simple bidirectional parser/printers for your routing data types.
2
2
3
3
# Why?
4
+
4
5
Strongly-typed languages let you define your routes as a data type, ensuring invalid routes fail to compile. But the browser represents locations as strings, so you have to write functions to decode strings into your routing data type and functions to write a route to a string.
5
6
6
7
Unfortunately, writing separate functions to parse and print your routing data type is error-prone and boilerplate-heavy. It’s easy to update a parser and forget to update the accompanying printer, even though almost all routing definitions should round-trip (parsing then printing returns the original string value).
7
8
8
9
`routing-duplex` takes an approach that solves both problems. This library lets you define a codec, or a means to both decode and encode a particular data type, for your routes. Write this codec once and it will handle parsing and printing the same representation for you.
9
10
10
11
## A Brief Example
11
-
Let’s build a codec for a simple app with two routes: the homepage and user profiles (identified by usernames).
12
+
13
+
Let’s build a codec for a simple app with two routes: the homepage and user profiles (identified by usernames).
12
14
13
15
1. Write a data type to represent our two routes, deriving `Generic`.
14
16
2. Build a codec using generics and combinators from `Routing.Duplex`
`routing-duplex` works by letting you define a codec which represents how to encode and decode your routing data type. You can define your routing data however you see fit, and then provide it to the library’s codec type, `RouteDuplex`:
52
54
53
55
```purescript
@@ -62,14 +64,16 @@ type RouteDuplex' a = RouteDuplex a a
62
64
63
65
This library exports a number of helper functions and combinators for constructing this codec with minimal boilerplate, mostly concentrated in two modules:
64
66
65
-
*`Routing.Duplex` exports the `RouteDuplex` type, the `print` and `parse` functions, and combinators that represent constants (`“/post“`), segments (`/:id`), parameters (`?foo=`), prefixes and suffixes, optional values, and more.
66
-
*`Routing.Duplex.Generic` exports helpers for deriving code via your data type’s `Generic` instance, most notably the `sum` function for describing a route as a sum type and the `product` and `noArgs` functions for working with product types.
67
-
*`Routing.Duplex.Generic.Syntax` exports some symbols that can be used to write terse codecs similar to those found in string-based routers.
67
+
-`Routing.Duplex` exports the `RouteDuplex` type, the `print` and `parse` functions, and combinators that represent constants (`“/post“`), segments (`/:id`), parameters (`?foo=`), prefixes and suffixes, optional values, and more.
68
+
-`Routing.Duplex.Generic` exports helpers for deriving code via your data type’s `Generic` instance, most notably the `sum` function for describing a route as a sum type and the `product` and `noArgs` functions for working with product types.
69
+
-`Routing.Duplex.Generic.Syntax` exports some symbols that can be used to write terse codecs similar to those found in string-based routers.
68
70
69
71
# Examples
72
+
70
73
We’ll explore several practical examples of this library in practice while building a real-world routing data type and codec.
71
74
72
75
## Example: Writing a codec for a sum type
76
+
73
77
Let’s begin developing a more complex set of routes. We’ll design routes for a small blogging site made up of users and their posts, as well as feed showing new posts from across the site. We can represent these routes in a small data type:
74
78
75
79
```purescript
@@ -95,7 +99,7 @@ Next, we need to represent the ability to parse static and dynamic segments from
95
99
We can use the `path`, `segment`, and `param` helper functions to capture these static and dynamic segments.
96
100
97
101
```purescript
98
-
-- Allows you to match a static segment. For example, to match the
102
+
-- Allows you to match a static segment. For example, to match the
99
103
-- /feed path, use `path "feed" ...` where `...` represents further
100
104
-- segments.
101
105
path :: forall a b. String -> RouteDuplex a b -> RouteDuplex a b
@@ -122,13 +126,14 @@ int :: RouteDuplex' String -> RouteDuplex' Int
122
126
optional :: forall a b. RouteDuplex a b -> RouteDuplex (Maybe a) (Maybe b)
123
127
```
124
128
125
-
You can easily implement your own combinators using `as`, the function used to construct each of the built-in combinators. We’ll see an example of that later on!
129
+
You can easily implement your own combinators using `as`, the function used to construct each of the built-in combinators. We’ll see an example of that later on!
126
130
127
131
At this point, we have the tools we need to:
128
-
* Handle static segments of a path, like `"/user"`
129
-
* Handle variable segments of a path, like `/:username` or `/:postid`
130
-
* Handle query parameters, like `?foo=bar`
131
-
* Transform string segments into other types, like using the `int` combinator to turn a post ID into a String
132
+
133
+
- Handle static segments of a path, like `"/user"`
134
+
- Handle variable segments of a path, like `/:username` or `/:postid`
135
+
- Handle query parameters, like `?foo=bar`
136
+
- Transform string segments into other types, like using the `int` combinator to turn a post ID into a String
132
137
133
138
However, we still need two more tools to construct our codec. First, we need to be able to specify codecs for every case in our routing sum type: this can be done with the `sum` function from the `Routing.Duplex.Generic` module. Second, we need to be able to specify that a type should match zero or more of these dynamic segments. For routes that have no arguments, we’ll use `noArgs`; for routes with one argument, we’ll just provide a codec; and for routes with multiple arguments, we’ll combine codecs with `product`.
134
139
@@ -143,17 +148,17 @@ route = root $ sum
143
148
144
149
`sum` exposes a nice record syntax so that we can specify codecs for each constructor of the type; if you forget to handle a constructor, you’ll get a compiler error. We need to write a few codecs:
145
150
146
-
* The `Root` constructor takes no arguments and should match when the path is empty. We can represent that with a simple `noArgs`.
147
-
* The `Profile` constructor takes one argument, a string `Username`, and should match a path that begins with `”user“`. We can represent that using the `path` and `segment` functions, along with the `string` combinator.
148
-
* The `Post` constructor takes two arguments: a string `Username` and an integer `PostId`. We’ll need to use the `product` function to put two dynamic segments together.
149
-
* The `Feed` constructor should only match a string constant in the path, `”feed”`. We can represent that with the `path` function and `noArgs`.
151
+
- The `Root` constructor takes no arguments and should match when the path is empty. We can represent that with a simple `noArgs`.
152
+
- The `Profile` constructor takes one argument, a string `Username`, and should match a path that begins with `”user“`. We can represent that using the `path` and `segment` functions, along with the `string` combinator.
153
+
- The `Post` constructor takes two arguments: a string `Username` and an integer `PostId`. We’ll need to use the `product` function to put two dynamic segments together.
154
+
- The `Feed` constructor should only match a string constant in the path, `”feed”`. We can represent that with the `path` function and `noArgs`.
150
155
151
156
```purescript
152
157
route = root $ sum
153
158
{ "Root": noArgs
154
159
, "Profile": path "user" (string segment)
155
-
, "Post":
156
-
product
160
+
, "Post":
161
+
product
157
162
(path "user" (string segment))
158
163
(path "post" (int segment))
159
164
, "Feed": path "feed" noArgs
@@ -177,6 +182,7 @@ In fact, when we’re matching string constants, we can omit the call to `path`
177
182
```
178
183
179
184
## Example: Working with optional or required query params
185
+
180
186
Users need to be able to search their feeds. This information will come via query parameters, which will be optional. We haven’t dealt with optional segments or query params so far, but they’re easy to add.
181
187
182
188
First, let’s adjust our route type so that it can accommodate query parameters. Query parameters have a key:value pairing, so it’s typical to represent them with a record type.
@@ -189,7 +195,7 @@ data Route
189
195
| Feed { search :: Maybe String }
190
196
```
191
197
192
-
We’ll have to update our codec so that `Feed` takes a record as an argument. We can do this manually with the `record` function and its `:=` operator, which lets you assign a key in the record to a particular codec. Record keys are type-level strings, so we’ll need to use `SProxy` to create them.
198
+
We’ll have to update our codec so that `Feed` takes a record as an argument. We can do this manually with the `record` function and its `:=` operator, which lets you assign a key in the record to a particular codec. Record keys are type-level strings, so we’ll need to use `Proxy` to create them.
193
199
194
200
Intuitively, we can read the below codec as “Match `”feed”` and then, if it exists, a query parameter with the key “search”, storing its value at the key “search” in the output record.” This time, we'll use the `optional` combinator to represent an optional value:
This explicit record creation can be done any time you have a record in your route type. However, using a record for query parameters is common enough that this library exports a helper function, `params`, which lets you just provide a record of codecs where the record keys are treated as the query param keys, too. There's also an operator version of `params`, `(?)`. We could rewrite our above codec using this helper function:
@@ -226,6 +232,7 @@ route = root $ sum
226
232
```
227
233
228
234
## Example: Defining a new codec for custom data
235
+
229
236
Unfortunately, our route data type is not as type-safe as we’d like it to be. We aren’t really parsing just string and ints — we’re dealing with `Username`s and `PostId`s. In addition, we’ve had a last-minute request to allow users to choose how to sort posts in their feed. We’ll need a custom data type for that, too.
230
237
231
238
Our codec can easily handle our custom data types. We just have to make our own combinator that describes how to transform to and from a string. In fact, the primitive combinators we saw before (`int`, `boolean`, `string`, `optional`, etc.) are all built using a helper function, `as`, which we can leverage as well.
@@ -235,9 +242,9 @@ Our codec can easily handle our custom data types. We just have to make our own
235
242
-- some types to how you will almost always use them in practice.
236
243
as
237
244
:: forall a
238
-
. (a -> String)
239
-
-> (String -> Either String a)
240
-
-> RouteDuplex' String
245
+
. (a -> String)
246
+
-> (String -> Either String a)
247
+
-> RouteDuplex' String
241
248
-> RouteDuplex a
242
249
```
243
250
@@ -332,6 +339,7 @@ route = root $ sum
332
339
```
333
340
334
341
## Example: Composing codecs to represent CRUD operations
342
+
335
343
We’ve seen the `RouteDuplex’ a` type all over the place, whether to represent a small codec for integers or strings or a larger one for our complex sum type. We can create codecs of any size and compose them into larger structures. Let’s walk through an example by extending our routing data type to accommodate create, read, and update operations for posts in our system.
336
344
337
345
First, we’ll define a data type to represent creating, reading, and updating a resource dependent on some kind of identifier, `a`:
Next, we’ll again use the `sum` function to write a codec for this sum type. We don’t know how to handle `a`, so we’ll accept a codec to handle it as an argument. We’d like to handle three cases:
349
357
350
-
*`/` should represent creation
351
-
*`/:id` should represent reading
352
-
*`/edit/:id` should represent updating
358
+
-`/` should represent creation
359
+
-`/:id` should represent reading
360
+
-`/edit/:id` should represent updating
353
361
354
362
Exactly the same way we wrote a codec for our `Route` type we can write one for our new `CRU` type:
355
363
@@ -386,6 +394,7 @@ We've developed a capable parser and printer for our route data type. To be usef
386
394
We'll use the library to handle hashes and pushState, but rather than use their parser combinators, we'll provide our own, custom parser -- our codec.
387
395
388
396
First, we'll choose the `matchesWith` function that fits our use case:
0 commit comments