1
1
mirror of https://github.com/srid/ema.git synced 2024-09-11 15:05:23 +03:00

docs: edit guide -> route

This commit is contained in:
Sridhar Ratnakumar 2022-08-12 10:57:00 -04:00
parent f4ab820b61
commit 721070ebb5
8 changed files with 135 additions and 44 deletions

View File

@ -6,8 +6,9 @@ order: 3
Writing an Ema app is an act in three parts:
1. Define your site **routes** as Haskell ADTs:
- [[route]] and [[generic]]
1. Define your site [[route]] as Haskell ADTs:
- Derive [[class]] for it
- Optionally via [[generic]]
2. Define your site **data model** as a Haskell record:
- [[model]] and [[dynamic]]
3. Connect it all using `EmaSite`

View File

@ -6,7 +6,7 @@ order: 3
A "model", in an Ema app, is a type that is required to render the site. The model is also used, optionally, to encode the [[route|route types]].
Taking the blog site example from [[route]], our model would look something like the following:
Taking the blog site example from [[example]], our model would look something like the following:
```haskell
data Model = Model
@ -17,7 +17,7 @@ data Model = Model
Models impact two places:
1. `RouteModel` of `IsRoute` ([[route]]) associates a route type with a model type. This enables [[prism]] (and `routeUniverse`) to accept that model value as an argument.
1. `RouteModel` of [[class]] associates a route type with a model type. This enables [[prism]] (and `routeUniverse`) to accept that model value as an argument.
2. In [[site]] typeclass, `siteInput` can now return this model value (and it can be time-varying if using a [[dynamic]]), as well as `siteOutput` can take the model value so as to render the site based on it.
## Useful libraries

View File

@ -4,32 +4,12 @@ order: 1
# Route type
[Algebraic datatypes](https://en.wikipedia.org/wiki/Algebraic_data_type) (ADT) are well suited to model website routes. For example, the following type can be used to model routes in a weblog site with "About" and "Contact" pages:
[Algebraic datatypes](https://en.wikipedia.org/wiki/Algebraic_data_type) (ADT) are well suited to define website routes in Haskell. For example, the following type represents routes in a particular weblog site:
```haskell
data Route
= Route_Index
| Route_About
| Route_Contact
| Route_Blog BlogRoute
![[example]]
data BlogRoute
= BlogRoute_Index
| BlogRoute_Post Slug
As you can see, routes can be *nested*. `BlogRoute` is a *subroute* of the `Route` type. When encoding `Route`, it can delegate to the encoder for `BlogRoute` (inductively).
newtype Slug = Slug { unSlug :: String }
```
## Routes are central to Ema apps
As you can see, routes can be *nested*. `BlogRoute` is a *subroute* of the `Route` type. When encoding `Route`, it can delegate to the encoder for `BlogRoute` (recursively).
## `IsRoute`
To make the above type an Ema route, you will define an `IsRoute` instance for it. The principle functionality of this typeclass is to provide a [[prism]] (`Prism' FilePath r`).
1. `RouteModel` is used to specify the value ([[model]]) that is used for encoding routes (as defined by `routePrism`). In a real site, this would be a type that contains your blog posts.
2. `routePrism` gives us an optics prism. See [[prism]] for details.
3. `routeUniverse` simply returns a list of routes to statically generate.
### Generic deriving
You do not always have to write `IsRoute` by hand. See [[generic]] for details.
An Ema app is defined by its route type. All routes must be an instance of the [[class]] class, which provides a route encoder (see [[prism]]) that is used to convert to and from the corresponding `.html` filepaths. This instance can be hand-written or [[generic|derived generically]].

34
docs/guide/route/class.md Normal file
View File

@ -0,0 +1,34 @@
---
order: -1
---
# `IsRoute`
The `IsRoute` typeclass is used to mainly define a [[prism]]-based encoding for your route types. In other words, deriving `IsRoute` for the example route,
![[example]]
enables us to convert between `Route_Blog BlogRoute_Index` and `/blog/index.html` and vice versa (as shown in [[01-routes|the tutorial]]).
## Explanation
`IsRoute` is defined as:
```haskell
class IsRoute r where
-- (1)
type RouteModel r :: Type
-- (2)
routePrism :: RouteModel r -> Prism_ FilePath r
-- (3)
routeUniverse :: RouteModel r -> [r]
```
1. `RouteModel` is (optionally) used to specify the [[model|value]] that is used for encoding routes (as defined by `routePrism`).
1. For the blog route example above, this maybe be a type that contains your blog posts. It can also be `()` if you do not care about it.
2. `routePrism` gives us an optics prism (see [[prism]]) that can be used to encode and decode between the route type and the `.html` filepath.
3. `routeUniverse` simply returns a list of routes to statically generate.
## Generic deriving
`IsRoute` may be derived genericallly; see [[generic]].

View File

@ -0,0 +1,22 @@
---
order: 100
---
# Example route
```haskell
-- An example of nested routes
-- Route's expected encoding is given as a comment.
data Route
= Route_Index -- index.html
| Route_About -- about.html
| Route_Contact -- contact.html
| Route_Blog BlogRoute -- blog/<BlogRoute>
data BlogRoute
= BlogRoute_Index -- index.html
| BlogRoute_Post Slug -- post/<Slug>
newtype Slug = Slug { unSlug :: String }
```

View File

@ -4,9 +4,13 @@ order: 2
# Generic deriving
`IsRoute` can be derived generically using `DerivingVia`.
[[class]] can be derived generically using `DerivingVia`.
Let's see how it looks using the blog website routes from [[route]]. Typically, the terminal sub-routes will require a hand-written instance. For eg., the `Slug` type will need a `IsRoute` instance as follows:
Let's see how it looks using the blog website routes (shown below).
![[example]]
Typically, the terminal sub-routes will require a hand-written instance. For eg., the `Slug` type will need a `IsRoute` instance as follows:
```haskell
instance IsRoute Slug where
@ -15,7 +19,7 @@ instance IsRoute Slug where
routeUniverse () = []
```
And the higher level routes can be derived automatically using generics via `DerivingVia`, for instance:
The higher level routes (`BlogRoute` and `Route`) can be derived automatically using generics via `DerivingVia`, for instance:
```haskell
data BlogRoute
@ -57,15 +61,47 @@ deriveIsRoute ''Route [t|'[WithModel ()]|]
The TH way has better compiler error messages due to the use of standalone deriving.
## `HasSubRoutes`: `FileRoute` and `FolderRoute`
## How generic deriving works
The `WithSubRoutes` option to `GenericRoute` can be powerful if you want to specify a custom encoding in the generic deriving (but without needing to hand-write encoders and decoders). `FileRoute` can be used to provide a specific filepath for a route constructor without arguments, and `FolderRoute` can do the same for a route constructor with an unary argumenmt. In GHC 9.2+, `WithSubRoutes` is generically determined in this manner. A constructor like `Route_Blog BlogRoute` automatically expands to `FolderRoute "blog" Slug`.
Ema uses [`generics-sop`](https://hackage.haskell.org/package/generics-sop). The implementation is delegated to `Ema.Route.Lib.Multi.MultiRoute`, which is a generic route type based on `NS` and `NP` from `sop-core`. Thus, much of generics machinary involves converting user's route type to `MultiRoute`; to do this, we must derive instances for `HasSubRoutes` and `HasSubModels`.
You can use any arbrirary type as long as their generic representations are isomorphic (per the `GIsomorphic` class). In effect, `WithSubRoutes` enables "deriving [HasSubRoutes] via" the specified isomorphic route constructor representations.
### `HasSubRoutes`: `FileRoute` and `FolderRoute`
## `HasSubModel`
`HasSubRoutes` gives us an isomorphism between the route type's sum constructors and `FileRoute` or `FolderRoute` types. For example, the `BlogRoute` type above is converted to:
The `HasSubModel` option to `GenericRoute` is relevant when your subroutes specify a [[model]] *different* to the top-level route (see Ex03_Store.hs in Ema source tree for an example). It tells the generic deriving system how to "break" the top-level model into submodels corresponding to the subroutes. Ema's generic deriving mechanism relies on [`HasAny`](https://hackage.haskell.org/package/generic-optics-2.2.1.0/docs/Data-Generics-Product-Any.html) from `generic-optics` for large part to determine this automatically, and the `WithSubModels` option can be used to explicitly specify the lenses if there are ambiguties. You can of course also derive `HasSubModels` manually.
```haskell
type BlogRoute' =
MultiRoute
'[ FileRoute "index.html"
, FolderRoute "blog" Slug
]
```
Notice how the "shape" of the two types match. Constructors with zero arguments (`BlogRoute_Index`) are isomorphic to `FileRoute`, whereas constructors with one argument (`BlogRoute_Post Slug`) are isomorphic to `FolderRoute a` (where `a` is that argument type). Route constructors cannot not have more than one argument.
#### `WithSubRoutes`
The `WithSubRoutes` option to `GenericRoute` can be powerful if you want to use something other than `FileRoute`/`FolderRoute` in the generic deriving (but without needing to hand-write encoders and decoders).
- `FileRoute` can be used to provide a specific filepath for a route constructor without arguments
- `FolderRoute` can do the same for a route constructor with an unary argumenmt.
In GHC 9.2+, `WithSubRoutes` is generically determined in this manner. A constructor like `Route_Blog BlogRoute` automatically expands to `FolderRoute "blog" Slug`.
You can use any arbitrary type as long as their generic representations are isomorphic (per the `GIsomorphic` class). In effect, `WithSubRoutes` enables "deriving [HasSubRoutes] via" the specified isomorphic route constructor representations.
### `HasSubModels`
`HasSubModels` does for `RouteModel` what `HasSubRoutes` does for route constructors. In many simple cases your sub-routes share the same model type as the larger route, but in some cases you want to have a different model type for each sub-route.
To generically achieve this, we want to be able to extract the sub-model from the larger model. `HasSubModel` provides this functionality via [`HasAny`](https://hackage.haskell.org/package/generic-optics-2.2.1.0/docs/Data-Generics-Product-Any.html).
#### `WithSubModels`
Like, `WithSubRoutes` you can explicitly specify the sub-model to extract (via `HasAny` for instance) if there are ambiguities.
See Ex03_Store.hs in Ema source tree for an example.
## Custom generic options

View File

@ -1,8 +1,12 @@
# Route Prism
Route types are associated with an [optics `Prism'`](https://hackage.haskell.org/package/optics-core-0.4.1/docs/Optics-Prism.html#t:Prism-39-) (`Prism' FilePath r`) that in turn can be used to encode and decode route values to and from URLs or filepaths.
Deriving [[class]] for our route types gives us a `routePrism` function that returns (effectively[^prism]) a `Prism' FilePath r` (from [optics-core `Prism'`](https://hackage.haskell.org/package/optics-core-0.4.1/docs/Optics-Prism.html#t:Prism-39-)) that in turn can be used to encode and decode route values to and from URLs or filepaths.
Here is a naive implementation for `BlogRoute` from the example in [[route]]:
Let's consider the example route,
![[example]]
Here is a naive implementation of [[class]] for the `BlogRoute` above:
```haskell
instance IsRoute BlogRoute where
@ -21,10 +25,24 @@ instance IsRoute BlogRoute where
routeUniverse () = []
```
`routePrism` can be generically determined for routes with "standard shapes"; see [[generic]].
In GHCi you can play with this prism as,
### optics-core
```haskell
ghci> let rp = routePrism @BlogRoute ()
ghci> preview rp "posts/foo.html"
Just (BlogRoute_Post (Slug "foo"))
ghci> review rp $ BlogRoute_Post (Slug "foo")
"posts/foo.html"
```
Ema uses the `optics-core` package (instead of `lens`), which provides [the `Prism'` type](https://hackage.haskell.org/package/optics-core-0.4.1/docs/Optics-Prism.html#t:Prism-39-). You can think of `routePrism` as returning a `Prism' FilePath r` in effect -- allowing us to convert between a route value and a filepath. In reality, `routePrism` must return a `Prism_` type that Ema provides.
Ema provides a `routeUrl` function that converts this filepath to an URL.
`Prism_` is isomorphic to `Prism'` -- with conversion functions `toPrism_` and `fromPrism_`. The typeclass is obliged to use `Prism_` instead of `Prism'` due to a Haskell limitation in regards to DerivingVia and coercions (see [details here](https://stackoverflow.com/q/71489589/55246)).
## Generic prism
`routePrism` can also be generically determined for routes with "standard shapes" (both `Route` and `BlogRoute` above); see [[generic]].
[^prism]:
In reality, `routePrism` must return a `Prism_` type that Ema provides.
`Prism_` is isomorphic to `Prism'` -- with conversion functions `toPrism_` and `fromPrism_`. The typeclass is obliged to use `Prism_` instead of `Prism'` due to a Haskell limitation in regards to DerivingVia and coercions (see [details here](https://stackoverflow.com/q/71489589/55246)).

View File

@ -12,7 +12,7 @@ Until 1.0 is released, newer releases of Ema may see breaking or significant cha
First, read [[tutorial]] to get a taste of the new API. Then, in your old Ema site:
- Extract `encodeRoute` and `decodeRoute` (of `Ema` instance) into `IsRoute` instances (see [[route]])
- Extract `encodeRoute` and `decodeRoute` (of `Ema` instance) into an [[class]] instance.
- Change `Ema` to `EmaSite`, and define `siteInput` (what used to be in argument to `runEma`) and `siteOutput` (the render argument to `runEma`). You will want to use [[dynamic]] instead of `LVar`.
- `Ema.routeUrl`: change to accept the [[prism]] that is now passed to `siteOutput`