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
<p><code>Hyper.Routing.TypeLevelRouter</code> provides an API for expressing the web application routes and their characterics as types, much like <ahref="https://haskell-servant.github.io">Servant</a> does. From this type you get static guarantees about having handled all cases, linking only to existing routes. You also get a lot of stuff for free, such as type-safe parameters for handlers and links.</p>
135
-
<p>Let’s say we want to render a <code>User</code> value as HTML on the <code>/</code> path. We start out by declaring the structure of our site:</p>
136
-
<preclass="purescript"><code>newtype User = User { firstName :: String
<p>Let’s say we want to render a home page as HTML. We start out by declaring the endpoint data type <code>Home</code>, and the structure of our site:</p>
140
+
<preclass="purescript"><code>data Home = Home
139
141
140
-
type MySite = Get HTML User</code></pre>
141
-
<p><code>Get HTML User</code> describes a structure with only one endpoint, rendering a User as HTML.</p>
142
-
<p>So where does the User value come from? We provide it using a <em>handler</em>. A handler for <code>MySite</code> would be some value of the following type:</p>
143
-
<preclass="purescript"><code>forall m. Monad m => ExceptT RoutingError m User</code></pre>
144
-
<p>We can construct such a value using <code>pure</code> and a <code>User</code> value:</p>
145
-
<preclass="purescript"><code>root = pure (User { firstName: "John", lastName: "Bonham" })</code></pre>
146
-
<p>Nice! But what comes out on the other end? We need something that renders the <code>User</code> value as HTML. The <code>MimeRender</code> type class encapsulates this concept. We provide an instance for <code>User</code> and the <code>HTML</code> content type.</p>
147
-
<preclass="purescript"><code>instance mimeRenderUserHTML :: MimeRender User HTML String where
148
-
mimeRender _ (User { firstName, lastName }) =
149
-
asString $
150
-
p [] [ text firstName
151
-
, text " "
152
-
, text lastName
153
-
]</code></pre>
142
+
type MySite = Get HTML Home</code></pre>
143
+
<p><code>Get HTML Home</code> is a routing type with only one endpoint, rendering a <code>Home</code> value as HTML. So where does the <code>Home</code> value come from? We provide it using a <em>handler</em>. A handler for <code>MySite</code> would be some value of the following type:</p>
144
+
<preclass="purescript"><code>forall m. Monad m => ExceptT RoutingError m Home</code></pre>
145
+
<p>We can construct such a value using <code>pure</code> and a <code>Home</code> value:</p>
146
+
<preclass="purescript"><code>home = pure Home</code></pre>
147
+
<p>Nice! But what comes out on the other end? We need something that renders the <code>Home</code> value as HTML. By providing an instance of <code>EncodeHTML</code> for <code>Home</code>, we instruct the endpoint how to render.</p>
148
+
<preclass="purescript"><code>instance encodeHTMLHome :: EncodeHTML Home where
149
+
encodeHTML Home =
150
+
p [] [ text "Welcome to my site!" ]</code></pre>
154
151
<p>We are getting ready to create the server. First, we need a value-level representation of the <code>MySite</code> type, to be able to pass it to the <code>router</code> function. For that we use <ahref="https://pursuit.purescript.org/packages/purescript-proxy/1.0.0/docs/Type.Proxy">Proxy</a>. Its documentation describes it as follows:</p>
155
152
<blockquote>
156
153
<p>The Proxy type and values are for situations where type information is required for an input to determine the type of an output, but where it is not possible or convenient to provide a value for the input.</p>
157
154
</blockquote>
158
155
<p>We create a top-level definition of the type <code>Proxy MySite</code> with the value constructor <code>Proxy</code>.</p>
<p>We pass the proxy, our handler, and the <code>onRoutingError</code> function for cases where no route matched the request.</p>
158
+
<p>We pass the proxy, our handler, and the <code>onRoutingError</code> function for cases where no route matched the request, to the <code>router</code> function.</p>
162
159
<preclass="purescript"><code>onRoutingError status msg =
<p>Real-world servers often need more than one endpoint. Let’s define a router for an application that shows a home page with links, a page listing users, and a page rendering a specific user.</p>
177
+
<preclass="purescript"><code>data Home = Home
178
+
179
+
data AllUsers = AllUsers (Array User)
180
+
181
+
newtype User = User { id :: Int, name :: String }
182
+
183
+
type MyOtherSite =
184
+
Get HTML Home
185
+
:<|> "users" :/ Get HTML AllUsers
186
+
:<|> "users" :/ Capture "user-id" Int :> Get HTML User
187
+
188
+
otherSite :: Proxy MyOtherSite
189
+
otherSite = Proxy</code></pre>
190
+
<p>Let’s go through the new constructs used:</p>
191
+
<ul>
192
+
<li><code>:<|></code> is a type operator that separates <em>alternatives</em>. A router for this type will try each route in order until one matches.</li>
193
+
<li><code>:/</code> separates a literal path segment and the rest of the endpoint type.</li>
194
+
<li><code>Capture</code> takes a descriptive string and a type. It takes the next available path segment and tries to convert it to the given type. Each capture in an endpoint type corresponds to an argument in the handler function.</li>
195
+
<li><code>:></code> separates a an endpoint modifier, like <code>Capture</code>, and the rest of the endpoint type.</li>
196
+
</ul>
197
+
<p>We define handlers for our routes as regular functions on the specified data types, returning <code>ExceptT RoutingError m a</code> values, where <code>m</code> is the monad of our middleware, and <code>a</code> is the type to render for the endpoint.</p>
198
+
<preclass="purescript"><code>home :: forall m. Monad m => ExceptT RoutingError m Home
199
+
home = pure Home
200
+
201
+
allUsers :: forall m. Monad m => ExceptT RoutingError m AllUsers
202
+
allUsers = AllUsers <$> getUsers
203
+
204
+
getUser :: forall m. Monad m => Int -> ExceptT RoutingError m User
<p>As in the single-endpoint example, we want to render as HTML. Let’s create instances for our data types. Notice how we can create links between routes in a type-safe manner.</p>
216
+
<preclass="purescript"><code>instance encodeHTMLHome :: EncodeHTML Home where
217
+
encodeHTML Home =
218
+
case linksTo otherSite of
219
+
_ :<|> allUsers' :<|> _ ->
220
+
p [] [ text "Welcome to my site! Go check out my "
221
+
, linkTo allUsers' [ text "Users" ]
222
+
, text "."
223
+
]
224
+
225
+
instance encodeHTMLAllUsers :: EncodeHTML AllUsers where
226
+
encodeHTML (AllUsers users) =
227
+
element_ "div" [ h1 [] [ text "Users" ]
228
+
, ul [] (map linkToUser users)
229
+
]
230
+
where
231
+
linkToUser (User u) =
232
+
case linksTo otherSite of
233
+
_ :<|> _ :<|> getUser' ->
234
+
li [] [ linkTo (getUser' u.id) [ text u.name ] ]
235
+
236
+
instance encodeHTMLUser :: EncodeHTML User where
237
+
encodeHTML (User { name }) =
238
+
h1 [] [ text name ]</code></pre>
239
+
<p>The pattern match on the value returned by <code>linksTo</code> must match the structure of the routing type. We use <code>:<|></code> to pattern match on links. Each matched link will have a type based on the corresponding endpoint. <code>getUser</code> in the previous code has type <code>Int -> URI</code>, while <code>allUsers</code> has no captures and thus has type <code>URI</code>.</p>
240
+
<p>We are still missing <code>getUsers</code>, our source of User values. In a real application it would probably be a database query, but for this example we simply hard-code some famous users of proper instruments.</p>
241
+
<preclass="purescript"><code>getUsers :: forall m. Applicative m => m (Array User)
242
+
getUsers =
243
+
pure
244
+
[ User { id: 1, name: "John Paul Jones" }
245
+
, User { id: 2, name: "Tal Wilkenfeld" }
246
+
, User { id: 3, name: "John Patitucci" }
247
+
, User { id: 4, name: "Jaco Pastorious" }
248
+
]</code></pre>
249
+
<p>Almost done! We just need to create the router, and start a server.</p>
log ("Listening on http://localhost:" <> show port)
262
+
263
+
onRequestError err =
264
+
log ("Request failed: " <> show err)
265
+
266
+
in runServer defaultOptions</code></pre>
267
+
<p>Notice how the composition of handler functions, using the value-level operator <code>:<|></code>, matches the structure of our routing type. If we fail to match the type we get a compile error.</p>
175
268
<h2id="resource-routing">Resource Routing</h2>
176
269
<p><em>This module is <strong>deprecated</strong> in favor of the one described in <ahref="#type-level-routing">Type-Level Routing</a>.</em></p>
177
270
<p><code>Hyper.Routing.ResourceRouter</code> aims to provide type-safe routing, based on REST resources. It should not be possible to link, using an HTML anchor, to a resource in the web application that does not exist, or that does not handle the GET method. Neither should it be possible to create a form that posts to a non-existing resource, or a resource not handling POST requests.</p>
0 commit comments