Skip to content

Commit 62954cf

Browse files
committed
Complete multi-endpoint example in docs
1 parent f5a26c1 commit 62954cf

File tree

11 files changed

+398
-56
lines changed

11 files changed

+398
-56
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ docs/index.html: $(MD_SOURCES) docs/template.html docs/docs.css docs/highlight.j
3434
docs/hyper.pdf: $(MD_SOURCES)
3535
pandoc $(SHARED_PANDOC_OPTIONS) \
3636
-t latex \
37+
--listings \
38+
-H docs/purescript-language.tex \
39+
-H docs/listings.tex \
3740
--latex-engine=xelatex \
3841
"--metadata=subtitle:$(VERSION)" \
3942
-o docs/hyper.pdf \

docs/docs.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@ pre .text {
6565
color: #4d4d4c;
6666
}
6767

68+
ol code,
69+
ul code,
6870
p code {
6971
display: inline-block;
70-
background: #eee;
72+
background: #f5f5f5;
7173
padding: 0 2px;
7274
line-height: 1.3;
7375
}
@@ -118,7 +120,7 @@ body > table {
118120
}
119121

120122
a {
121-
color: #706ec3;
123+
color: #3737c1;
122124
}
123125
a:visited {
124126
color: #80808e;

docs/github.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/hyper.pdf

19.4 KB
Binary file not shown.

docs/index.html

Lines changed: 118 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<meta name="author" content="Oskar Wickström">
88
<title>Hyper</title>
99
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Source+Sans+Pro:400,400i,700" rel="stylesheet">
10-
<link rel="stylesheet" href="tomorrow.min.css">
10+
<link rel="stylesheet" href="github.css">
1111
<!--[if lt IE 9]>
1212
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
1313
<![endif]-->
@@ -17,7 +17,7 @@
1717
<header>
1818
<h1 class="title" itemprop="name">Hyper</h1>
1919
<p class="version" itemprop="version">
20-
Version 2b0b531
20+
Version 4423a79
2121
</p>
2222
<cite class="author" itemprop="author">By Oskar Wickström</cite>
2323
</header>
@@ -33,7 +33,10 @@ <h2 id="contents">Contents</h2>
3333
<li><a href="#response-state-transitions">Response State Transitions</a></li>
3434
</ul></li>
3535
<li><a href="#request-body-parsing">Request Body Parsing</a></li>
36-
<li><a href="#type-level-routing">Type-Level Routing</a></li>
36+
<li><a href="#type-level-routing">Type-Level Routing</a><ul>
37+
<li><a href="#a-single-endpoint-example">A Single-Endpoint Example</a></li>
38+
<li><a href="#routing-multiple-endpoints">Routing Multiple Endpoints</a></li>
39+
</ul></li>
3740
<li><a href="#resource-routing">Resource Routing</a><ul>
3841
<li><a href="#resources">Resources</a></li>
3942
<li><a href="#resource-routers">Resource Routers</a></li>
@@ -132,46 +135,136 @@ <h2 id="request-body-parsing">Request Body Parsing</h2>
132135
<p>More efficient parsers, directly operating on the <code>RequestBody</code>, instead of <code>String</code>, can of course be built as well.</p>
133136
<h2 id="type-level-routing">Type-Level Routing</h2>
134137
<p><code>Hyper.Routing.TypeLevelRouter</code> provides an API for expressing the web application routes and their characterics as types, much like <a href="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-
<pre class="purescript"><code>newtype User = User { firstName :: String
137-
, lastName :: String
138-
}
138+
<h3 id="a-single-endpoint-example">A Single-Endpoint Example</h3>
139+
<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+
<pre class="purescript"><code>data Home = Home
139141

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-
<pre class="purescript"><code>forall m. Monad m =&gt; 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-
<pre class="purescript"><code>root = pure (User { firstName: &quot;John&quot;, lastName: &quot;Bonham&quot; })</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-
<pre class="purescript"><code>instance mimeRenderUserHTML :: MimeRender User HTML String where
148-
mimeRender _ (User { firstName, lastName }) =
149-
asString $
150-
p [] [ text firstName
151-
, text &quot; &quot;
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+
<pre class="purescript"><code>forall m. Monad m =&gt; 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+
<pre class="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+
<pre class="purescript"><code>instance encodeHTMLHome :: EncodeHTML Home where
149+
encodeHTML Home =
150+
p [] [ text &quot;Welcome to my site!&quot; ]</code></pre>
154151
<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 <a href="https://pursuit.purescript.org/packages/purescript-proxy/1.0.0/docs/Type.Proxy">Proxy</a>. Its documentation describes it as follows:</p>
155152
<blockquote>
156153
<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>
157154
</blockquote>
158155
<p>We create a top-level definition of the type <code>Proxy MySite</code> with the value constructor <code>Proxy</code>.</p>
159156
<pre class="purescript"><code>mySite :: Proxy MySite
160157
mySite = Proxy</code></pre>
161-
<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>
162159
<pre class="purescript"><code>onRoutingError status msg =
163160
writeStatus status
164161
&gt;=&gt; contentType textHTML
165162
&gt;=&gt; closeHeaders
166163
&gt;=&gt; respond (maybe &quot;&quot; id msg)
167164

168-
siteRouter = router mySite root onRoutingError</code></pre>
165+
siteRouter = router mySite home onRoutingError</code></pre>
169166
<p>The value returned by <code>router</code> is regular middleware, ready to be passed to a server.</p>
170167
<pre class="purescript"><code>main =
171168
runServer defaultOptions onListening onRequestError {} siteRouter
172169
where
173-
onListening (Port port) = log (&quot;Listening on http://localhost:&quot; &lt;&gt; show port)
174-
onRequestError err = log (&quot;Request failed: &quot; &lt;&gt; show err)</code></pre>
170+
onListening (Port port) =
171+
log (&quot;Listening on http://localhost:&quot; &lt;&gt; show port)
172+
173+
onRequestError err =
174+
log (&quot;Request failed: &quot; &lt;&gt; show err)</code></pre>
175+
<h3 id="routing-multiple-endpoints">Routing Multiple Endpoints</h3>
176+
<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+
<pre class="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+
:&lt;|&gt; &quot;users&quot; :/ Get HTML AllUsers
186+
:&lt;|&gt; &quot;users&quot; :/ Capture &quot;user-id&quot; Int :&gt; 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>:&lt;|&gt;</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>:&gt;</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+
<pre class="purescript"><code>home :: forall m. Monad m =&gt; ExceptT RoutingError m Home
199+
home = pure Home
200+
201+
allUsers :: forall m. Monad m =&gt; ExceptT RoutingError m AllUsers
202+
allUsers = AllUsers &lt;$&gt; getUsers
203+
204+
getUser :: forall m. Monad m =&gt; Int -&gt; ExceptT RoutingError m User
205+
getUser id&#39; =
206+
find userWithId &lt;$&gt; getUsers &gt;&gt;=
207+
case _ of
208+
Just user -&gt; pure user
209+
Nothing -&gt;
210+
throwError (HTTPError { status: statusNotFound
211+
, message: Just &quot;User not found.&quot;
212+
})
213+
where
214+
userWithId (User u) = u.id == id&#39;</code></pre>
215+
<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+
<pre class="purescript"><code>instance encodeHTMLHome :: EncodeHTML Home where
217+
encodeHTML Home =
218+
case linksTo otherSite of
219+
_ :&lt;|&gt; allUsers&#39; :&lt;|&gt; _ -&gt;
220+
p [] [ text &quot;Welcome to my site! Go check out my &quot;
221+
, linkTo allUsers&#39; [ text &quot;Users&quot; ]
222+
, text &quot;.&quot;
223+
]
224+
225+
instance encodeHTMLAllUsers :: EncodeHTML AllUsers where
226+
encodeHTML (AllUsers users) =
227+
element_ &quot;div&quot; [ h1 [] [ text &quot;Users&quot; ]
228+
, ul [] (map linkToUser users)
229+
]
230+
where
231+
linkToUser (User u) =
232+
case linksTo otherSite of
233+
_ :&lt;|&gt; _ :&lt;|&gt; getUser&#39; -&gt;
234+
li [] [ linkTo (getUser&#39; 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>:&lt;|&gt;</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 -&gt; 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+
<pre class="purescript"><code>getUsers :: forall m. Applicative m =&gt; m (Array User)
242+
getUsers =
243+
pure
244+
[ User { id: 1, name: &quot;John Paul Jones&quot; }
245+
, User { id: 2, name: &quot;Tal Wilkenfeld&quot; }
246+
, User { id: 3, name: &quot;John Patitucci&quot; }
247+
, User { id: 4, name: &quot;Jaco Pastorious&quot; }
248+
]</code></pre>
249+
<p>Almost done! We just need to create the router, and start a server.</p>
250+
<pre class="purescript"><code>main =
251+
let otherSiteRouter =
252+
router otherSite (home :&lt;|&gt; allUsers :&lt;|&gt; getUser) onRoutingError
253+
254+
onRoutingError status msg =
255+
writeStatus status
256+
&gt;=&gt; contentType textHTML
257+
&gt;=&gt; closeHeaders
258+
&gt;=&gt; respond (maybe &quot;&quot; id msg)
259+
260+
onListening (Port port) =
261+
log (&quot;Listening on http://localhost:&quot; &lt;&gt; show port)
262+
263+
onRequestError err =
264+
log (&quot;Request failed: &quot; &lt;&gt; show err)
265+
266+
in runServer defaultOptions</code></pre>
267+
<p>Notice how the composition of handler functions, using the value-level operator <code>:&lt;|&gt;</code>, matches the structure of our routing type. If we fail to match the type we get a compile error.</p>
175268
<h2 id="resource-routing">Resource Routing</h2>
176269
<p><em>This module is <strong>deprecated</strong> in favor of the one described in <a href="#type-level-routing">Type-Level Routing</a>.</em></p>
177270
<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>

docs/listings.tex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
\usepackage{upquote}
2+
\usepackage{xcolor}
3+
4+
\def\postbreak{\raisebox{0ex}[0ex][0ex]{\ensuremath{\hookrightarrow\space}}}
5+
6+
\lstset{
7+
columns=fixed,
8+
basewidth=.5em,
9+
basicstyle=\ttfamily,
10+
commentstyle=\color{gray}\textit,
11+
keywordstyle=\color{purple}\ttfamily,
12+
stringstyle=\ttfamily\color{olive},
13+
numberstyle=\color{olive},
14+
typestyle=\color{teal},
15+
showstringspaces=false,
16+
breaklines=true,
17+
breakatwhitespace=true,
18+
postbreak=\postbreak,
19+
breakindent=0pt,
20+
upquote=true,
21+
xleftmargin=8pt,
22+
xrightmargin=8pt,
23+
aboveskip=12pt,
24+
belowskip=12pt,
25+
lineskip=0pt,
26+
literate={\$$}{{\$$}}1,
27+
mathescape=false,
28+
escapechar=@,
29+
language=purescript
30+
}

docs/purescript-language.tex

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
\makeatletter
2+
\lst@InstallKeywords k{types}{typestyle}\slshape{typestyle}{}ld
3+
\lst@InstallKeywords k{constructors}{constructorstyle}\slshape{constructorstyle}{}ld
4+
\makeatother
5+
6+
\lstdefinelanguage{purescript}
7+
{
8+
% list of keywords
9+
morekeywords={
10+
module,
11+
import,
12+
if,
13+
then,
14+
else,
15+
let,
16+
in,
17+
case,
18+
of,
19+
type,
20+
newtype,
21+
data,
22+
forall,
23+
where,
24+
class,
25+
derive,
26+
instance
27+
},
28+
otherkeywords={=,==,>,<,<=,>=,\\=,~,\$,\@,*,\&,<<<,>>>,>>=,<\$>,<=<,>=>,<>,<*>,<*,*>,->,<-,_,::,\(,\),\{,\},[,],\|},
29+
moretypes={
30+
Int,
31+
String,
32+
Maybe,
33+
Either,
34+
Error,
35+
Functor,
36+
Monoid,
37+
Applicative,
38+
Alternative,
39+
Semigroup,
40+
Monad,
41+
HTTP,
42+
Eff,
43+
CONSOLE,
44+
EXCEPTION,
45+
Aff,
46+
AVAR,
47+
Proxy,
48+
},
49+
moreconstructors={
50+
Just,
51+
Nothing,
52+
Left,
53+
Right,
54+
Proxy,
55+
},
56+
% Project-specific
57+
moretypes={
58+
Conn,
59+
Middleware,
60+
RequestBody,
61+
ResponseWriter,
62+
Response,
63+
GET,
64+
POST,
65+
Header,
66+
ResponseStateTransition,
67+
StatusLineOpen,
68+
HeadersOpen,
69+
BodyOpen,
70+
ResponseEnded,
71+
EncodeHTML,
72+
},
73+
moreconstructors={
74+
GET,
75+
POST,
76+
}
77+
sensitive=true,
78+
morecomment=[l]{--}, % l is for line comment
79+
morecomment=[s]{\{-}{-\}}, % s is for start and end delimiter
80+
morestring=[b]" % defines that strings are enclosed in double quotes
81+
}

docs/template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
$endif$
1616
<title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
1717
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Source+Sans+Pro:400,400i,700" rel="stylesheet">
18-
<link rel="stylesheet" href="tomorrow.min.css">
18+
<link rel="stylesheet" href="github.css">
1919
$if(math)$
2020
$math$
2121
$endif$

docs/tomorrow.min.css

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)