Move tutorial files over

This commit is contained in:
Julian K. Arni 2016-01-25 14:11:40 +01:00 committed by Sönke Hahn
parent 94e07f9519
commit 3b3c929b40
10 changed files with 2622 additions and 0 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ Setup
.stack-work
shell.nix
default.nix
tutorial/_build

216
tutorial/Makefile Normal file
View File

@ -0,0 +1,216 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/generics-eot.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/generics-eot.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/generics-eot"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/generics-eot"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

309
tutorial/api-type.lhs Normal file
View File

@ -0,0 +1,309 @@
---
title: A web API as a type
toc: true
---
The source for this tutorial section is a literate haskell file, so first we
need to have some language extensions and imports:
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE TypeOperators #-}
>
> module ApiType where
>
> import Data.Text
> import Servant.API
Consider the following informal specification of an API:
> The endpoint at `/users` expects a GET request with query string parameter
> `sortby` whose value can be one of `age` or `name` and returns a
> list/array of JSON objects describing users, with fields `age`, `name`,
> `email`, `registration_date`".
You *should* be able to formalize that. And then use the formalized version to
get you much of the way towards writing a web app. And all the way towards
getting some client libraries, and documentation (and in the future, who knows
- tests, HATEOAS, ...).
How would we describe it with servant? As mentioned earlier, an endpoint
description is a good old Haskell **type**:
> type UserAPI = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
>
> data SortBy = Age | Name
>
> data User = User {
> name :: String,
> age :: Int
> }
Let's break that down:
- `"users"` says that our endpoint will be accessible under `/users`;
- `QueryParam "sortby" SortBy`, where `SortBy` is defined by `data SortBy = Age
| Name`, says that the endpoint has a query string parameter named `sortby`
whose value will be extracted as a value of type `SortBy`.
- `Get '[JSON] [User]` says that the endpoint will be accessible through HTTP
GET requests, returning a list of users encoded as JSON. You will see
later how you can make use of this to make your data available under different
formats, the choice being made depending on the [Accept
header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) specified in
the client's request.
- the `:>` operator that separates the various "combinators" just lets you
sequence static path fragments, URL captures and other combinators. The
ordering only matters for static path fragments and URL captures. `"users" :>
"list-all" :> Get '[JSON] [User]`, equivalent to `/users/list-all`, is
obviously not the same as `"list-all" :> "users" :> Get '[JSON] [User]`, which
is equivalent to `/list-all/users`. This means that sometimes `:>` is somehow
equivalent to `/`, but sometimes it just lets you chain another combinator.
We can also describe APIs with multiple endpoints by using the `:<|>`
combinators. Here's an example:
> type UserAPI2 = "users" :> "list-all" :> Get '[JSON] [User]
> :<|> "list-all" :> "users" :> Get '[JSON] [User]
*servant* provides a fair amount of combinators out-of-the-box, but you can
always write your own when you need it. Here's a quick overview of all the
combinators that servant comes with.
Combinators
===========
Static strings
--------------
As you've already seen, you can use type-level strings (enabled with the
`DataKinds` language extension) for static path fragments. Chaining
them amounts to `/`-separating them in a URL.
> type UserAPI3 = "users" :> "list-all" :> "now" :> Get '[JSON] [User]
> -- describes an endpoint reachable at:
> -- /users/list-all/now
`Delete`, `Get`, `Patch`, `Post` and `Put`
------------------------------------------
These 5 combinators are very similar except that they each describe a
different HTTP method. This is how they're declared
``` haskell
data Delete (contentTypes :: [*]) a
data Get (contentTypes :: [*]) a
data Patch (contentTypes :: [*]) a
data Post (contentTypes :: [*]) a
data Put (contentTypes :: [*]) a
```
An endpoint ends with one of the 5 combinators above (unless you write your
own). Examples:
> type UserAPI4 = "users" :> Get '[JSON] [User]
> :<|> "admins" :> Get '[JSON] [User]
`Capture`
---------
URL captures are parts of the URL that are variable and whose actual value is
captured and passed to the request handlers. In many web frameworks, you'll see
it written as in `/users/:userid`, with that leading `:` denoting that `userid`
is just some kind of variable name or placeholder. For instance, if `userid` is
supposed to range over all integers greater or equal to 1, our endpoint will
match requests made to `/users/1`, `/users/143` and so on.
The `Capture` combinator in servant takes a (type-level) string representing
the "name of the variable" and a type, which indicates the type we want to
decode the "captured value" to.
``` haskell
data Capture (s :: Symbol) a
-- s :: Symbol just says that 's' must be a type-level string.
```
In some web frameworks, you use regexes for captures. We use a
[`FromText`](https://hackage.haskell.org/package/servant/docs/Servant-Common-Text.html#t:FromText)
class, which the captured value must be an instance of.
Examples:
> type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
> -- equivalent to 'GET /user/:userid'
> -- except that we explicitly say that "userid"
> -- must be an integer
>
> :<|> "user" :> Capture "userid" Integer :> Delete '[] ()
> -- equivalent to 'DELETE /user/:userid'
`QueryParam`, `QueryParams`, `QueryFlag`, `MatrixParam`, `MatrixParams` and `MatrixFlag`
----------------------------------------------------------------------------------------
`QueryParam`, `QueryParams` and `QueryFlag` are about query string
parameters, i.e., those parameters that come after the question mark
(`?`) in URLs, like `sortby` in `/users?sortby=age`, whose value is
set to `age`. `QueryParams` lets you specify that the query parameter
is actually a list of values, which can be specified using
`?param[]=value1&param[]=value2`. This represents a list of values
composed of `value1` and `value2`. `QueryFlag` lets you specify a
boolean-like query parameter where a client isn't forced to specify a
value. The absence or presence of the parameter's name in the query
string determines whether the parameter is considered to have the
value `True` or `False`. For instance, `/users?active` would list only
active users whereas `/users` would list them all.
Here are the corresponding data type declarations:
``` haskell
data QueryParam (sym :: Symbol) a
data QueryParams (sym :: Symbol) a
data QueryFlag (sym :: Symbol)
```
[Matrix parameters](http://www.w3.org/DesignIssues/MatrixURIs.html)
are similar to query string parameters, but they can appear anywhere
in the paths (click the link for more details). A URL with matrix
parameters in it looks like `/users;sortby=age`, as opposed to
`/users?sortby=age` with query string parameters. The big advantage is
that they are not necessarily at the end of the URL. You could have
`/users;active=true;registered_after=2005-01-01/locations` to get
geolocation data about users whom are still active and registered
after *January 1st, 2005*.
Corresponding data type declarations below.
``` haskell
data MatrixParam (sym :: Symbol) a
data MatrixParams (sym :: Symbol) a
data MatrixFlag (sym :: Symbol)
```
Examples:
> type UserAPI6 = "users" :> QueryParam "sortby" SortBy :> Get '[JSON] [User]
> -- equivalent to 'GET /users?sortby={age, name}'
>
> :<|> "users" :> MatrixParam "sortby" SortBy :> Get '[JSON] [User]
> -- equivalent to 'GET /users;sortby={age, name}'
Again, your handlers don't have to deserialize these things (into, for example,
a `SortBy`). *servant* takes care of it.
`ReqBody`
---------
Each HTTP request can carry some additional data that the server can use in its
*body*, and this data can be encoded in any format -- as long as the server
understands it. This can be used for example for an endpoint for creating new
users: instead of passing each field of the user as a separate query string
parameter or something dirty like that, we can group all the data into a JSON
object. This has the advantage of supporting nested objects.
*servant*'s `ReqBody` combinator takes a list of content types in which the
data encoded in the request body can be represented and the type of that data.
And, as you might have guessed, you don't have to check the content-type
header, and do the deserialization yourself. We do it for you. And return `Bad
Request` or `Unsupported Content Type` as appropriate.
Here's the data type declaration for it:
``` haskell
data ReqBody (contentTypes :: [*]) a
```
Examples:
> type UserAPI7 = "users" :> ReqBody '[JSON] User :> Post '[JSON] User
> -- - equivalent to 'POST /users' with a JSON object
> -- describing a User in the request body
> -- - returns a User encoded in JSON
>
> :<|> "users" :> Capture "userid" Integer
> :> ReqBody '[JSON] User
> :> Put '[JSON] User
> -- - equivalent to 'PUT /users/:userid' with a JSON
> -- object describing a User in the request body
> -- - returns a User encoded in JSON
Request `Header`s
-----------------
Request headers are used for various purposes, from caching to carrying
auth-related data. They consist of a header name and an associated value. An
example would be `Accept: application/json`.
The `Header` combinator in servant takes a type-level string for the header
name and the type to which we want to decode the header's value (from some
textual representation), as illustrated below:
``` haskell
data Header (sym :: Symbol) a
```
Here's an example where we declare that an endpoint makes use of the
`User-Agent` header which specifies the name of the software/library used by
the client to send the request.
> type UserAPI8 = "users" :> Header "User-Agent" Text :> Get '[JSON] [User]
Content types
-------------
So far, whenever we have used a combinator that carries a list of content
types, we've always specified `'[JSON]`. However, *servant* lets you use several
content types, and also lets you define your own content types.
Four content-types are provided out-of-the-box by the core *servant* package:
`JSON`, `PlainText`, `FormUrlEncoded` and `OctetStream`. If for some obscure
reason you wanted one of your endpoints to make your user data available under
those 4 formats, you would write the API type as below:
> type UserAPI9 = "users" :> Get '[JSON, PlainText, FormUrlEncoded, OctetStream] [User]
We also provide an HTML content-type, but since there's no single library
that everyone uses, we decided to release 2 packages, *servant-lucid* and
*servant-blaze*, to provide HTML encoding of your data.
We will further explain how these content types and your data types can play
together in the [section about serving an API](/tutorial/server.html).
Response `Headers`
------------------
Just like an HTTP request, the response generated by a webserver can carry
headers too. *servant* provides a `Headers` combinator that carries a list of
`Header` and can be used by simply wrapping the "return type" of an endpoint
with it.
``` haskell
data Headers (ls :: [*]) a
```
If you want to describe an endpoint that returns a "User-Count" header in each
response, you could write it as below:
> type UserAPI10 = "users" :> Get '[JSON] (Headers '[Header "User-Count" Integer] [User])
Interoperability with other WAI `Application`s: `Raw`
-----------------------------------------------------
Finally, we also include a combinator named `Raw` that can be used for two reasons:
- You want to serve static files from a given directory. In that case you can just say:
> type UserAPI11 = "users" :> Get '[JSON] [User]
> -- a /users endpoint
>
> :<|> Raw
> -- requests to anything else than /users
> -- go here, where the server will try to
> -- find a file with the right name
> -- at the right path
- You more generally want to plug a [WAI `Application`](http://hackage.haskell.org/package/wai)
into your webservice. Static file serving is a specific example of that. The API type would look the
same as above though. (You can even combine *servant* with other web frameworks
this way!)
<div style="text-align: center;">
<a href="/tutorial/server.html">Next page: Serving an API</a>
</div>

138
tutorial/client.lhs Normal file
View File

@ -0,0 +1,138 @@
---
title: Deriving Haskell functions to query an API
toc: true
---
While defining handlers that serve an API has a lot to it, querying an API is simpler: we do not care about what happens inside the webserver, we just need to know how to talk to it and get a response back. Except that we usually have to write the querying functions by hand because the structure of the API isn't a first class citizen and can't be inspected to generate a bunch of client-side functions.
*servant* however has a way to inspect API, because APIs are just Haskell types and (GHC) Haskell lets us do quite a few things with types. In the same way that we look at an API type to deduce the types the handlers should have, we can inspect the structure of the API to *derive* Haskell functions that take one argument for each occurence of `Capture`, `ReqBody`, `QueryParam`
and friends. By *derive*, we mean that there's no code generation involved, the functions are defined just by the structure of the API type.
The source for this tutorial section is a literate haskell file, so first we
need to have some language extensions and imports:
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE DeriveGeneric #-}
> {-# LANGUAGE TypeOperators #-}
>
> module Client where
>
> import Control.Monad.Trans.Either
> import Data.Aeson
> import Data.Proxy
> import GHC.Generics
> import Servant.API
> import Servant.Client
Also, we need examples for some domain specific data types:
> data Position = Position
> { x :: Int
> , y :: Int
> } deriving (Show, Generic)
>
> instance FromJSON Position
>
> newtype HelloMessage = HelloMessage { msg :: String }
> deriving (Show, Generic)
>
> instance FromJSON HelloMessage
>
> data ClientInfo = ClientInfo
> { clientName :: String
> , clientEmail :: String
> , clientAge :: Int
> , clientInterestedIn :: [String]
> } deriving Generic
>
> instance ToJSON ClientInfo
>
> data Email = Email
> { from :: String
> , to :: String
> , subject :: String
> , body :: String
> } deriving (Show, Generic)
>
> instance FromJSON Email
Enough chitchat, let's see an example. Consider the following API type from the previous section:
> type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
What we are going to get with *servant-client* here is 3 functions, one to query each endpoint:
> position :: Int -- ^ value for "x"
> -> Int -- ^ value for "y"
> -> EitherT ServantError IO Position
>
> hello :: Maybe String -- ^ an optional value for "name"
> -> EitherT ServantError IO HelloMessage
>
> marketing :: ClientInfo -- ^ value for the request body
> -> EitherT ServantError IO Email
Each function makes available as an argument any value that the response may depend on, as evidenced in the API type. How do we get these functions? Just give a `Proxy` to your API and a host to make the requests to:
> api :: Proxy API
> api = Proxy
>
> position :<|> hello :<|> marketing = client api (BaseUrl Http "localhost" 8081)
As you can see in the code above, we just "pattern match our way" to these functions. If we try to derive less or more functions than there are endpoints in the API, we obviously get an error. The `BaseUrl` value there is just:
``` haskell
-- | URI scheme to use
data Scheme =
Http -- ^ http://
| Https -- ^ https://
deriving
-- | Simple data type to represent the target of HTTP requests
-- for servant's automatically-generated clients.
data BaseUrl = BaseUrl
{ baseUrlScheme :: Scheme -- ^ URI scheme to use
, baseUrlHost :: String -- ^ host (eg "haskell.org")
, baseUrlPort :: Int -- ^ port (eg 80)
}
```
That's it. Let's now write some code that uses our client functions.
> queries :: EitherT ServantError IO (Position, HelloMessage, Email)
> queries = do
> pos <- position 10 10
> msg <- hello (Just "servant")
> em <- marketing (ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"])
> return (pos, msg, em)
>
> run :: IO ()
> run = do
> res <- runEitherT queries
> case res of
> Left err -> putStrLn $ "Error: " ++ show err
> Right (pos, msg, em) -> do
> print pos
> print msg
> print em
You can now run `dist/build/tutorial/tutorial 8` (the server) and
`dist/build/t8-main/t8-main` (the client) to see them both in action.
``` bash
$ dist/build/tutorial/tutorial 8
# and in another terminal:
$ dist/build/t8-main/t8-main
Position {x = 10, y = 10}
HelloMessage {msg = "Hello, servant"}
Email {from = "great@company.com", to = "alp@foo.com", subject = "Hey Alp, we miss you!", body = "Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!"}
```
The types of the arguments for the functions are the same as for (server-side) request handlers. You now know how to use *servant-client*!
<div style="text-align: center;">
<p><a href="/tutorial/server.html">Previous page: Serving an API</a></p>
<p><a href="/tutorial/javascript.html">Next page: Generating javascript functions to query an API</a></p>
</div>

287
tutorial/conf.py Normal file
View File

@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
#
# generics-eot documentation build configuration file, created by
# sphinx-quickstart on Fri Jan 22 12:22:48 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
from recommonmark.parser import CommonMarkParser
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
source_suffix = ['.md', '.rst']
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'generics-eot'
copyright = u'2016, Sönke Hahn'
author = u'Sönke Hahn'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.1'
# The full version, including alpha/beta/rc tags.
release = u'0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build', 'venv']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'generics-eotdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'generics-eot.tex', u'generics-eot Documentation',
u'Sönke Hahn', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'generics-eot', u'generics-eot Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'generics-eot', u'generics-eot Documentation',
author, 'generics-eot', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
source_parsers = {
'.md': CommonMarkParser,
}

227
tutorial/docs.lhs Normal file
View File

@ -0,0 +1,227 @@
---
title: Generating documentation from API types
toc: true
---
The source for this tutorial section is a literate haskell file, so first we
need to have some language extensions and imports:
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE DeriveGeneric #-}
> {-# LANGUAGE FlexibleInstances #-}
> {-# LANGUAGE MultiParamTypeClasses #-}
> {-# LANGUAGE OverloadedStrings #-}
> {-# LANGUAGE TypeOperators #-}
> {-# OPTIONS_GHC -fno-warn-orphans #-}
>
> module Docs where
>
> import Data.ByteString.Lazy (ByteString)
> import Data.Proxy
> import Data.Text.Lazy.Encoding (encodeUtf8)
> import Data.Text.Lazy (pack)
> import Network.HTTP.Types
> import Network.Wai
> import Servant.API
> import Servant.Docs
> import Servant.Server
And we'll import some things from one of our earlier modules
([Serving an API](/tutorial/server.html)):
> import Server (Email(..), ClientInfo(..), Position(..), HelloMessage(..),
> server3, emailForClient)
Like client function generation, documentation generation amounts to inspecting the API type and extracting all the data we need to then present it in some format to users of your API.
This time however, we have to assist *servant*. While it is able to deduce a lot of things about our API, it can't magically come up with descriptions of the various pieces of our APIs that are human-friendly and explain what's going on "at the business-logic level". A good example to study for documentation generation is our webservice with the `/position`, `/hello` and `/marketing` endpoints from earlier:
> type ExampleAPI = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position
> :<|> "hello" :> QueryParam "name" String :> Get '[JSON] HelloMessage
> :<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
>
> exampleAPI :: Proxy ExampleAPI
> exampleAPI = Proxy
While *servant* can see e.g. that there are 3 endpoints and that the response bodies will be in JSON, it doesn't know what influence the captures, parameters, request bodies and other combinators have on the webservice. This is where some manual work is required.
For every capture, request body, response body, query param, we have to give some explanations about how it influences the response, what values are possible and the likes. Here's how it looks like for the parameters we have above.
> instance ToCapture (Capture "x" Int) where
> toCapture _ =
> DocCapture "x" -- name
> "(integer) position on the x axis" -- description
>
> instance ToCapture (Capture "y" Int) where
> toCapture _ =
> DocCapture "y" -- name
> "(integer) position on the y axis" -- description
>
> instance ToSample Position Position where
> toSample _ = Just (Position 3 14) -- example of output
>
> instance ToParam (QueryParam "name" String) where
> toParam _ =
> DocQueryParam "name" -- name
> ["Alp", "John Doe", "..."] -- example of values (not necessarily exhaustive)
> "Name of the person to say hello to." -- description
> Normal -- Normal, List or Flag
>
> instance ToSample HelloMessage HelloMessage where
> toSamples _ =
> [ ("When a value is provided for 'name'", HelloMessage "Hello, Alp")
> , ("When 'name' is not specified", HelloMessage "Hello, anonymous coward")
> ]
> -- mutliple examples to display this time
>
> ci :: ClientInfo
> ci = ClientInfo "Alp" "alp@foo.com" 26 ["haskell", "mathematics"]
>
> instance ToSample ClientInfo ClientInfo where
> toSample _ = Just ci
>
> instance ToSample Email Email where
> toSample _ = Just (emailForClient ci)
Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above.
With all of this, we can derive docs for our API.
> apiDocs :: API
> apiDocs = docs exampleAPI
`API` is a type provided by *servant-docs* that stores all the information one needs about a web API in order to generate documentation in some format. Out of the box, *servant-docs* only provides a pretty documentation printer that outputs [Markdown](http://en.wikipedia.org/wiki/Markdown), but the [servant-pandoc](http://hackage.haskell.org/package/servant-pandoc) package can be used to target many useful formats.
*servant*'s markdown pretty printer is a function named `markdown`.
``` haskell
markdown :: API -> String
```
That lets us see what our API docs look down in markdown, by looking at `markdown apiDocs`.
``` text
## Welcome
This is our super webservice's API.
Enjoy!
## GET /hello
#### GET Parameters:
- name
- **Values**: *Alp, John Doe, ...*
- **Description**: Name of the person to say hello to.
#### Response:
- Status code 200
- Headers: []
- Supported content types are:
- `application/json`
- When a value is provided for 'name'
```javascript
{"msg":"Hello, Alp"}
```
- When 'name' is not specified
```javascript
{"msg":"Hello, anonymous coward"}
```
## POST /marketing
#### Request:
- Supported content types are:
- `application/json`
- Example: `application/json`
```javascript
{"email":"alp@foo.com","interested_in":["haskell","mathematics"],"age":26,"name":"Alp"}
```
#### Response:
- Status code 201
- Headers: []
- Supported content types are:
- `application/json`
- Response body as below.
```javascript
{"subject":"Hey Alp, we miss you!","body":"Hi Alp,\n\nSince you've recently turned 26, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"}
```
## GET /position/:x/:y
#### Captures:
- *x*: (integer) position on the x axis
- *y*: (integer) position on the y axis
#### Response:
- Status code 200
- Headers: []
- Supported content types are:
- `application/json`
- Response body as below.
```javascript
{"x":3,"y":14}
```
```
However, we can also add one or more introduction sections to the document. We just need to tweak the way we generate `apiDocs`. We will also convert the content to a lazy `ByteString` since this is what *wai* expects for `Raw` endpoints.
> docsBS :: ByteString
> docsBS = encodeUtf8
> . pack
> . markdown
> $ docsWithIntros [intro] exampleAPI
>
> where intro = DocIntro "Welcome" ["This is our super webservice's API.", "Enjoy!"]
`docsWithIntros` just takes an additional parameter, a list of `DocIntro`s that must be displayed before any endpoint docs.
We can now serve the API *and* the API docs with a simple server.
> type DocsAPI = ExampleAPI :<|> Raw
>
> api :: Proxy DocsAPI
> api = Proxy
>
> server :: Server DocsAPI
> server = Server.server3 :<|> serveDocs
>
> where serveDocs _ respond =
> respond $ responseLBS ok200 [plain] docsBS
>
> plain = ("Content-Type", "text/plain")
>
> app :: Application
> app = serve api server
And if you spin up this server with `dist/build/tutorial/tutorial 10` and go to anywhere else than `/position`, `/hello` and `/marketing`, you will see the API docs in markdown. This is because `serveDocs` is attempted if the 3 other endpoints don't match and systematically succeeds since its definition is to just return some fixed bytestring with the `text/plain` content type.
<div style="text-align: center;">
<a href="/tutorial/javascript.html">Previous page: Generating javascript functions to query an API</a>
</div>

68
tutorial/index.rst Normal file
View File

@ -0,0 +1,68 @@
Servant tutorial
================
This is an introductory tutorial to the current version of *servant*, which is **0.4**. Any comment or issue can be directed to [this website's issue tracker](http://github.com/haskell-servant/haskell-servant.github.io/issues).
Github
-------
- the servant packages: [haskell-servant/servant](https://github.com/haskell-servant/servant)
- the website (including this tutorial): [haskell-servant/haskell-servant.github.io](https://github.com/haskell-servant/haskell-servant.github.io/)
- Feel free to use the issue tracker (or to send PRs!) on the website's repository to give feedback and suggestions about this tutorial
Introduction
-------------
*servant* has the following guiding principles:
- concision
This is a pretty wide-ranging principle. You should be able to get nice
documentation for your web servers, and client libraries, without repeating
yourself. You should not have to manually serialize and deserialize your
resources, but only declare how to do those things *once per type*. If a
bunch of your handlers take the same query parameters, you shouldn't have to
repeat that logic for each handler, but instead just "apply" it to all of
them at once. Your handlers shouldn't be where composition goes to die. And
so on.
- flexibility
If we haven't thought of your use case, it should still be easily
achievable. If you want to use templating library X, go ahead. Forms? Do
them however you want, but without difficulty. We're not opinionated.
- separation of concerns
Your handlers and your HTTP logic should be separate. True to the philosphy
at the core of HTTP and REST, with *servant* your handlers return normal
Haskell datatypes - that's the resource. And then from a description of your
API, *servant* handles the *presentation* (i.e., the Content-Types). But
that's just one example.
- type safety
Want to be sure your API meets a specification? Your compiler can check
that for you. Links you can be sure exist? You got it.
To stick true to these principles, we do things a little differently than you
might expect. The core idea is *reifying the description of your API*. Once
reified, everything follows. We think we might be the first web framework to
reify API descriptions in an extensible way. We're pretty sure we're the first
to reify it as *types*.
To be able to write a webservice you only need to read the first two sections,
but the goal of this document being to get you started with servant, we also
cover the couple of ways you can extend servant for a great good.
Tutorial
---------
.. toctree::
:maxdepth: 2
api-type.lhs
server.lhs
client.lhs
javascript.lhs
docs.lhs

175
tutorial/javascript.lhs Normal file
View File

@ -0,0 +1,175 @@
---
title: Deriving Javascript functions to query an API
toc: true
---
We will now see how *servant* lets you turn an API type into javascript
functions that you can call to query a webservice. The derived code assumes you
use *jQuery* but you could very easily adapt the code to generate ajax requests
based on vanilla javascript or another library than *jQuery*.
For this, we will consider a simple page divided in two parts. At the top, we
will have a search box that lets us search in a list of Haskell books by
author/title with a list of results that gets updated every time we enter or
remove a character, while at the bottom we will be able to see the classical
[probabilistic method to approximate
pi](http://en.wikipedia.org/wiki/Approximations_of_%CF%80#Summing_a_circle.27s_area),
using a webservice to get random points. Finally, we will serve an HTML file
along with a couple of Javascript files, among which one that's automatically
generated from the API type and which will provide ready-to-use functions to
query your API.
The source for this tutorial section is a literate haskell file, so first we
need to have some language extensions and imports:
> {-# LANGUAGE DataKinds #-}
> {-# LANGUAGE DeriveGeneric #-}
> {-# LANGUAGE OverloadedStrings #-}
> {-# LANGUAGE TypeOperators #-}
>
> module Javascript where
>
> import Control.Monad.IO.Class
> import Data.Aeson
> import Data.Proxy
> import Data.Text (Text)
> import qualified Data.Text as T
> import GHC.Generics
> import Language.Javascript.JQuery
> import Network.Wai
> import Servant
> import Servant.JQuery
> import System.Random
Now let's have the API type(s) and the accompanying datatypes.
> type API = "point" :> Get '[JSON] Point
> :<|> "books" :> QueryParam "q" Text :> Get '[JSON] (Search Book)
>
> type API' = API :<|> Raw
>
> data Point = Point
> { x :: Double
> , y :: Double
> } deriving Generic
>
> instance ToJSON Point
>
> data Search a = Search
> { query :: Text
> , results :: [a]
> } deriving Generic
>
> mkSearch :: Text -> [a] -> Search a
> mkSearch = Search
>
> instance ToJSON a => ToJSON (Search a)
>
> data Book = Book
> { author :: Text
> , title :: Text
> , year :: Int
> } deriving Generic
>
> instance ToJSON Book
>
> book :: Text -> Text -> Int -> Book
> book = Book
We need a "book database". For the purpose of this guide, let's restrict ourselves to the following books.
> books :: [Book]
> books =
> [ book "Paul Hudak" "The Haskell School of Expression: Learning Functional Programming through Multimedia" 2000
> , book "Bryan O'Sullivan, Don Stewart, and John Goerzen" "Real World Haskell" 2008
> , book "Miran Lipovača" "Learn You a Haskell for Great Good!" 2011
> , book "Graham Hutton" "Programming in Haskell" 2007
> , book "Simon Marlow" "Parallel and Concurrent Programming in Haskell" 2013
> , book "Richard Bird" "Introduction to Functional Programming using Haskell" 1998
> ]
Now, given an optional search string `q`, we want to perform a case insensitive search in that list of books. We're obviously not going to try and implement the best possible algorithm, this is out of scope for this tutorial. The following simple linear scan will do, given how small our list is.
> searchBook :: Monad m => Maybe Text -> m (Search Book)
> searchBook Nothing = return (mkSearch "" books)
> searchBook (Just q) = return (mkSearch q books')
>
> where books' = filter (\b -> q' `T.isInfixOf` T.toLower (author b)
> || q' `T.isInfixOf` T.toLower (title b)
> )
> books
> q' = T.toLower q
We also need an endpoint that generates random points `(x, y)` with `-1 <= x,y <= 1`. The code below uses [random](http://hackage.haskell.org/package/random)'s `System.Random`.
> randomPoint :: MonadIO m => m Point
> randomPoint = liftIO . getStdRandom $ \g ->
> let (rx, g') = randomR (-1, 1) g
> (ry, g'') = randomR (-1, 1) g'
> in (Point rx ry, g'')
If we add static file serving, our server is now complete.
> api :: Proxy API
> api = Proxy
>
> api' :: Proxy API'
> api' = Proxy
>
> server :: Server API
> server = randomPoint
> :<|> searchBook
>
> server' :: Server API'
> server' = server
> :<|> serveDirectory "tutorial/t9"
>
> app :: Application
> app = serve api' server'
Why two different API types, proxies and servers though? Simply because we don't want to generate javascript functions for the `Raw` part of our API type, so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
Very similarly to how one can derive haskell functions, we can derive the javascript with just a simple function call to `jsForAPI` from `Servant.JQuery`.
> apiJS :: String
> apiJS = jsForAPI api
This `String` contains 2 Javascript functions:
``` javascript
function getpoint(onSuccess, onError)
{
$.ajax(
{ url: '/point'
, success: onSuccess
, error: onError
, method: 'GET'
});
}
function getbooks(q, onSuccess, onError)
{
$.ajax(
{ url: '/books' + '?q=' + encodeURIComponent(q)
, success: onSuccess
, error: onError
, method: 'GET'
});
}
```
Right before starting up our server, we will need to write this `String` to a file, say `api.js`, along with a copy of the *jQuery* library, as provided by the [js-jquery](http://hackage.haskell.org/package/js-jquery) package.
> writeJSFiles :: IO ()
> writeJSFiles = do
> writeFile "getting-started/gs9/api.js" apiJS
> jq <- readFile =<< Language.Javascript.JQuery.file
> writeFile "getting-started/gs9/jq.js" jq
And we're good to go. Start the server with `dist/build/tutorial/tutorial 9` and go to `http://localhost:8081/`. Start typing in the name of one of the authors in our database or part of a book title, and check out how long it takes to approximate &pi; using the method mentioned above.
<div style="text-align: center;">
<p><a href="/tutorial/client.html">Previous page: Deriving Haskell functions to query an API</a></p>
<p><a href="/tutorial/docs.html">Next page: Generating documentation for APIs</a></p>
</div>

25
tutorial/requirements.txt Normal file
View File

@ -0,0 +1,25 @@
alabaster==0.7.7
argh==0.26.1
Babel==2.2.0
backports-abc==0.4
backports.ssl-match-hostname==3.5.0.1
certifi==2015.11.20.1
CommonMark==0.5.4
docutils==0.12
Jinja2==2.8
livereload==2.4.1
MarkupSafe==0.23
pathtools==0.1.2
Pygments==2.1
pytz==2015.7
PyYAML==3.11
recommonmark==0.4.0
singledispatch==3.4.0.3
six==1.10.0
snowballstemmer==1.2.1
Sphinx==1.3.4
sphinx-autobuild==0.5.2
sphinx-rtd-theme==0.1.9
tornado==4.3
watchdog==0.8.3
wheel==0.26.0

1176
tutorial/server.lhs Normal file

File diff suppressed because it is too large Load Diff