Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,79 @@ route = root $ sum
}
```

## Example: Using Variant codecs to represent polymorphic CRUD operations

In the previous example we’ve seen how to compose codecs in CRUD operations, but the route data types for those operations were fixed, closed; and yet, in most cases, resources in an application need to support different CRUD operations or only a subset of those.

In order to solve this problem, we may use the `Variant` data type from `Data.Variant`. This library exports codecs for polymorphic variants: `variant` and `vcase` (and its operator alias `%=`). The API for these combinators follows the idea of `record` and `prop` seen previously.

In this example we’ll model the same `Post` codec from the previous example, supporting _create_, _read_ and _update_ operations, as well as a `User` codec that supports only the _read_ and _update_ operations, with the caveat that the update operation for users should not take an argument, as a user should only be able to update their own data, and not that of others.

So a complete description of the routes for posts is:

- `/` should represent creation
- `/:id` should represent reading
- `/edit/:id` should represent updating

And for users:

- `/:id` should represent reading
- `/edit` should represent updating

Let’s first create some standard type aliases for common CRUD operations that may be used with `Variant` first:

```purescript
type Create r = (create :: Unit | r)
type Read a r = (read :: a | r)
type Update a r = (update :: a | r)
```

We can use these type aliases to build the parts of the `Route` data type that describe the user and post route schemes. In this example, the `+` type operator from `Type.Row` is used (from the `purescript-typelevel-prelude` package) for extra syntactic sugar:

```purescript
data Route
= ...
| User (Variant (Read Username + Update Unit + ()))
| Post Username (Variant (Create + Read PostId + Update PostId + ()))
```

Next, we can create some helper functions for defining codecs for these common operations using `vcase`:

```purescript
create :: forall r. Lacks "create" r => RouteDuplex' (Variant r) -> RouteDuplex' (Variant (Create r))
create = vcase (Proxy :: _ "create") (pure unit)

read :: forall a r. Lacks "read" r => RouteDuplex' a -> RouteDuplex' (Variant r) -> RouteDuplex' (Variant (Read a r))
read = vcase (Proxy :: _ "read")

update :: forall a r. Lacks "update" r => RouteDuplex' a -> RouteDuplex' (Variant r) -> RouteDuplex' (Variant (Update a r))
update = vcase (Proxy :: _ "update")
```

And finally, we can use `variant` and the helper codecs we’ve just defined together with `postId` and `uname` to produce the larger `Route` codec:

```purescript
route = root $ sum
{ ...
, "User":
path "user"
$ variant
# read uname
# update (pure unit)
, "Post":
"user"
/ uname
/ path "post"
( variant
# create
# read postId
# update postId
)
}
```

It’s important to note here that the read and update routes for users may collide. To solve this ambiguity, we had to define the Variant parser for users in the correct order: `update` takes priority over `read` as it is applied later. To learn more read ["About ordering" section](#about-ordering)

## Example: Running our codec with `purescript-routing`

We've developed a capable parser and printer for our route data type. To be useful, though, we'll want to use our parser along with a library that handles hash-based or pushState routing for us. The most common choice is the `purescript-routing` library. If you aren't familiar with how the library works, [consider skimming the official guide](https://github.com/slamdata/purescript-routing/blob/v8.0.0/GUIDE.md).
Expand Down Expand Up @@ -422,3 +495,41 @@ To perform your routing effects, provide your custom callback function:
canceller <- matchesWith (parse route) \old new -> do
... your routing effects, called every time the route changes ...
```

# About ordering

### For `variant` and `vcase` ORDERING OF FUNCTIONS is important

1. `(_x %= segment) ((_y %= segment) variant)` the `_x` will be tried first (it always from left to right)
2. `(_y %= segment) ((_x %= segment) variant)` the `_y` will be tried first (it always from left to right)

Therefore, strict parsers should start on left.

Example:

1. `(_x %= pure unit) ((_y %= segment) variant)` - bad, because `_x` will always succeed.
2. `(_y %= segment) ((_x %= pure unit) variant)` - ok.

### For `sum` ORDERING OF A CONSTRUCTORS is important

1. `data ABRoute = A String | B String; RDG.sum { "A": segment, "B" segment }` the `A` constructor will always be tried first (because generated by Generic class)
2. `data ABRoute = B String | A String; RDG.sum { "A": segment, "B" segment }` the `B` constructor will always be tried first

Therefore, strict parsers should start on leading constructors.

Example:

1. `data ABRoute = A Unit | B String; RDG.sum { "A": pure unit, "B": segment }` - bad, because `A` will always succeed.
2. `data ABRoute = B String | A Unit; RDG.sum { "A": pure unit, "B": segment }` - ok.

### For `vmatch` NAMES OF KEYS is important

1. `vmatch { x: segment, y: segment }` the `x` will always be tried first
2. `vmatch { y: segment, x: segment }` the `x` will always be tried first too (ordering doesnt matter)

Therefore, strict parsers should start on leading letters.

Example:

1. `vmatch { x: pure unit, y: segment }` - bad, because `x` will always succeed.
2. `vmatch { r2: pure unit, r1: segment }` - ok.
1 change: 1 addition & 0 deletions spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
, "record"
, "strings"
, "tuples"
, "variant"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
Expand Down
Loading