sundry tutorial improvements
This commit is contained in:
parent
14aa3ebbe3
commit
6f74172a64
5 changed files with 76 additions and 95 deletions
|
@ -94,6 +94,11 @@ exclude_patterns = ['_build', 'venv']
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
from sphinx.highlighting import lexers
|
||||||
|
from pygments.lexers import HaskellLexer
|
||||||
|
lexers['haskell ignore'] = HaskellLexer(stripnl=False)
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
# A list of ignored prefixes for module index sorting.
|
||||||
#modindex_common_prefix = []
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ Jinja2==2.8
|
||||||
livereload==2.4.1
|
livereload==2.4.1
|
||||||
MarkupSafe==0.23
|
MarkupSafe==0.23
|
||||||
pathtools==0.1.2
|
pathtools==0.1.2
|
||||||
Pygments==2.1
|
Pygments==2.1.1
|
||||||
pytz==2015.7
|
pytz==2015.7
|
||||||
PyYAML==3.11
|
PyYAML==3.11
|
||||||
recommonmark==0.4.0
|
recommonmark==0.4.0
|
||||||
|
|
|
@ -23,8 +23,7 @@ Usage: tutorial N
|
||||||
where N is the number of the example you want to run.
|
where N is the number of the example you want to run.
|
||||||
```
|
```
|
||||||
|
|
||||||
A first example
|
## A first example
|
||||||
===============
|
|
||||||
|
|
||||||
Equipped with some basic knowledge about the way we represent API, let's now write our first webservice.
|
Equipped with some basic knowledge about the way we represent API, let's now write our first webservice.
|
||||||
|
|
||||||
|
@ -53,7 +52,6 @@ import Data.Aeson
|
||||||
import Data.Aeson.Types
|
import Data.Aeson.Types
|
||||||
import Data.Attoparsec.ByteString
|
import Data.Attoparsec.ByteString
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import Data.Int
|
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.String.Conversions
|
import Data.String.Conversions
|
||||||
import Data.Time.Calendar
|
import Data.Time.Calendar
|
||||||
|
@ -97,7 +95,7 @@ data User = User
|
||||||
{ name :: String
|
{ name :: String
|
||||||
, age :: Int
|
, age :: Int
|
||||||
, email :: String
|
, email :: String
|
||||||
, registration_date :: Day
|
, registrationDate :: Day
|
||||||
} deriving (Eq, Show, Generic)
|
} deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
instance ToJSON User
|
instance ToJSON User
|
||||||
|
@ -179,8 +177,7 @@ $ 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"}]
|
[{"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
|
## More endpoints
|
||||||
==============
|
|
||||||
|
|
||||||
What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON.
|
What if we want more than one endpoint? Let's add `/albert` and `/isaac` to view the corresponding users encoded in JSON.
|
||||||
|
|
||||||
|
@ -218,8 +215,7 @@ And that's it! You can run this example with
|
||||||
`dist/build/tutorial/tutorial 2` and check out the data available
|
`dist/build/tutorial/tutorial 2` and check out the data available
|
||||||
at `/users`, `/albert` and `/isaac`.
|
at `/users`, `/albert` and `/isaac`.
|
||||||
|
|
||||||
From combinators to handler arguments
|
## From combinators to handler arguments
|
||||||
=====================================
|
|
||||||
|
|
||||||
Fine, we can write trivial webservices easily, but none of the two above use
|
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`,
|
any "fancy" combinator from servant. Let's address this and use `QueryParam`,
|
||||||
|
@ -237,8 +233,8 @@ type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Posit
|
||||||
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
:<|> "marketing" :> ReqBody '[JSON] ClientInfo :> Post '[JSON] Email
|
||||||
|
|
||||||
data Position = Position
|
data Position = Position
|
||||||
{ x :: Int
|
{ xCoord :: Int
|
||||||
, y :: Int
|
, yCoord :: Int
|
||||||
} deriving Generic
|
} deriving Generic
|
||||||
|
|
||||||
instance ToJSON Position
|
instance ToJSON Position
|
||||||
|
@ -334,8 +330,7 @@ that get turned into arguments to the handlers, the type of the argument.
|
||||||
> - `QueryParams "something" a` and `MatrixParams "something" a` get turned into arguments of type `[a]`.
|
> - `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`.
|
> - `ReqBody contentTypes a` gets turned into an argument of type `a`.
|
||||||
|
|
||||||
The `FromText`/`ToText` classes
|
## The `FromHttpApiData`/`ToHttpApiData` classes
|
||||||
===============================
|
|
||||||
|
|
||||||
Wait... How does *servant* know how to decode the `Int`s from the URL? Or how
|
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
|
to decode a `ClientInfo` value from the request body? This is what this and the
|
||||||
|
@ -343,54 +338,42 @@ following two sections address.
|
||||||
|
|
||||||
`Capture`s and `QueryParam`s are represented by some textual value in URLs.
|
`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
|
`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
|
corresponding (textual) value in the request's "metadata". How types are
|
||||||
decided to provide a pair of typeclasses, `FromText` and `ToText` which just
|
decoded from headers, captures, and query params is expressed in a class
|
||||||
let you say that you can respectively *extract* or *encode* values of some type
|
`FromHttpApiData` (from the package
|
||||||
*from*/*to* text. Here are the definitions:
|
[*http-api-data*](http://hackage.haskell.org/package/http-api-data)):
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
class FromText a where
|
class FromHttpApiData a where
|
||||||
fromText :: Text -> Maybe a
|
{-# MINIMAL parseUrlPiece | parseQueryParam #-}
|
||||||
|
-- | Parse URL path piece.
|
||||||
|
parseUrlPiece :: Text -> Either Text a
|
||||||
|
parseUrlPiece = parseQueryParam
|
||||||
|
|
||||||
class ToText a where
|
-- | Parse HTTP header value.
|
||||||
toText :: a -> Text
|
parseHeader :: ByteString -> Either Text a
|
||||||
|
parseHeader = parseUrlPiece . decodeUtf8
|
||||||
|
|
||||||
|
-- | Parse query param value.
|
||||||
|
parseQueryParam :: Text -> Either Text a
|
||||||
|
parseQueryParam = parseUrlPiece
|
||||||
```
|
```
|
||||||
|
|
||||||
And as long as the type that a `Capture`/`QueryParam`/`Header`/etc will be
|
As you can see, as long as you provide either `parseUrlPiece` (for `Capture`s)
|
||||||
decoded to provides a `FromText` instance, it will Just Work. *servant*
|
or `parseQueryParam` (for `QueryParam`s), the other methods will be defined in
|
||||||
provides a decent number of instances, but here are some examples of defining
|
terms of this.
|
||||||
your own.
|
|
||||||
|
|
||||||
``` haskell
|
*http-api-data* provides a decent number of instances, helpers for defining new
|
||||||
-- A typical enumeration
|
ones, and wonderful documentation.
|
||||||
data Direction
|
|
||||||
= Up
|
|
||||||
| Down
|
|
||||||
| Left
|
|
||||||
| Right
|
|
||||||
|
|
||||||
newtype UserId = UserId Int64
|
|
||||||
```
|
|
||||||
|
|
||||||
or writing the instances by hand:
|
|
||||||
|
|
||||||
``` haskell ignore
|
|
||||||
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
|
There's not much else to say about these classes. You will need instances for
|
||||||
them when using `Capture`, `QueryParam`, `QueryParams`, `MatrixParam`,
|
them when using `Capture`, `QueryParam`, `QueryParams`, and `Header` with your
|
||||||
`MatrixParams` and `Header` with your types. You will need `FromText` instances
|
types. You will need `FromHttpApiData` instances for server-side request
|
||||||
for server-side request handlers and `ToText` instances only when using
|
handlers and `ToHttpApiData` instances only when using
|
||||||
*servant-client*, as described in the [section about deriving haskell
|
*servant-client*, as described in the [section about deriving haskell
|
||||||
functions to query an API](/tutorial/client.html).
|
functions to query an API](/tutorial/client.html).
|
||||||
|
|
||||||
Using content-types with your data types
|
## Using content-types with your data types
|
||||||
========================================
|
|
||||||
|
|
||||||
The same principle was operating when decoding request bodies from JSON, and
|
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
|
responses *into* JSON. (JSON is just the running example - you can do this with
|
||||||
|
@ -399,8 +382,8 @@ any content-type.)
|
||||||
This section introduces a couple of typeclasses provided by *servant* that make
|
This section introduces a couple of typeclasses provided by *servant* that make
|
||||||
all of this work.
|
all of this work.
|
||||||
|
|
||||||
The truth behind `JSON`
|
### The truth behind `JSON`
|
||||||
-----------------------
|
|
||||||
|
|
||||||
What exactly is `JSON`? Like the 3 other content types provided out of the box
|
What exactly is `JSON`? Like the 3 other content types provided out of the box
|
||||||
by *servant*, it's a really dumb data type.
|
by *servant*, it's a really dumb data type.
|
||||||
|
@ -464,8 +447,6 @@ And now the `MimeUnrender` class, which lets us extract values from lazy
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
class Accept ctype => MimeUnrender ctype a where
|
class Accept ctype => MimeUnrender ctype a where
|
||||||
mimeUnrender :: Proxy ctype -> ByteString -> Either String a
|
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
|
We don't have much work to do there either, `Data.Aeson.eitherDecode` is
|
||||||
|
@ -496,8 +477,7 @@ 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:
|
document, e.g. using [jQuery's `load` function](https://api.jquery.com/load/), simply by adding `Accept:
|
||||||
text/html` to their request headers.
|
text/html` to their request headers.
|
||||||
|
|
||||||
Case-studies: *servant-blaze* and *servant-lucid*
|
### Case-studies: *servant-blaze* and *servant-lucid*
|
||||||
-------------------------------------------------
|
|
||||||
|
|
||||||
These days, most of the haskellers who write their HTML UIs directly from
|
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)
|
Haskell use either [blaze-html](http://hackage.haskell.org/package/blaze-html)
|
||||||
|
@ -615,8 +595,8 @@ instance ToHtml [Person] where
|
||||||
We create some `Person` values and serve them as a list:
|
We create some `Person` values and serve them as a list:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
persons :: [Person]
|
people :: [Person]
|
||||||
persons =
|
people =
|
||||||
[ Person "Isaac" "Newton"
|
[ Person "Isaac" "Newton"
|
||||||
, Person "Albert" "Einstein"
|
, Person "Albert" "Einstein"
|
||||||
]
|
]
|
||||||
|
@ -625,7 +605,7 @@ personAPI :: Proxy PersonAPI
|
||||||
personAPI = Proxy
|
personAPI = Proxy
|
||||||
|
|
||||||
server4 :: Server PersonAPI
|
server4 :: Server PersonAPI
|
||||||
server4 = return persons
|
server4 = return people
|
||||||
|
|
||||||
app2 :: Application
|
app2 :: Application
|
||||||
app2 = serve personAPI EmptyConfig server4
|
app2 = serve personAPI EmptyConfig server4
|
||||||
|
@ -641,8 +621,7 @@ And we're good to go. You can run this example with `dist/build/tutorial/tutoria
|
||||||
# or just point your browser to http://localhost:8081/persons
|
# or just point your browser to http://localhost:8081/persons
|
||||||
```
|
```
|
||||||
|
|
||||||
The `ExceptT ServantErr IO` monad
|
## The `ExceptT ServantErr IO` monad
|
||||||
=================================
|
|
||||||
|
|
||||||
At the heart of the handlers is the monad they run in, namely `ExceptT
|
At the heart of the handlers is the monad they run in, namely `ExceptT
|
||||||
ServantErr IO`. One might wonder: why this monad? The answer is that it is the
|
ServantErr IO`. One might wonder: why this monad? The answer is that it is the
|
||||||
|
@ -660,40 +639,39 @@ Let's recall some definitions.
|
||||||
-- from the Prelude
|
-- from the Prelude
|
||||||
data Either e a = Left e | Right a
|
data Either e a = Left e | Right a
|
||||||
|
|
||||||
-- from the 'either' package at
|
-- from the 'mtl' package at
|
||||||
-- http://hackage.haskell.org/package/either-4.3.3.2/docs/Control-Monad-Trans-Either.html
|
newtype ExceptT e m a = ExceptT ( m (Either e a) )
|
||||||
newtype ExceptT e m a
|
|
||||||
= ExceptT { runEitherT :: m (Either e a) }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In short, this means that a handler of type `ExceptT ServantErr IO a` is simply
|
In short, this means that a handler of type `ExceptT ServantErr IO a` is simply
|
||||||
equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO
|
equivalent to a computation of type `IO (Either ServantErr a)`, that is, an IO
|
||||||
action that either returns an error or a result.
|
action that either returns an error or a result.
|
||||||
|
|
||||||
The aforementioned `either` package is worth taking a look at. Perhaps most
|
The module [`Control.Monad.Except`](https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Except.html#t:ExceptT)
|
||||||
importantly:
|
from which `ExceptT` comes is worth looking at.
|
||||||
|
Perhaps most importantly, `ExceptT` is an instance of `MonadError`, so
|
||||||
``` haskell ignore
|
`throwError` can be used to return an error from your handler (whereas `return`
|
||||||
left :: Monad m => e -> ExceptT e m a
|
is enough to return a success).
|
||||||
```
|
|
||||||
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,
|
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
|
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.
|
kind and abort early. The next two sections cover how to do just that.
|
||||||
|
|
||||||
Performing IO
|
### Performing IO
|
||||||
-------------
|
|
||||||
|
|
||||||
Another important instance from the list above is `MonadIO m => MonadIO (ExceptT 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:
|
Another important instance from the list above is `MonadIO m => MonadIO
|
||||||
|
(ExceptT 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 ignore
|
``` haskell ignore
|
||||||
class Monad m => MonadIO m where
|
class Monad m => MonadIO m where
|
||||||
liftIO :: IO a -> m a
|
liftIO :: IO a -> m a
|
||||||
```
|
```
|
||||||
|
|
||||||
Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type `e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of IO computation in your handlers, just use `liftIO`:
|
Obviously, the `IO` monad provides a `MonadIO` instance. Hence for any type
|
||||||
|
`e`, `ExceptT e IO` has a `MonadIO` instance. So if you want to run any kind of
|
||||||
|
IO computation in your handlers, just use `liftIO`:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent
|
type IOAPI1 = "myfile.txt" :> Get '[JSON] FileContent
|
||||||
|
@ -710,8 +688,7 @@ server5 = do
|
||||||
return (FileContent filecontent)
|
return (FileContent filecontent)
|
||||||
```
|
```
|
||||||
|
|
||||||
Failing, through `ServantErr`
|
### Failing, through `ServantErr`
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
If you want to explicitly fail at providing the result promised by an endpoint
|
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
|
using the appropriate HTTP status code (not found, unauthorized, etc) and some
|
||||||
|
@ -787,8 +764,7 @@ query it, first without the file and then with the file.
|
||||||
{"content":"Hello\n"}
|
{"content":"Hello\n"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Response headers
|
## Response headers
|
||||||
================
|
|
||||||
|
|
||||||
To add headers to your response, use [addHeader](http://hackage.haskell.org/package/servant-0.4.4/docs/Servant-API-ResponseHeaders.html).
|
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:
|
Note that this changes the type of your API, as we can see in the following example:
|
||||||
|
@ -800,9 +776,9 @@ myHandler :: Server MyHandler
|
||||||
myHandler = return $ addHeader 1797 albert
|
myHandler = return $ addHeader 1797 albert
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that the type of `addHeader x` is different than the type of `x`!
|
||||||
|
|
||||||
Serving static files
|
## Serving static files
|
||||||
====================
|
|
||||||
|
|
||||||
*servant-server* also provides a way to just serve the content of a directory
|
*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
|
under some path in your web API. As mentioned earlier in this document, the
|
||||||
|
@ -842,7 +818,9 @@ app3 :: Application
|
||||||
app3 = serve codeAPI EmptyConfig server7
|
app3 = serve codeAPI EmptyConfig 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.
|
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:
|
In other words:
|
||||||
|
|
||||||
|
@ -941,8 +919,7 @@ $ curl http://localhost:8081/foo
|
||||||
not found
|
not found
|
||||||
```
|
```
|
||||||
|
|
||||||
Nested APIs
|
## Nested APIs
|
||||||
===========
|
|
||||||
|
|
||||||
Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example:
|
Let's see how you can define APIs in a modular way, while avoiding repetition. Consider this simple example:
|
||||||
|
|
||||||
|
@ -1130,8 +1107,7 @@ serverFor = error "..."
|
||||||
-- or the mailing list if you get stuck!
|
-- or the mailing list if you get stuck!
|
||||||
```
|
```
|
||||||
|
|
||||||
Using another monad for your handlers
|
## Using another monad for your handlers
|
||||||
=====================================
|
|
||||||
|
|
||||||
Remember how `Server` turns combinators for HTTP methods into `ExceptT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym.
|
Remember how `Server` turns combinators for HTTP methods into `ExceptT ServantErr IO`? Well, actually, there's more to that. `Server` is actually a simple type synonym.
|
||||||
|
|
||||||
|
@ -1143,8 +1119,7 @@ type Server api = ServerT api (ExceptT ServantErr IO)
|
||||||
|
|
||||||
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?
|
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
|
### Natural transformations
|
||||||
-----------------------
|
|
||||||
|
|
||||||
If we have a function that gets us from an `m a` to an `n a`, for any `a`, what
|
If we have a function that gets us from an `m a` to an `n a`, for any `a`, what
|
||||||
do we have?
|
do we have?
|
||||||
|
@ -1202,8 +1177,7 @@ We unfortunately can't use `readerServerT` as an argument of `serve`, because
|
||||||
`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `ExceptT
|
`serve` wants a `Server ReaderAPI`, i.e., with handlers running in `ExceptT
|
||||||
ServantErr IO`. But there's a simple solution to this.
|
ServantErr IO`. But there's a simple solution to this.
|
||||||
|
|
||||||
Enter `enter`
|
### Enter `enter`
|
||||||
-------------
|
|
||||||
|
|
||||||
That's right. We have just written `readerToEither`, which is exactly what we
|
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
|
would need to apply to the results of all handlers to make the handlers have the
|
||||||
|
@ -1230,8 +1204,7 @@ $ curl http://localhost:8081/b
|
||||||
"hi"
|
"hi"
|
||||||
```
|
```
|
||||||
|
|
||||||
Conclusion
|
## 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*.
|
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*.
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ library
|
||||||
build-depends: base == 4.*
|
build-depends: base == 4.*
|
||||||
, base-compat
|
, base-compat
|
||||||
, text
|
, text
|
||||||
, aeson
|
, aeson >= 0.11
|
||||||
, blaze-html
|
, blaze-html
|
||||||
, directory
|
, directory
|
||||||
, blaze-markup
|
, blaze-markup
|
||||||
|
|
|
@ -17,4 +17,7 @@ extra-deps:
|
||||||
- engine-io-wai-1.0.2
|
- engine-io-wai-1.0.2
|
||||||
- control-monad-omega-0.3.1
|
- control-monad-omega-0.3.1
|
||||||
- should-not-typecheck-2.0.1
|
- should-not-typecheck-2.0.1
|
||||||
|
- markdown-unlit-0.4.0
|
||||||
|
- aeson-0.11.0.0
|
||||||
|
- fail-4.9.0.0
|
||||||
resolver: nightly-2015-10-08
|
resolver: nightly-2015-10-08
|
||||||
|
|
Loading…
Reference in a new issue