forked from haskell-servant/haskell-servant.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclient-in-5-minutes.html
169 lines (150 loc) · 13 KB
/
client-in-5-minutes.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Write a client library for any web API in 5 minutes - haskell-servant</title>
<link rel="stylesheet" type="text/css" href="./css/default.css" />
</head>
<body>
<div id="header">
<div id="logo">
<a href="./">servant</a>
</div>
<div id="navigation">
<a href="./">Home</a>
<a href="./blog.html">Blog</a>
<a href="./tutorial">Tutorial</a>
<a href="./tips.html">Tips and tricks</a>
<a href="./talks.html">Talks</a>
<a href="https://github.com/haskell-servant/servant">Github</a>
</div>
</div>
<div id="content">
<h1>Write a client library for any web API in 5 minutes</h1>
<div id="toc"><h3>Table of contents</h3><ul>
<li><a href="#the-hackage-api">The Hackage API</a></li>
<li><a href="#describing-hackages-api-as-a-type">Describing Hackage’s API as a type</a></li>
<li><a href="#data-types-and-json-serialization">Data types and JSON serialization</a></li>
<li><a href="#deriving-functions-to-query-hackage">Deriving functions to query hackage</a></li>
<li><a href="#code">Code</a></li>
</ul></div>
<p><em>servant</em> lets us write request handlers for webservices in a quite straighforward way, without polluting your logic with encoding/decoding of all sorts. What may be less obvious is that you also get a somehow symetric benefit too by being able to <em>derive</em> (without <em>actually writing them</em>) functions to query an API described by some servant API type. Here’s an example.</p>
<section id="the-hackage-api" class="level1">
<h1>The Hackage API</h1>
<p>Let’s write some functions to query a couple of endpoints of <a href="http://hackage.haskell.org/api">Hackage’s API</a>. Let’s just consider the following ones:</p>
<pre><code>/users/
GET: json -- list of users
/user/:username
GET: json -- user id info
/packages/
GET: json -- List of all packages</code></pre>
<p>Let’s see what the output looks like by using <em>curl</em>:</p>
<pre class="sourceCode bash"><code class="sourceCode bash">$ <span class="kw">curl</span> -H <span class="st">"Accept: application/json"</span> http://hackage.haskell.org/users/
[<span class="dt">{"username":"admin","userid":0}</span>, <span class="kw">...</span>]
$ <span class="kw">curl</span> -H <span class="st">"Accept: application/json"</span> http://hackage.haskell.org/user/AlpMestanogullari
<span class="dt">{"groups":["/package/gloss-juicy/maintainers","/package/hnn/maintainers","/package/hspec-attoparsec/maintainers","/package/kmeans-vector/maintainers","/package/pastis/maintainers","/package/probable/maintainers","/package/servant-client/maintainers","/package/servant-docs/maintainers","/package/servant-jquery/maintainers","/package/servant-pool/maintainers","/package/servant-postgresql/maintainers","/package/servant-response/maintainers","/package/servant-scotty/maintainers","/package/servant-server/maintainers","/package/servant/maintainers","/package/sitemap/maintainers","/package/statistics-linreg/maintainers","/package/taggy-lens/maintainers","/package/taggy/maintainers","/packages/uploaders"],"username":"AlpMestanogullari","userid":75}</span>
$ <span class="kw">curl</span> -H <span class="st">"Accept: application/json"</span> http://hackage.haskell.org/packages/
[<span class="dt">{"packageName":"3d-graphics-examples"},{"packageName":"3dmodels"}</span>, <span class="kw">...</span>]</code></pre>
<p>This is enough to get us started.</p>
</section>
<section id="describing-hackages-api-as-a-type" class="level1">
<h1>Describing Hackage’s API as a type</h1>
<p>First, some pragmas and imports:</p>
<pre class="sourceCode haskell"><code class="sourceCode haskell"><span class="ot">{-# LANGUAGE DataKinds #-}</span>
<span class="ot">{-# LANGUAGE DeriveGeneric #-}</span>
<span class="ot">{-# LANGUAGE TypeOperators #-}</span>
<span class="ot">{-# LANGUAGE OverloadedStrings #-}</span>
<span class="kw">import </span><span class="dt">Control.Applicative</span>
<span class="kw">import </span><span class="dt">Control.Monad</span>
<span class="kw">import </span><span class="dt">Control.Monad.IO.Class</span>
<span class="kw">import </span><span class="dt">Control.Monad.Trans.Either</span>
<span class="kw">import </span><span class="dt">Data.Aeson</span>
<span class="kw">import </span><span class="dt">Data.Monoid</span>
<span class="kw">import </span><span class="dt">Data.Proxy</span>
<span class="kw">import </span><span class="dt">Data.Text</span> (<span class="dt">Text</span>)
<span class="kw">import </span><span class="dt">GHC.Generics</span>
<span class="kw">import </span><span class="dt">Servant.API</span>
<span class="kw">import </span><span class="dt">Servant.Client</span>
<span class="kw">import qualified</span> <span class="dt">Data.Text</span> <span class="kw">as</span> <span class="dt">T</span>
<span class="kw">import qualified</span> <span class="dt">Data.Text.IO</span> <span class="kw">as</span> <span class="dt">T</span></code></pre>
<p>Now, let’s write the API type that corresponds to those 3 endpoints we’re interested in.</p>
<pre class="sourceCode haskell"><code class="sourceCode haskell"><span class="kw">type</span> <span class="dt">HackageAPI</span> <span class="fu">=</span>
<span class="st">"users"</span> <span class="fu">:></span> <span class="dt">Get</span> <span class="ch">'[JSON] [UserSummary]</span>
<span class="fu">:<|></span> <span class="st">"user"</span> <span class="fu">:></span> <span class="dt">Capture</span> <span class="st">"username"</span> <span class="dt">Username</span> <span class="fu">:></span> <span class="dt">Get</span> <span class="ch">'[JSON] UserDetailed</span>
<span class="fu">:<|></span> <span class="st">"packages"</span> <span class="fu">:></span> <span class="dt">Get</span> <span class="ch">'[JSON] [Package]</span></code></pre>
<p>Nothing fancy here, except that we clearly specify we are expecting the output to be in JSON (this will insert the appropriate <code>Accept</code> header).</p>
</section>
<section id="data-types-and-json-serialization" class="level1">
<h1>Data types and JSON serialization</h1>
<p>We also need some types to go with that: <code>UserSummary</code>, <code>Username</code>, <code>UserDetailed</code>, <code>Package</code>. Here they are, along with JSON deserialization instances.</p>
<pre class="sourceCode haskell"><code class="sourceCode haskell"><span class="kw">type</span> <span class="dt">Username</span> <span class="fu">=</span> <span class="dt">Text</span>
<span class="kw">data</span> <span class="dt">UserSummary</span> <span class="fu">=</span> <span class="dt">UserSummary</span>
{<span class="ot"> summaryUsername ::</span> <span class="dt">Username</span>
,<span class="ot"> summaryUserid ::</span> <span class="dt">Int</span>
} <span class="kw">deriving</span> (<span class="dt">Eq</span>, <span class="dt">Show</span>)
<span class="kw">instance</span> <span class="dt">FromJSON</span> <span class="dt">UserSummary</span> <span class="kw">where</span>
parseJSON (<span class="dt">Object</span> o) <span class="fu">=</span>
<span class="dt">UserSummary</span> <span class="fu"><$></span> o <span class="fu">.:</span> <span class="st">"username"</span>
<span class="fu"><*></span> o <span class="fu">.:</span> <span class="st">"userid"</span>
parseJSON _ <span class="fu">=</span> mzero
<span class="kw">type</span> <span class="dt">Group</span> <span class="fu">=</span> <span class="dt">Text</span>
<span class="kw">data</span> <span class="dt">UserDetailed</span> <span class="fu">=</span> <span class="dt">UserDetailed</span>
{<span class="ot"> username ::</span> <span class="dt">Username</span>
,<span class="ot"> userid ::</span> <span class="dt">Int</span>
,<span class="ot"> groups ::</span> [<span class="dt">Group</span>]
} <span class="kw">deriving</span> (<span class="dt">Eq</span>, <span class="dt">Show</span>, <span class="dt">Generic</span>)
<span class="kw">instance</span> <span class="dt">FromJSON</span> <span class="dt">UserDetailed</span>
<span class="kw">newtype</span> <span class="dt">Package</span> <span class="fu">=</span> <span class="dt">Package</span> {<span class="ot"> packageName ::</span> <span class="dt">Text</span> }
<span class="kw">deriving</span> (<span class="dt">Eq</span>, <span class="dt">Show</span>, <span class="dt">Generic</span>)
<span class="kw">instance</span> <span class="dt">FromJSON</span> <span class="dt">Package</span></code></pre>
</section>
<section id="deriving-functions-to-query-hackage" class="level1">
<h1>Deriving functions to query hackage</h1>
<p>Finally, we can automatically derive our client functions:</p>
<pre class="sourceCode haskell"><code class="sourceCode haskell"><span class="ot">hackageAPI ::</span> <span class="dt">Proxy</span> <span class="dt">HackageAPI</span>
hackageAPI <span class="fu">=</span> <span class="dt">Proxy</span>
<span class="ot">getUsers ::</span> <span class="dt">EitherT</span> <span class="dt">ServantError</span> <span class="dt">IO</span> [<span class="dt">UserSummary</span>]
<span class="ot">getUser ::</span> <span class="dt">Username</span> <span class="ot">-></span> <span class="dt">EitherT</span> <span class="dt">ServantError</span> <span class="dt">IO</span> <span class="dt">UserDetailed</span>
<span class="ot">getPackages ::</span> <span class="dt">EitherT</span> <span class="dt">ServantError</span> <span class="dt">IO</span> [<span class="dt">Package</span>]
getUsers <span class="fu">:<|></span> getUser <span class="fu">:<|></span> getPackages <span class="fu">=</span> client hackageAPI (<span class="dt">BaseUrl</span> <span class="dt">Http</span> <span class="st">"hackage.haskell.org"</span> <span class="dv">80</span>)</code></pre>
<p>And here’s some runnable code to actually check that everything works as expected:</p>
<pre class="sourceCode haskell"><code class="sourceCode haskell"><span class="ot">main ::</span> <span class="dt">IO</span> ()
main <span class="fu">=</span> print <span class="fu">=<<</span> uselessNumbers
<span class="ot">uselessNumbers ::</span> <span class="dt">IO</span> (<span class="dt">Either</span> <span class="dt">ServantError</span> ())
uselessNumbers <span class="fu">=</span> runEitherT <span class="fu">$</span> <span class="kw">do</span>
users <span class="ot"><-</span> getUsers
liftIO <span class="fu">.</span> putStrLn <span class="fu">$</span> show (length users) <span class="fu">++</span> <span class="st">" users"</span>
user <span class="ot"><-</span> liftIO <span class="fu">$</span> <span class="kw">do</span>
putStrLn <span class="st">"Enter a valid hackage username"</span>
T.getLine
userDetailed <span class="ot"><-</span> run (getUser user)
liftIO <span class="fu">.</span> T.putStrLn <span class="fu">$</span> user <span class="fu"><></span> <span class="st">" maintains "</span> <span class="fu"><></span> T.pack (show (length <span class="fu">$</span> groups userDetailed)) <span class="fu"><></span> <span class="st">" packages"</span>
packages <span class="ot"><-</span> run getPackages
<span class="kw">let</span> monadPackages <span class="fu">=</span> filter (isMonadPackage <span class="fu">.</span> packageName) packages
liftIO <span class="fu">.</span> putStrLn <span class="fu">$</span> show (length monadPackages) <span class="fu">++</span> <span class="st">" monad packages"</span>
<span class="kw">where</span> isMonadPackage <span class="fu">=</span> T.isInfixOf <span class="st">"monad"</span></code></pre>
<p>Here’s a sample run:</p>
<pre><code>$ cabal run hackage
Preprocessing executable hackage for servant-examples-0.3...
Running hackage...
2460 users
Enter a valid hackage username
AlpMestanogullari
AlpMestanogullari maintains 20 packages
130 monad packages
Right ()</code></pre>
</section>
<section id="code" class="level1">
<h1>Code</h1>
<p>The whole code is available in <a href="http://github.com/haskell-servant/servant">servant’s repo</a>, under the <code>servant-examples/hackage</code> directory.</p>
</section>
</div>
<div id="footer">
Site proudly generated by
<a href="http://jaspervdj.be/hakyll">Hakyll</a>
-
<a href="https://github.com/haskell-servant/haskell-servant.github.io">Source</a>
</div>
</body>
</html>