packages feed

tmp-proc-example-0.5.0.0: src/TmpProc/Example2/Server.hs

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeOperators              #-}
{-|
Copyright   : (c) 2020-2021 Tim Emiola
SPDX-License-Identifier: BSD3
Maintainer  : Tim Emiola <adetokunbo@users.noreply.github.com>

Implements a demo service.

-}
module TmpProc.Example2.Server
  ( -- * Server implementation
    AppEnv(..)
  , runServer'
  , runServer
  , waiApp
  ) where

import           Control.Exception          (try, throw)
import           Control.Monad.Catch        (MonadCatch, MonadMask, MonadThrow)
import           Control.Monad.IO.Class     (MonadIO, liftIO)
import           Control.Monad.Reader       (MonadReader, ReaderT, asks,
                                             runReaderT)
import           Control.Monad.Trans.Except (ExceptT (..))
import           Network.Wai                (Application)
import           Network.Wai.Handler.Warp   (Port, run)
import           Servant.API                ((:<|>) (..))
import           Servant.Server             (Handler (..), ServerT,
                                             err401, errBody, serve, hoistServer)

import           TmpProc.Example2.Routes    (ContactsAPI, contactsAPI)
import           TmpProc.Example2.Schema    (Contact, ContactID)

import qualified TmpProc.Example2.Cache     as Cache
import qualified TmpProc.Example2.Database  as DB


{-| Runs 'waiApp' on the given port. -}
runServer' :: IO AppEnv -> Port -> IO ()
runServer' mkEnv port = mkEnv >>= run port . waiApp


{-| An 'Application' that runs the server using the given DB and Cache. -}
waiApp :: AppEnv -> Application
waiApp env =
  let
    hoist' = Handler . ExceptT . try . runApp' env
  in
    serve contactsAPI $ hoistServer contactsAPI hoist' server


{-| Runs 'waiApp' using defaults for local development. -}
runServer :: IO ()
runServer = runServer' defaultEnv 8000


fetchContact
  :: (MonadIO m, MonadReader r m, Has DB.Locator r, Has Cache.Connection r)
  => ContactID -> m Contact
fetchContact cid = do
  cache <- grab @Cache.Connection
  (liftIO $ Cache.loadContact cache cid) >>= \case
    Just contact -> pure contact
    Nothing -> do
      db <- grab @DB.Locator
      (liftIO $ DB.fetch db cid) >>= \case
        Just contact -> liftIO (Cache.saveContact cache cid contact) >> pure contact
        Nothing -> throw $ err401 { errBody = "No Contact with this ID" }


createContact
  :: (MonadIO m, MonadReader r m, Has DB.Locator r)
  => Contact -> m ContactID
createContact contact = do
  db <- grab @DB.Locator
  liftIO $ DB.create db contact


server
  :: ( Has Cache.Connection r
     , Has DB.Locator r
     , MonadReader r m
     , MonadIO m
     )
  => ServerT ContactsAPI m
server = fetchContact :<|> createContact


-- | The application-level Monad, provides access to AppEnv via @Reader AppEnv@.
newtype App a = App
  { runApp :: ReaderT AppEnv IO a
  } deriving ( Applicative
             , Functor
             , Monad
             , MonadCatch
             , MonadMask
             , MonadThrow
             , MonadReader AppEnv
             , MonadIO
             )


instance Has DB.Locator     AppEnv  where obtain = aeDbLocator
instance Has Cache.Connection  AppEnv  where obtain = aeCacheLocator


defaultEnv :: IO AppEnv
defaultEnv = AppEnv <$> (pure DB.defaultLoc) <*> Cache.defaultConn


-- | Run a 'App' computation with the given environment.
runApp' :: AppEnv -> App a -> IO a
runApp' env = flip runReaderT env . runApp


{-| An application-level environment suitable for storing in a Reader. -}
data AppEnv = AppEnv
  { aeDbLocator    :: !(DB.Locator)
  , aeCacheLocator :: !(Cache.Connection)
  }


{- | General type class representing which @field@ is in @env@. -}
class Has field env where
  obtain :: env -> field


-- | A combinator that simplifies accessing 'Has' fields.
grab :: forall field env m . (MonadReader env m, Has field env) => m field
grab = asks $ obtain @field