diff --git a/.gitignore b/.gitignore index 2b2f3487..ee3cbf51 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ Setup .stack-work shell.nix default.nix +tutorial/_build diff --git a/tutorial/Makefile b/tutorial/Makefile new file mode 100644 index 00000000..95957c1a --- /dev/null +++ b/tutorial/Makefile @@ -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 ' where 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." diff --git a/tutorial/api-type.lhs b/tutorial/api-type.lhs new file mode 100644 index 00000000..71c84631 --- /dev/null +++ b/tutorial/api-type.lhs @@ -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¶m[]=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!) + +
+ Next page: Serving an API +
diff --git a/tutorial/client.lhs b/tutorial/client.lhs new file mode 100644 index 00000000..21779f2b --- /dev/null +++ b/tutorial/client.lhs @@ -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*! + +
+

Previous page: Serving an API

+

Next page: Generating javascript functions to query an API

+
diff --git a/tutorial/conf.py b/tutorial/conf.py new file mode 100644 index 00000000..6d4f897d --- /dev/null +++ b/tutorial/conf.py @@ -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 +# " v 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 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, +} diff --git a/tutorial/docs.lhs b/tutorial/docs.lhs new file mode 100644 index 00000000..cb662a54 --- /dev/null +++ b/tutorial/docs.lhs @@ -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. + +
+ Previous page: Generating javascript functions to query an API +
diff --git a/tutorial/index.rst b/tutorial/index.rst new file mode 100644 index 00000000..378155e1 --- /dev/null +++ b/tutorial/index.rst @@ -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 diff --git a/tutorial/javascript.lhs b/tutorial/javascript.lhs new file mode 100644 index 00000000..33b4f73b --- /dev/null +++ b/tutorial/javascript.lhs @@ -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 π using the method mentioned above. + +
+

Previous page: Deriving Haskell functions to query an API

+

Next page: Generating documentation for APIs

+
diff --git a/tutorial/requirements.txt b/tutorial/requirements.txt new file mode 100644 index 00000000..8f89e4b8 --- /dev/null +++ b/tutorial/requirements.txt @@ -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 diff --git a/tutorial/server.lhs b/tutorial/server.lhs new file mode 100644 index 00000000..32f098ba --- /dev/null +++ b/tutorial/server.lhs @@ -0,0 +1,1176 @@ +--- +title: Serving an API +toc: true +--- + +Enough chit-chat about type-level combinators and representing an API as a +type. Can we have a webservice already? + +If you want to follow along with the code and run the examples while you read this guide: + +``` bash +cabal get servant-examples +cd servant-examples- +cabal sandbox init +cabal install --dependencies-only +cabal configure && cabal build +``` + +This will produce a `tutorial` executable in the +`dist/build/tutorial` directory that just runs the example corresponding +to the number specified as a command line argument: + +``` bash +$ dist/build/tutorial/tutorial +Usage: tutorial N + where N is the number of the example you want to run. +``` + +A first example +=============== + +Equipped with some basic knowledge about the way we represent API, let's now write our first webservice. + +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 GeneralizedNewtypeDeriving #-} +> {-# LANGUAGE MultiParamTypeClasses #-} +> {-# LANGUAGE OverloadedStrings #-} +> {-# LANGUAGE ScopedTypeVariables #-} +> {-# LANGUAGE TypeOperators #-} +> +> module Server where +> +> import Control.Monad.IO.Class +> import Control.Monad.Reader +> import Control.Monad.Trans.Either +> import Data.Aeson +> import Data.Aeson.Types +> import Data.Attoparsec.ByteString +> import Data.ByteString (ByteString) +> import Data.Int +> import Data.List +> import Data.String.Conversions +> import Data.Time.Calendar +> import GHC.Generics +> import Lucid +> import Network.HTTP.Media ((//), (/:)) +> import Network.Wai +> import Network.Wai.Handler.Warp +> import Servant +> import System.Directory +> import Text.Blaze +> import Text.Blaze.Html.Renderer.Utf8 +> import qualified Data.Aeson.Parser +> import qualified Text.Blaze.Html + +``` haskell +{-# LANGUAGE TypeFamilies #-} +``` + +**Important**: the `Servant` module comes from the *servant-server* package, the one that lets us run webservers that implement a particular API type. It reexports all the types from the *servant* package that let you declare API types as well as everything you need to turn your request handlers into a fully-fledged webserver. This means that in your applications, you can just add *servant-server* as a dependency, import `Servant` and not worry about anything else. + +We will write a server that will serve the following API. + +> type UserAPI1 = "users" :> Get '[JSON] [User] + +Here's what we would like to see when making a GET request to `/users`. + +``` javascript +[ {"name": "Isaac Newton", "age": 372, "email": "isaac@newton.co.uk", "registration_date": "1683-03-01"} +, {"name": "Albert Einstein", "age": 136, "email": "ae@mc2.org", "registration_date": "1905-12-01"} +] +``` + +Now let's define our `User` data type and write some instances for it. + +> data User = User +> { name :: String +> , age :: Int +> , email :: String +> , registration_date :: Day +> } deriving (Eq, Show, Generic) +> +> instance ToJSON User + +Nothing funny going on here. But we now can define our list of two users. + +> users1 :: [User] +> users1 = +> [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) +> , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) +> ] + +Let's also write our API type. + +``` haskell +type UserAPI1 = "users" :> Get '[JSON] [User] +``` + +We can now take care of writing the actual webservice that will handle requests +to such an API. This one will be very simple, being reduced to just a single +endpoint. The type of the web application is determined by the API type, +through a *type family* named `Server`. (Type families are just functions that +take types as input and return types.) The `Server` type family will compute +the right type that a bunch of request handlers should have just from the +corresponding API type. + +The first thing to know about the `Server` type family is that behind the +scenes it will drive the routing, letting you focus only on the business +logic. The second thing to know is that for each endpoint, your handlers will +by default run in the `EitherT ServantErr IO` monad. This is overridable very +easily, as explained near the end of this guide. Third thing, the type of the +value returned in that monad must be the same as the second argument of the +HTTP method combinator used for the corresponding endpoint. In our case, it +means we must provide a handler of type `EitherT ServantErr IO [User]`. Well, +we have a monad, let's just `return` our list: + +> server1 :: Server UserAPI1 +> server1 = return users1 + +That's it. Now we can turn `server` into an actual webserver using [wai](http://hackage.haskell.org/package/wai) and [warp](http://hackage.haskell.org/package/warp): + +> userAPI :: Proxy UserAPI1 +> userAPI = Proxy +> +> -- 'serve' comes from servant and hands you a WAI Application, +> -- which you can think of as an "abstract" web application, +> -- not yet a webserver. +> app1 :: Application +> app1 = serve userAPI server1 + +The `userAPI` bit is, alas, boilerplate (we need it to guide type inference). +But that's about as much boilerplate as you get. + +And we're done! Let's run our webservice on the port 8081. + +> main :: IO () +> main = run 8081 app1 + +You can put this all into a file or just grab [servant's +repo](http://github.com/haskell-servant/servant) and look at the +*servant-examples* directory. The code we have just explored is in +*tutorial/T1.hs*, runnable with +`dist/build/tutorial/tutorial 1`. + +If you run it, you can go to `http://localhost:8081/users` in your browser or +query it with curl and you see: + +``` bash +$ curl http://localhost:8081/users +[{"email":"isaac@newton.co.uk","registration_date":"1683-03-01","age":372,"name":"Isaac Newton"},{"email":"ae@mc2.org","registration_date":"1905-12-01","age":136,"name":"Albert Einstein"}] +``` + +More endpoints +============== + +What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON. + +> type UserAPI2 = "users" :> Get '[JSON] [User] +> :<|> "albert" :> Get '[JSON] User +> :<|> "isaac" :> Get '[JSON] User + +And let's adapt our code a bit. + +> isaac :: User +> isaac = User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) +> +> albert :: User +> albert = User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) +> +> users2 :: [User] +> users2 = [isaac, albert] + +Now, just like we separate the various endpoints in `UserAPI` with `:<|>`, we +are going to separate the handlers with `:<|>` too! They must be provided in +the same order as the one they appear in in the API type. + +> server2 :: Server UserAPI2 +> server2 = return users2 +> :<|> return albert +> :<|> return isaac + +And that's it! You can run this example with +`dist/build/tutorial/tutorial 2` and check out the data available +at `/users`, `/albert` and `/isaac`. + +From combinators to handler arguments +===================================== + +Fine, we can write trivial webservices easily, but none of the two above use +any "fancy" combinator from servant. Let's address this and use `QueryParam`, +`Capture` and `ReqBody` right away. You'll see how each occurence of these +combinators in an endpoint makes the corresponding handler receive an +argument of the appropriate type automatically. You don't have to worry about +manually looking up URL captures or query string parameters, or +decoding/encoding data from/to JSON. Never. + +We are going to use the following data types and functions to implement a server for `API`. + +> 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 +> +> data Position = Position +> { x :: Int +> , y :: Int +> } deriving Generic +> +> instance ToJSON Position +> +> newtype HelloMessage = HelloMessage { msg :: String } +> deriving Generic +> +> instance ToJSON HelloMessage +> +> data ClientInfo = ClientInfo +> { clientName :: String +> , clientEmail :: String +> , clientAge :: Int +> , clientInterestedIn :: [String] +> } deriving Generic +> +> instance FromJSON ClientInfo +> instance ToJSON ClientInfo +> +> data Email = Email +> { from :: String +> , to :: String +> , subject :: String +> , body :: String +> } deriving Generic +> +> instance ToJSON Email +> +> emailForClient :: ClientInfo -> Email +> emailForClient c = Email from' to' subject' body' +> +> where from' = "great@company.com" +> to' = clientEmail c +> subject' = "Hey " ++ clientName c ++ ", we miss you!" +> body' = "Hi " ++ clientName c ++ ",\n\n" +> ++ "Since you've recently turned " ++ show (clientAge c) +> ++ ", have you checked out our latest " +> ++ intercalate ", " (clientInterestedIn c) +> ++ " products? Give us a visit!" + +We can implement handlers for the three endpoints: + +> server3 :: Server API +> server3 = position +> :<|> hello +> :<|> marketing +> +> where position :: Int -> Int -> EitherT ServantErr IO Position +> position x y = return (Position x y) +> +> hello :: Maybe String -> EitherT ServantErr IO HelloMessage +> hello mname = return . HelloMessage $ case mname of +> Nothing -> "Hello, anonymous coward" +> Just n -> "Hello, " ++ n +> +> marketing :: ClientInfo -> EitherT ServantErr IO Email +> marketing clientinfo = return (emailForClient clientinfo) + +Did you see that? The types for your handlers changed to be just what we +needed! In particular: + + - a `Capture "something" a` becomes an argument of type `a` (for `position`); + - a `QueryParam "something" a` becomes an argument of type `Maybe a` (because +an endpoint can technically be accessed without specifying any query +string parameter, we decided to "force" handlers to be aware that the +parameter might not always be there); + + - a `ReqBody contentTypeList a` becomes an argument of type `a`; + +And that's it. You can see this example in action by running `dist/build/tutorial/tutorial 3`. + +``` bash +$ curl http://localhost:8081/position/1/2 +{"x":1,"y":2} +$ curl http://localhost:8081/hello +{"msg":"Hello, anonymous coward"} +$ curl http://localhost:8081/hello?name=Alp +{"msg":"Hello, Alp"} +$ curl -X POST -d '{"name":"Alp Mestanogullari", "email" : "alp@foo.com", "age": 25, "interested_in": ["haskell", "mathematics"]}' -H 'Accept: application/json' -H 'Content-type: application/json' http://localhost:8081/marketing +{"subject":"Hey Alp Mestanogullari, we miss you!","body":"Hi Alp Mestanogullari,\n\nSince you've recently turned 25, have you checked out our latest haskell, mathematics products? Give us a visit!","to":"alp@foo.com","from":"great@company.com"} +``` + +For reference, here's a list of some combinators from *servant* and for those +that get turned into arguments to the handlers, the type of the argument. + + > - `Delete`, `Get`, `Patch`, `Post`, `Put`: these do not become arguments. They provide the return type of handlers, which usually is `EitherT ServantErr IO `. + > - `Capture "something" a` becomes an argument of type `a`. + > - `QueryParam "something" a`, `MatrixParam "something" a`, `Header "something" a` all become arguments of type `Maybe a`, because there might be no value at all specified by the client for these. + > - `QueryFlag "something"` and `MatrixFlag "something"` get turned into arguments of type `Bool`. + > - `QueryParams "something" a` and `MatrixParams "something" a` get turned into arguments of type `[a]`. + > - `ReqBody contentTypes a` gets turned into an argument of type `a`. + +The `FromText`/`ToText` classes +=============================== + +Wait... How does *servant* know how to decode the `Int`s from the URL? Or how +to decode a `ClientInfo` value from the request body? This is what this and the +following two sections address. + +`Capture`s and `QueryParam`s are represented by some textual value in URLs. +`Header`s are similarly represented by a pair of a header name and a +corresponding (textual) value in the request's "metadata". This is why we +decided to provide a pair of typeclasses, `FromText` and `ToText` which just +let you say that you can respectively *extract* or *encode* values of some type +*from*/*to* text. Here are the definitions: + +``` haskell +class FromText a where + fromText :: Text -> Maybe a + +class ToText a where + toText :: a -> Text +``` + +And as long as the type that a `Capture`/`QueryParam`/`Header`/etc will be +decoded to provides a `FromText` instance, it will Just Work. *servant* +provides a decent number of instances, but here are some examples of defining +your own. + +> -- A typical enumeration +> data Direction +> = Up +> | Down +> | Left +> | Right +> +> instance FromText Direction where +> -- requires {-# LANGUAGE OverloadedStrings #-} +> fromText "up" = Just Up +> fromText "down" = Just Down +> fromText "left" = Just Server.Left +> fromText "right" = Just Server.Right +> fromText _ = Nothing +> +> instance ToText Direction where +> toText Up = "up" +> toText Down = "down" +> toText Server.Left = "left" +> toText Server.Right = "right" +> +> newtype UserId = UserId Int64 +> deriving (FromText, ToText) + +or writing the instances by hand: + +``` haskell +instance FromText UserId where + fromText = fmap UserId fromText + +instance ToText UserId where + toText (UserId i) = toText i +``` + +There's not much else to say about these classes. You will need instances for +them when using `Capture`, `QueryParam`, `QueryParams`, `MatrixParam`, +`MatrixParams` and `Header` with your types. You will need `FromText` instances +for server-side request handlers and `ToText` instances only when using +*servant-client*, as described in the [section about deriving haskell +functions to query an API](/tutorial/client.html). + +Using content-types with your data types +======================================== + +The same principle was operating when decoding request bodies from JSON, and +responses *into* JSON. (JSON is just the running example - you can do this with +any content-type.) + +This section introduces a couple of typeclasses provided by *servant* that make +all of this work. + +The truth behind `JSON` +----------------------- + +What exactly is `JSON`? Like the 3 other content types provided out of the box +by *servant*, it's a really dumb data type. + +``` haskell +data JSON +data PlainText +data FormUrlEncoded +data OctetStream +``` + +Obviously, this is not all there is to `JSON`, otherwise it would be quite +pointless. Like most of the data types in *servant*, `JSON` is mostly there as +a special *symbol* that's associated with encoding (resp. decoding) to (resp. +from) the *JSON* format. The way this association is performed can be +decomposed into two steps. + +The first step is to provide a proper +[`MediaType`](https://hackage.haskell.org/package/http-media-0.6.2/docs/Network-HTTP-Media.html) +representation for `JSON`, or for your own content types. If you look at the +haddocks from this link, you can see that we just have to specify +`application/json` using the appropriate functions. In our case, we can just +use `(//) :: ByteString -> ByteString -> MediaType`. The precise way to specify +the `MediaType` is to write an instance for the `Accept` class: + +``` haskell +-- for reference: +class Accept ctype where + contentType :: Proxy ctype -> MediaType + +instance Accept JSON where + contentType _ = "application" // "json" +``` + +The second step is centered around the `MimeRender` and `MimeUnrender` classes. +These classes just let you specify a way to respectively encode and decode +values respectively into or from your content-type's representation. + +``` haskell +class Accept ctype => MimeRender ctype a where + mimeRender :: Proxy ctype -> a -> ByteString + -- alternatively readable as: + mimeRender :: Proxy ctype -> (a -> ByteString) +``` + +Given a content-type and some user type, `MimeRender` provides a function that +encodes values of type `a` to lazy `ByteString`s. + +In the case of `JSON`, this is easily dealt with! For any type `a` with a +`ToJSON` instance, we can render values of that type to JSON using +`Data.Aeson.encode`. + +``` haskell +instance ToJSON a => MimeRender JSON a where + mimeRender _ = encode +``` + +And now the `MimeUnrender` class, which lets us extract values from lazy +`ByteString`s, alternatively failing with an error string. + +``` haskell +class Accept ctype => MimeUnrender ctype a where + mimeUnrender :: Proxy ctype -> ByteString -> Either String a + -- alternatively: + mimeUnrender :: Proxy ctype -> (ByteString -> Either String a) +``` + +We don't have much work to do there either, `Data.Aeson.eitherDecode` is +precisely what we need. However, it only allows arrays and objects as toplevel +JSON values and this has proven to get in our way more than help us so we wrote +our own little function around *aeson* and *attoparsec* that allows any type of +JSON value at the toplevel of a "JSON document". Here's the definition in case +you are curious. + +> eitherDecodeLenient :: FromJSON a => ByteString -> Either String a +> eitherDecodeLenient input = do +> v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input) +> parseEither parseJSON v + +This function is exactly what we need for our `MimeUnrender` instance. + +``` haskell +instance FromJSON a => MimeUnrender JSON a where + mimeUnrender _ = eitherDecodeLenient +``` + +And this is all the code that lets you use `JSON` for with `ReqBody`, `Get`, +`Post` and friends. We can check our understanding by implementing support +for an `HTML` content type, so that users of your webservice can access an +HTML representation of the data they want, ready to be included in any HTML +document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), simply by adding `Accept: +text/html` to their request headers. + +Case-studies: *servant-blaze* and *servant-lucid* +------------------------------------------------- + +These days, most of the haskellers who write their HTML UIs directly from +Haskell use either [blaze-html](http://hackage.haskell.org/package/blaze-html) +or [lucid](http://hackage.haskell.org/package/lucid). The best option for +*servant* is obviously to support both (and hopefully other templating +solutions!). + +> data HTMLLucid + +Once again, the data type is just there as a symbol for the encoding/decoding +functions, except that this time we will only worry about encoding since +*blaze-html* and *lucid* don't provide a way to extract data from HTML. + +Both packages also have the same `Accept` instance for their `HTMLLucid` type. + +> instance Accept HTMLLucid where +> contentType _ = "text" // "html" /: ("charset", "utf-8") + +Note that this instance uses the `(/:)` operator from *http-media* which lets +us specify additional information about a content-type, like the charset here. + +The rendering instances for both packages both call similar functions that take +types with an appropriate instance to an "abstract" HTML representation and +then write that to a `ByteString`. + +For *lucid*: + +> instance ToHtml a => MimeRender HTMLLucid a where +> mimeRender _ = renderBS . toHtml +> +> -- let's also provide an instance for lucid's +> -- 'Html' wrapper. +> instance MimeRender HTMLLucid (Html a) where +> mimeRender _ = renderBS + +For *blaze-html*: + +> -- For this tutorial to compile 'HTMLLucid' and 'HTMLBlaze' have to be +> -- distinct. Usually you would stick to one html rendering library and then +> -- you can go with one 'HTML' type. +> data HTMLBlaze +> +> instance Accept HTMLBlaze where +> contentType _ = "text" // "html" /: ("charset", "utf-8") +> +> instance ToMarkup a => MimeRender HTMLBlaze a where +> mimeRender _ = renderHtml . Text.Blaze.Html.toHtml +> +> -- while we're at it, just like for lucid we can +> -- provide an instance for rendering blaze's 'Html' type +> instance MimeRender HTMLBlaze Text.Blaze.Html.Html where +> mimeRender _ = renderHtml + +Both [servant-blaze](http://hackage.haskell.org/package/servant-blaze) and +[servant-lucid](http://hackage.haskell.org/package/servant-lucid) let you use +`HTMLLucid` in any content type list as long as you provide an instance of the +appropriate class (`ToMarkup` for *blaze-html*, `ToHtml` for *lucid*). + +We can now write webservice that uses *servant-lucid* to show the `HTMLLucid` +content type in action. First off, imports and pragmas as usual. + +We will be serving the following API: + +> type PersonAPI = "persons" :> Get '[JSON, HTMLLucid] [Person] + +where `Person` is defined as follows: + +> data Person = Person +> { firstName :: String +> , lastName :: String +> } deriving Generic -- for the JSON instance +> +> instance ToJSON Person + +Now, let's teach *lucid* how to render a `Person` as a row in a table, and then +a list of `Person`s as a table with a row per person. + +> -- HTML serialization of a single person +> instance ToHtml Person where +> toHtml person = +> tr_ $ do +> td_ (toHtml $ firstName person) +> td_ (toHtml $ lastName person) +> +> -- do not worry too much about this +> toHtmlRaw = toHtml +> +> -- HTML serialization of a list of persons +> instance ToHtml [Person] where +> toHtml persons = table_ $ do +> tr_ $ do +> th_ "first name" +> th_ "last name" +> +> -- this just calls toHtml on each person of the list +> -- and concatenates the resulting pieces of HTML together +> foldMap toHtml persons +> +> toHtmlRaw = toHtml + +We create some `Person` values and serve them as a list: + +> persons :: [Person] +> persons = +> [ Person "Isaac" "Newton" +> , Person "Albert" "Einstein" +> ] +> +> personAPI :: Proxy PersonAPI +> personAPI = Proxy +> +> server4 :: Server PersonAPI +> server4 = return persons +> +> app2 :: Application +> app2 = serve personAPI server4 + +And we're good to go. You can run this example with `dist/build/tutorial/tutorial 4`. + +``` bash + $ curl http://localhost:8081/persons + [{"lastName":"Newton","firstName":"Isaac"},{"lastName":"Einstein","firstName":"Albert"}] + $ curl -H 'Accept: text/html' http://localhost:8081/persons +
first namelast name
IsaacNewton
AlbertEinstein
+ # or just point your browser to http://localhost:8081/persons +``` + +The `EitherT ServantErr IO` monad +================================= + +At the heart of the handlers is the monad they run in, namely `EitherT +ServantErr IO`. One might wonder: why this monad? The answer is that it is the +simplest monad with the following properties: + +- it lets us both return a successful result (with the `Right` branch of +`Either`) or "fail" with a descriptive error (with the `Left` branch of +`Either`); +- it lets us perform IO, which is absolutely vital since most webservices exist +as interfaces to databases that we interact with in `IO`; + +Let's recall some definitions. + +``` haskell +-- from the Prelude +data Either e a = Left e | Right a + +-- from the 'either' package at +-- http://hackage.haskell.org/package/either-4.3.3.2/docs/Control-Monad-Trans-Either.html +newtype EitherT e m a + = EitherT { runEitherT :: m (Either e a) } +``` + +In short, this means that a handler of type `EitherT ServantErr IO a` is simply +equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO +action that either returns an error or a result. + +The aforementioned `either` package is worth taking a look at. Perhaps most +importantly: + +``` haskell +left :: Monad m => e -> EitherT e m a +``` +Allows you to return an error from your handler (whereas `return` is enough to +return a success). + +Most of what you'll be doing in your handlers is running some IO and, +depending on the result, you might sometimes want to throw an error of some +kind and abort early. The next two sections cover how to do just that. + +Performing IO +------------- + +Another important instance from the list above is `MonadIO m => MonadIO (EitherT e m)`. [`MonadIO`](http://hackage.haskell.org/package/transformers-0.4.3.0/docs/Control-Monad-IO-Class.html) is a class from the *transformers* package defined as: + +``` haskell +class Monad m => MonadIO m where + liftIO :: IO a -> m a +``` + +Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `EitherT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`: + +> type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent +> +> newtype FileContent = FileContent +> { content :: String } +> deriving Generic +> +> instance ToJSON FileContent +> +> server5 :: Server IOAPI1 +> server5 = do +> filecontent <- liftIO (readFile "myfile.txt") +> return (FileContent filecontent) + +Failing, through `ServantErr` +----------------------------- + +If you want to explicitly fail at providing the result promised by an endpoint +using the appropriate HTTP status code (not found, unauthorized, etc) and some +error message, all you have to do is use the `left` function mentioned above +and provide it with the appropriate value of type `ServantErr`, which is +defined as: + +``` haskell +data ServantErr = ServantErr + { errHTTPCode :: Int + , errReasonPhrase :: String + , errBody :: ByteString -- lazy bytestring + , errHeaders :: [Header] + } +``` + +Many standard values are provided out of the box by the `Servant.Server` +module. If you want to use these values but add a body or some headers, just +use record update syntax: + +> failingHandler :: EitherT ServantErr IO () +> failingHandler = left myerr +> +> where myerr :: ServantErr +> myerr = err503 { errBody = "Sorry dear user." } + +Here's an example where we return a customised 404-Not-Found error message in +the response body if "myfile.txt" isn't there: + +> server6 :: Server IOAPI1 +> server6 = do +> exists <- liftIO (doesFileExist "myfile.txt") +> if exists +> then liftIO (readFile "myfile.txt") >>= return . FileContent +> else left custom404Err +> +> where custom404Err = err404 { errBody = "myfile.txt just isn't there, please leave this server alone." } + +Let's run this server (`dist/build/tutorial/tutorial 5`) and +query it, first without the file and then with the file. + +``` bash + $ curl --verbose http://localhost:8081/myfile.txt + [snip] + * Connected to localhost (127.0.0.1) port 8081 (#0) + > GET /myfile.txt HTTP/1.1 + > User-Agent: curl/7.30.0 + > Host: localhost:8081 + > Accept: */* + > + < HTTP/1.1 404 Not Found + [snip] + myfile.txt just isnt there, please leave this server alone. + + $ echo Hello > myfile.txt + + $ curl --verbose http://localhost:8081/myfile.txt + [snip] + * Connected to localhost (127.0.0.1) port 8081 (#0) + > GET /myfile.txt HTTP/1.1 + > User-Agent: curl/7.30.0 + > Host: localhost:8081 + > Accept: */* + > + < HTTP/1.1 200 OK + [snip] + < Content-Type: application/json + [snip] + {"content":"Hello\n"} +``` + +Response headers +================ + +To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html). +Note that this changes the type of your API, as we can see in the following example: + +> type MyHandler = Get '[JSON] (Headers '[Header "X-An-Int" Int] User) +> +> myHandler :: Server MyHandler +> myHandler = return $ addHeader 1797 albert + + +Serving static files +==================== + +*servant-server* also provides a way to just serve the content of a directory +under some path in your web API. As mentioned earlier in this document, the +`Raw` combinator can be used in your APIs to mean "plug here any WAI +application". Well, servant-server provides a function to get a file and +directory serving WAI application, namely: + +``` haskell +-- exported by Servant and Servant.Server +serveDirectory :: FilePath -> Server Raw +``` + +`serveDirectory`'s argument must be a path to a valid directory. You can see an +example below, runnable with `dist/build/tutorial/tutorial 6` +(you **must** run it from within the *servant-examples/* directory!), which is +a webserver that serves the various bits of code covered in this +getting-started. + +The API type will be the following. + +> type CodeAPI = "code" :> Raw + +And the server: + +> codeAPI :: Proxy CodeAPI +> codeAPI = Proxy + +> server7 :: Server CodeAPI +> server7 = serveDirectory "tutorial" +> +> app3 :: Application +> app3 = serve codeAPI server7 + +This server will match any request whose path starts with `/code` and will look for a file at the path described by the rest of the request path, inside the *tutorial/* directory of the path you run the program from. + +In other words: + +- If a client requests `/code/foo.txt`, the server will look for a file at `./tutorial/foo.txt` (and fail) +- If a client requests `/code/T1.hs`, the server will look for a file at `./tutorial/T1.hs` (and succeed) +- If a client requests `/code/foo/bar/baz/movie.mp4`, the server will look for a file at `./tutorial/foo/bar/baz/movie.mp4` (and fail) + +Here is our little server in action. + +``` haskell +$ curl http://localhost:8081/code/T1.hs +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TypeOperators #-} +module T1 where + +import Data.Aeson +import Data.Time.Calendar +import GHC.Generics +import Network.Wai +import Servant + +data User = User + { name :: String + , age :: Int + , email :: String + , registration_date :: Day + } deriving (Eq, Show, Generic) + +-- orphan ToJSON instance for Day. necessary to derive one for User +instance ToJSON Day where + -- display a day in YYYY-mm-dd format + toJSON d = toJSON (showGregorian d) + +instance ToJSON User + +type UserAPI = "users" :> Get '[JSON] [User] + +users :: [User] +users = + [ User "Isaac Newton" 372 "isaac@newton.co.uk" (fromGregorian 1683 3 1) + , User "Albert Einstein" 136 "ae@mc2.org" (fromGregorian 1905 12 1) + ] + +userAPI :: Proxy UserAPI +userAPI = Proxy + +server :: Server UserAPI +server = return users + +app :: Application +app = serve userAPI server +$ curl http://localhost:8081/code/tutorial.hs +import Network.Wai +import Network.Wai.Handler.Warp +import System.Environment + +import qualified T1 +import qualified T2 +import qualified T3 +import qualified T4 +import qualified T5 +import qualified T6 +import qualified T7 +import qualified T9 +import qualified T10 + +app :: String -> (Application -> IO ()) -> IO () +app n f = case n of + "1" -> f T1.app + "2" -> f T2.app + "3" -> f T3.app + "4" -> f T4.app + "5" -> f T5.app + "6" -> f T6.app + "7" -> f T7.app + "8" -> f T3.app + "9" -> T9.writeJSFiles >> f T9.app + "10" -> f T10.app + _ -> usage + +main :: IO () +main = do + args <- getArgs + case args of + [n] -> app n (run 8081) + _ -> usage + +usage :: IO () +usage = do + putStrLn "Usage:\t tutorial N" + putStrLn "\t\twhere N is the number of the example you want to run." + +$ curl http://localhost:8081/foo +not found +``` + +Nested APIs +=========== + +Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example: + +> type UserAPI3 = -- view the user with given userid, in JSON +> Capture "userid" Int :> Get '[JSON] User +> +> :<|> -- delete the user with given userid. empty response +> Capture "userid" Int :> Delete '[] () + +We can instead factor out the `userid`: + +> type UserAPI4 = Capture "userid" Int :> +> ( Get '[JSON] User +> :<|> Delete '[] () +> ) + +However, you have to be aware that this has an effect on the type of the corresponding `Server`: + +``` haskell +Server UserAPI3 = (Int -> EitherT ServantErr IO User) + :<|> (Int -> EitherT ServantErr IO ()) + +Server UserAPI4 = Int -> ( EitherT ServantErr IO User + :<|> EitherT ServantErr IO () + ) +``` + +In the first case, each handler receives the *userid* argument. In the latter, +the whole `Server` takes the *userid* and has handlers that are just computations in `EitherT`, with no arguments. In other words: + +> server8 :: Server UserAPI3 +> server8 = getUser :<|> deleteUser +> +> where getUser :: Int -> EitherT ServantErr IO User +> getUser _userid = error "..." +> +> deleteUser :: Int -> EitherT ServantErr IO () +> deleteUser _userid = error "..." +> +> -- notice how getUser and deleteUser +> -- have a different type! no argument anymore, +> -- the argument directly goes to the whole Server +> server9 :: Server UserAPI4 +> server9 userid = getUser userid :<|> deleteUser userid +> +> where getUser :: Int -> EitherT ServantErr IO User +> getUser = error "..." +> +> deleteUser :: Int -> EitherT ServantErr IO () +> deleteUser = error "..." + +Note that there's nothing special about `Capture` that lets you "factor it out": this can be done with any combinator. Here are a few examples of APIs with a combinator factored out for which we can write a perfectly valid `Server`. + +> -- we just factor out the "users" path fragment +> type API1 = "users" :> +> ( Get '[JSON] [User] -- user listing +> :<|> Capture "userid" Int :> Get '[JSON] User -- view a particular user +> ) +> +> -- we factor out the Request Body +> type API2 = ReqBody '[JSON] User :> +> ( Get '[JSON] User -- just display the same user back, don't register it +> :<|> Post '[JSON] () -- register the user. empty response +> ) +> +> -- we factor out a Header +> type API3 = Header "Authorization" Token :> +> ( Get '[JSON] SecretData -- get some secret data, if authorized +> :<|> ReqBody '[JSON] SecretData :> Post '[] () -- add some secret data, if authorized +> ) +> +> newtype Token = Token ByteString +> newtype SecretData = SecretData ByteString + +This approach lets you define APIs modularly and assemble them all into one big API type only at the end. + +> type UsersAPI = +> Get '[JSON] [User] -- list users +> :<|> ReqBody '[JSON] User :> Post '[] () -- add a user +> :<|> Capture "userid" Int :> +> ( Get '[JSON] User -- view a user +> :<|> ReqBody '[JSON] User :> Put '[] () -- update a user +> :<|> Delete '[] () -- delete a user +> ) +> +> usersServer :: Server UsersAPI +> usersServer = getUsers :<|> newUser :<|> userOperations +> +> where getUsers :: EitherT ServantErr IO [User] +> getUsers = error "..." +> +> newUser :: User -> EitherT ServantErr IO () +> newUser = error "..." +> +> userOperations userid = +> viewUser userid :<|> updateUser userid :<|> deleteUser userid +> +> where +> viewUser :: Int -> EitherT ServantErr IO User +> viewUser = error "..." +> +> updateUser :: Int -> User -> EitherT ServantErr IO () +> updateUser = error "..." +> +> deleteUser :: Int -> EitherT ServantErr IO () +> deleteUser = error "..." + +> type ProductsAPI = +> Get '[JSON] [Product] -- list products +> :<|> ReqBody '[JSON] Product :> Post '[] () -- add a product +> :<|> Capture "productid" Int :> +> ( Get '[JSON] Product -- view a product +> :<|> ReqBody '[JSON] Product :> Put '[] () -- update a product +> :<|> Delete '[] () -- delete a product +> ) +> +> data Product = Product { productId :: Int } +> +> productsServer :: Server ProductsAPI +> productsServer = getProducts :<|> newProduct :<|> productOperations +> +> where getProducts :: EitherT ServantErr IO [Product] +> getProducts = error "..." +> +> newProduct :: Product -> EitherT ServantErr IO () +> newProduct = error "..." +> +> productOperations productid = +> viewProduct productid :<|> updateProduct productid :<|> deleteProduct productid +> +> where +> viewProduct :: Int -> EitherT ServantErr IO Product +> viewProduct = error "..." +> +> updateProduct :: Int -> Product -> EitherT ServantErr IO () +> updateProduct = error "..." +> +> deleteProduct :: Int -> EitherT ServantErr IO () +> deleteProduct = error "..." + +> type CombinedAPI = "users" :> UsersAPI +> :<|> "products" :> ProductsAPI +> +> server10 :: Server CombinedAPI +> server10 = usersServer :<|> productsServer + +Finally, we can realize the user and product APIs are quite similar and abstract that away: + +> -- API for values of type 'a' +> -- indexed by values of type 'i' +> type APIFor a i = +> Get '[JSON] [a] -- list 'a's +> :<|> ReqBody '[JSON] a :> Post '[] () -- add an 'a' +> :<|> Capture "id" i :> +> ( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i' +> :<|> ReqBody '[JSON] a :> Put '[] () -- update an 'a' +> :<|> Delete '[] () -- delete an 'a' +> ) +> +> -- Build the appropriate 'Server' +> -- given the handlers of the right type. +> serverFor :: EitherT ServantErr IO [a] -- handler for listing of 'a's +> -> (a -> EitherT ServantErr IO ()) -- handler for adding an 'a' +> -> (i -> EitherT ServantErr IO a) -- handler for viewing an 'a' given its identifier of type 'i' +> -> (i -> a -> EitherT ServantErr IO ()) -- updating an 'a' with given id +> -> (i -> EitherT ServantErr IO ()) -- deleting an 'a' given its id +> -> Server (APIFor a i) +> serverFor = error "..." +> -- implementation left as an exercise. contact us on IRC +> -- or the mailing list if you get stuck! + +Using another monad for your handlers +===================================== + +Remember how `Server` turns combinators for HTTP methods into `EitherT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym. + +``` haskell +type Server api = ServerT api (EitherT ServantErr IO) +``` + +`ServerT` is the actual type family that computes the required types for the handlers that's part of the `HasServer` class. It's like `Server` except that it takes a third parameter which is the monad you want your handlers to run in, or more generally the return types of your handlers. This third parameter is used for specifying the return type of the handler for an endpoint, e.g when computing `ServerT (Get '[JSON] Person) SomeMonad`. The result would be `SomeMonad Person`. + +The first and main question one might have then is: how do we write handlers that run in another monad? How can we "bring back" the value from a given monad into something *servant* can understand? + +Natural transformations +----------------------- + +If we have a function that gets us from an `m a` to an `n a`, for any `a`, what +do we have? + +``` haskell +newtype m :~> n = Nat { unNat :: forall a. m a -> n a} + +-- For example +-- listToMaybeNat ::`[] :~> Maybe` +-- listToMaybeNat = Nat listToMaybe -- from Data.Maybe +``` +(`Nat` comes from "natural transformation", in case you're wondering.) + +So if you want to write handlers using another monad/type than `EitherT +ServantErr IO`, say the `Reader String` monad, the first thing you have to +prepare is a function: + +``` haskell +readerToEither :: Reader String :~> EitherT ServantErr IO +``` + +Let's start with `readerToEither'`. We obviously have to run the `Reader` +computation by supplying it with a `String`, like `"hi"`. We get an `a` out +from that and can then just `return` it into `EitherT`. We can then just wrap +that function with the `Nat` constructor to make it have the fancier type. + +> readerToEither' :: forall a. Reader String a -> EitherT ServantErr IO a +> readerToEither' r = return (runReader r "hi") +> +> readerToEither :: Reader String :~> EitherT ServantErr IO +> readerToEither = Nat readerToEither' + +We can write some simple webservice with the handlers running in `Reader String`. + +> type ReaderAPI = "a" :> Get '[JSON] Int +> :<|> "b" :> Get '[JSON] String +> +> readerAPI :: Proxy ReaderAPI +> readerAPI = Proxy +> +> readerServerT :: ServerT ReaderAPI (Reader String) +> readerServerT = a :<|> b +> +> where a :: Reader String Int +> a = return 1797 +> +> b :: Reader String String +> b = ask + +We unfortunately can't use `readerServerT` as an argument of `serve`, because +`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `EitherT +ServantErr IO`. But there's a simple solution to this. + +Enter `enter` +------------- + +That's right. We have just written `readerToEither`, which is exactly what we +would need to apply to the results of all handlers to make the handlers have the +right type for `serve`. Being cumbersome to do by hand, we provide a function +`enter` which takes a natural transformation between two parametrized types `m` +and `n` and a `ServerT someapi m`, and returns a `ServerT someapi n`. + +In our case, we can wrap up our little webservice by using `enter readerToEither` on our handlers. + +> readerServer :: Server ReaderAPI +> readerServer = enter readerToEither readerServerT +> +> app4 :: Application +> app4 = serve readerAPI readerServer + +And we can indeed see this webservice in action by running `dist/build/tutorial/tutorial 7`. + +``` bash +$ curl http://localhost:8081/a +1797 +$ curl http://localhost:8081/b +"hi" +``` + +Conclusion +========== + +You're now equipped to write any kind of webservice/web-application using *servant*. One thing not covered here is how to incorporate your own combinators and will be the topic of a page on the website. The rest of this document focuses on *servant-client*, *servant-jquery* and *servant-docs*. + +