Skip to content

Commit 4f59293

Browse files
Allow any proxy type, not just SProxy (#24)
1 parent 246dd2a commit 4f59293

File tree

5 files changed

+55
-45
lines changed

5 files changed

+55
-45
lines changed

README.md

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
Simple bidirectional parser/printers for your routing data types.
22

33
# Why?
4+
45
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.
56

67
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).
78

89
`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.
910

1011
## 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).
1214

1315
1. Write a data type to represent our two routes, deriving `Generic`.
1416
2. Build a codec using generics and combinators from `Routing.Duplex`
@@ -46,8 +48,8 @@ print route $ Profile "jake-delhomme"
4648
> "/profile/jake-delhomme"
4749
```
4850

49-
5051
# How to use this library
52+
5153
`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`:
5254

5355
```purescript
@@ -62,14 +64,16 @@ type RouteDuplex' a = RouteDuplex a a
6264

6365
This library exports a number of helper functions and combinators for constructing this codec with minimal boilerplate, mostly concentrated in two modules:
6466

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.
6870

6971
# Examples
72+
7073
We’ll explore several practical examples of this library in practice while building a real-world routing data type and codec.
7174

7275
## Example: Writing a codec for a sum type
76+
7377
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:
7478

7579
```purescript
@@ -95,7 +99,7 @@ Next, we need to represent the ability to parse static and dynamic segments from
9599
We can use the `path`, `segment`, and `param` helper functions to capture these static and dynamic segments.
96100

97101
```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
99103
-- /feed path, use `path "feed" ...` where `...` represents further
100104
-- segments.
101105
path :: forall a b. String -> RouteDuplex a b -> RouteDuplex a b
@@ -122,13 +126,14 @@ int :: RouteDuplex' String -> RouteDuplex' Int
122126
optional :: forall a b. RouteDuplex a b -> RouteDuplex (Maybe a) (Maybe b)
123127
```
124128

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!
126130

127131
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
132137

133138
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`.
134139

@@ -143,17 +148,17 @@ route = root $ sum
143148

144149
`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:
145150

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`.
150155

151156
```purescript
152157
route = root $ sum
153158
{ "Root": noArgs
154159
, "Profile": path "user" (string segment)
155-
, "Post":
156-
product
160+
, "Post":
161+
product
157162
(path "user" (string segment))
158163
(path "post" (int segment))
159164
, "Feed": path "feed" noArgs
@@ -177,6 +182,7 @@ In fact, when we’re matching string constants, we can omit the call to `path`
177182
```
178183

179184
## Example: Working with optional or required query params
185+
180186
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.
181187

182188
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
189195
| Feed { search :: Maybe String }
190196
```
191197

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.
193199

194200
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:
195201

@@ -199,7 +205,7 @@ route = root $ sum
199205
, "Feed": path "feed" (record # _search := optional (param "search"))
200206
}
201207
where
202-
_search = SProxy :: SProxy "search"
208+
_search = Proxy :: Proxy "search"
203209
```
204210

205211
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
226232
```
227233

228234
## Example: Defining a new codec for custom data
235+
229236
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.
230237

231238
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
235242
-- some types to how you will almost always use them in practice.
236243
as
237244
:: forall a
238-
. (a -> String)
239-
-> (String -> Either String a)
240-
-> RouteDuplex' String
245+
. (a -> String)
246+
-> (String -> Either String a)
247+
-> RouteDuplex' String
241248
-> RouteDuplex a
242249
```
243250

@@ -332,6 +339,7 @@ route = root $ sum
332339
```
333340

334341
## Example: Composing codecs to represent CRUD operations
342+
335343
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.
336344

337345
First, we’ll define a data type to represent creating, reading, and updating a resource dependent on some kind of identifier, `a`:
@@ -347,9 +355,9 @@ derive instance genericCRU :: Generic (CRU a) _
347355

348356
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:
349357

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
353361

354362
Exactly the same way we wrote a codec for our `Route` type we can write one for our new `CRU` type:
355363

@@ -386,6 +394,7 @@ We've developed a capable parser and printer for our route data type. To be usef
386394
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.
387395

388396
First, we'll choose the `matchesWith` function that fits our use case:
397+
389398
- [`Routing.Hash.matchesWith`](https://pursuit.purescript.org/packages/purescript-routing/8.0.0/docs/Routing.Hash#v:matchesWith)
390399
- [`Routing.PushState.matchesWith`](https://pursuit.purescript.org/packages/purescript-routing/8.0.0/docs/Routing.PushState#v:matchesWith)
391400

src/Routing/Duplex.purs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ import Data.Maybe (Maybe)
3939
import Data.Profunctor (class Profunctor)
4040
import Data.String (Pattern(..))
4141
import Data.String as String
42-
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
42+
import Data.Symbol (class IsSymbol, reflectSymbol)
4343
import Prim.Row as Row
4444
import Prim.RowList (RowList, class RowToList, Cons, Nil)
4545
import Record as Record
4646
import Routing.Duplex.Parser (RouteParser)
4747
import Routing.Duplex.Parser as Parser
4848
import Routing.Duplex.Printer (RoutePrinter)
4949
import Routing.Duplex.Printer as Printer
50-
import Type.Data.RowList (RLProxy(..))
50+
import Type.Proxy (Proxy(..))
5151

5252
-- | The core abstraction of this library. The values of this type can be used both for parsing
5353
-- | values of type `o` from `String` as well as printing values of type `i` into `String`.
@@ -303,9 +303,9 @@ string = identity
303303
-- | ```purescript
304304
-- | date =
305305
-- | record
306-
-- | # prop (SProxy :: _ "year") (int segment)
307-
-- | # prop (SProxy :: _ "month") (int segment)
308-
-- | # prop (SProxy :: _ "day") (int segment)
306+
-- | # prop (Proxy :: _ "year") (int segment)
307+
-- | # prop (Proxy :: _ "month") (int segment)
308+
-- | # prop (Proxy :: _ "day") (int segment)
309309
-- |
310310
-- | parse (path "blog" date) "blog/2019/1/2" ==
311311
-- | Right { year: 2019, month: 1, day: 2 }
@@ -314,12 +314,12 @@ record :: forall r. RouteDuplex r {}
314314
record = RouteDuplex mempty (pure {})
315315

316316
-- | See `record`.
317-
prop :: forall sym a b r1 r2 r3 rx.
317+
prop :: forall proxy sym a b r1 r2 r3 rx.
318318
IsSymbol sym =>
319319
Row.Cons sym a rx r1 =>
320320
Row.Cons sym b r2 r3 =>
321321
Row.Lacks sym r2 =>
322-
SProxy sym ->
322+
proxy sym ->
323323
RouteDuplex a b ->
324324
RouteDuplex { | r1 } { | r2 } ->
325325
RouteDuplex { | r1 } { | r3 }
@@ -351,11 +351,11 @@ instance routeDuplexParams ::
351351
RouteDuplexParams r1 r2 where
352352
params r =
353353
record
354-
# buildParams (RLProxy :: RLProxy rl) r
354+
# buildParams (Proxy :: Proxy rl) r
355355

356356
class RouteDuplexBuildParams (rl :: RowList Type) (r1 :: Row Type) (r2 :: Row Type) (r3 :: Row Type) (r4 :: Row Type) | rl -> r1 r2 r3 r4 where
357357
buildParams ::
358-
RLProxy rl ->
358+
Proxy rl ->
359359
{ | r1 } ->
360360
RouteDuplex { | r2 } { | r3 } ->
361361
RouteDuplex { | r2 } { | r4 }
@@ -372,9 +372,9 @@ instance buildParamsCons ::
372372
buildParams _ r prev =
373373
prev
374374
# prop sym ((Record.get sym r) (param (reflectSymbol sym)))
375-
# buildParams (RLProxy :: RLProxy rest) r
375+
# buildParams (Proxy :: Proxy rest) r
376376
where
377-
sym = SProxy :: SProxy sym
377+
sym = Proxy :: Proxy sym
378378

379379
instance buildParamsNil ::
380380
RouteDuplexBuildParams Nil r1 r2 r3 r3 where

src/Routing/Duplex/Generic.purs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import Prelude
55
import Control.Alt ((<|>))
66
import Data.Generic.Rep (class Generic, Argument(..), Constructor(..), NoArguments(..), Product(..), Sum(..), from, to)
77
import Data.Profunctor (dimap)
8-
import Data.Symbol (class IsSymbol, SProxy(..))
8+
import Data.Symbol (class IsSymbol)
99
import Prim.Row as Row
1010
import Record as Record
1111
import Routing.Duplex (RouteDuplex(..), RouteDuplex', end)
12+
import Type.Proxy (Proxy(..))
1213

1314
sum :: forall a rep r.
1415
Generic a rep =>
@@ -45,7 +46,7 @@ instance gRouteConstructor ::
4546
RouteDuplex enc' dec' =
4647
end
4748
$ (gRouteDuplexCtr :: RouteDuplex' a -> RouteDuplex' b)
48-
$ Record.get (SProxy :: SProxy sym) r
49+
$ Record.get (Proxy :: Proxy sym) r
4950
enc (Constructor a) = enc' a
5051
dec = Constructor <$> dec'
5152

test/Main.purs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import Data.Either (Either(..))
66
import Data.Generic.Rep (class Generic)
77
import Data.Show.Generic (genericShow)
88
import Data.String.Gen (genAlphaString)
9-
import Data.Symbol (SProxy(..))
109
import Effect (Effect)
1110
import Routing.Duplex (RouteDuplex', flag, int, param, parse, print, record, rest, root, segment, string, (:=))
1211
import Routing.Duplex.Generic (noArgs)
@@ -15,6 +14,7 @@ import Routing.Duplex.Generic.Syntax ((/), (?))
1514
import Test.QuickCheck (Result(..), arbitrary, quickCheckGen, (===))
1615
import Test.QuickCheck.Gen (Gen, arrayOf, chooseInt)
1716
import Test.Unit (combinatorUnitTests)
17+
import Type.Proxy (Proxy(..))
1818

1919
data TestRoute
2020
= Root
@@ -39,8 +39,8 @@ genTestRoute = do
3939
3 -> Bar <$> ({ id: _, search: _ } <$> genAlphaString <*> genAlphaString)
4040
_ -> Baz <$> genAlphaString <*> (arrayOf genAlphaString)
4141

42-
_id = SProxy :: SProxy "id"
43-
_search = SProxy :: SProxy "search"
42+
_id = Proxy :: Proxy "id"
43+
_search = Proxy :: Proxy "search"
4444

4545
route :: RouteDuplex' TestRoute
4646
route =

test/Unit.purs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Test.Assert (assertEqual)
88
import Effect (Effect)
99
import Data.Either (Either(..))
1010
import Data.Maybe (Maybe(..))
11-
import Data.Symbol (SProxy(..))
11+
import Type.Proxy (Proxy(..))
1212

1313
combinatorUnitTests :: Effect Unit
1414
combinatorUnitTests = do
@@ -124,9 +124,9 @@ sort = as sortToString sortFromString
124124
date :: RouteDuplex' { year :: Int, month :: Int, day :: Int }
125125
date =
126126
record
127-
# prop (SProxy :: _ "year") (int segment)
128-
# prop (SProxy :: _ "month") (int segment)
129-
# prop (SProxy :: _ "day") (int segment)
127+
# prop (Proxy :: _ "year") (int segment)
128+
# prop (Proxy :: _ "month") (int segment)
129+
# prop (Proxy :: _ "day") (int segment)
130130

131131
search :: RouteDuplex' { page :: Int, filter :: Maybe String }
132132
search =

0 commit comments

Comments
 (0)