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:
parent
f4ab820b61
commit
721070ebb5
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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
34
docs/guide/route/class.md
Normal 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]].
|
22
docs/guide/route/example.md
Normal file
22
docs/guide/route/example.md
Normal 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 }
|
||||
```
|
@ -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
|
||||
|
||||
|
@ -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)).
|
@ -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`
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user