120 lines
3.7 KiB
Text
120 lines
3.7 KiB
Text
# Using a custom monad
|
|
|
|
In this section we will create and API for a book shelf without any backing DB storage.
|
|
We will keep state in memory and share it between requests using `Reader` monad and `STM`.
|
|
|
|
We start with a pretty standard set of imports and definition of the model:
|
|
|
|
``` haskell
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
import Control.Concurrent (forkIO, killThread)
|
|
import Control.Concurrent.STM.TVar (TVar, newTVar, readTVar,
|
|
writeTVar)
|
|
import Control.Exception (bracket)
|
|
import Control.Monad.IO.Class (liftIO)
|
|
import Control.Monad.STM (atomically)
|
|
import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
|
|
import Data.Aeson (FromJSON, ToJSON)
|
|
import GHC.Generics (Generic)
|
|
import Network.HTTP.Client (defaultManagerSettings,
|
|
newManager)
|
|
import Network.Wai.Handler.Warp (run)
|
|
|
|
import Servant
|
|
import Servant.Client
|
|
|
|
newtype Book = Book String deriving (Show, Generic)
|
|
instance ToJSON Book
|
|
instance FromJSON Book
|
|
```
|
|
|
|
Now, let's define the API for book storage.
|
|
For the sake of simplicity we'll only have methods for getting all books and adding a new one.
|
|
|
|
``` haskell
|
|
type GetBooks = Get '[JSON] [Book]
|
|
type AddBook = ReqBody '[JSON] Book :> PostCreated '[JSON] Book
|
|
type BooksAPI = "books" :> (GetBooks :<|> AddBook)
|
|
|
|
api :: Proxy BooksAPI
|
|
api = Proxy
|
|
```
|
|
|
|
Next, we define the state and the monad to run our handlers
|
|
|
|
``` haskell
|
|
data State = State
|
|
{ books :: TVar [Book]
|
|
}
|
|
|
|
type AppM = ReaderT State Handler
|
|
```
|
|
|
|
Note that we can't use `State` monad here, because state will not be shared between requests.
|
|
|
|
We can now define handlers in terms of `AppM`...
|
|
|
|
```haskell
|
|
server :: ServerT BooksAPI AppM
|
|
server = getBooks :<|> addBook
|
|
where getBooks :: AppM [Book]
|
|
getBooks = do
|
|
State{books = p} <- ask
|
|
liftIO $ atomically $ readTVar p
|
|
|
|
addBook :: Book -> AppM Book
|
|
addBook book = do
|
|
State{books = p} <- ask
|
|
liftIO $ atomically $ readTVar p >>= writeTVar p . (book :)
|
|
return book
|
|
|
|
```
|
|
|
|
...and transform `AppM` to `Handler` by simply using `runReaderT`
|
|
|
|
``` haskell
|
|
nt :: State -> AppM a -> Handler a
|
|
nt s x = runReaderT x s
|
|
|
|
app :: State -> Application
|
|
app s = serve api $ hoistServer api (nt s) server
|
|
```
|
|
|
|
Finally, we end up with the following program
|
|
|
|
``` haskell
|
|
main :: IO ()
|
|
main = do
|
|
let port = 8080
|
|
mgr <- newManager defaultManagerSettings
|
|
initialBooks <- atomically $ newTVar []
|
|
let runApp = run port $ app $ State initialBooks
|
|
bracket (forkIO runApp) killThread $ \_ -> do
|
|
let getBooksClient :<|> addBookClient = client api
|
|
let printBooks = getBooksClient >>= liftIO . print
|
|
_ <- flip runClientM (mkClientEnv mgr (BaseUrl Http "localhost" port "")) $ do
|
|
_ <- printBooks
|
|
_ <- addBookClient $ Book "Harry Potter and the Order of the Phoenix"
|
|
_ <- printBooks
|
|
_ <- addBookClient $ Book "To Kill a Mockingbird"
|
|
_ <- printBooks
|
|
_ <- addBookClient $ Book "The Picture of Dorian Gray"
|
|
printBooks
|
|
return ()
|
|
```
|
|
|
|
When run, it outputs the following:
|
|
|
|
```
|
|
Running cookbook-using-custom-monad...
|
|
[]
|
|
[Book "Harry Potter and the Order of the Phoenix"]
|
|
[Book "To Kill a Mockingbird",Book "Harry Potter and the Order of the Phoenix"]
|
|
[Book "The Picture of Dorian Gray",Book "To Kill a Mockingbird",Book "Harry Potter and the Order of the Phoenix"]
|
|
```
|
|
|
|
To use `Raw` endpoints, look at the
|
|
[servant-rawm](http://hackage.haskell.org/package/servant-rawm) package.
|