{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
-- | This example is very similar to @./Server.hs@, but it is making use of
-- 'EnvelopeT' instead of just 'Envelope'.
module Main where
import Control.Monad.IO.Class (MonadIO(liftIO))
import Data.Char (toLower)
import Data.Proxy (Proxy(Proxy))
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
import Servant (Handler, (:<|>)((:<|>)), ServerT, serve)
import Servant.Checked.Exceptions
( Envelope
, EnvelopeT
, IsMember
, pureErrEnvelope
, pureSuccEnvelope
, relaxEnvT
, runEnvelopeT
, throwErrEnvT
)
import Api
( Api
, BadSearchTermErr(BadSearchTermErr)
, IncorrectCapitalization(IncorrectCapitalization)
, SearchQuery(SearchQuery)
, SearchResponse
, port
)
-- | This is our server root for the 'ServerT' for 'Api'. We only have two
-- handlers, 'postStrictSearch' and 'postLaxSearch'.
serverRoot :: ServerT Api Handler
serverRoot = postStrictSearch :<|> postLaxSearch :<|> postNoErrSearch
-- | This is the handler for 'Api.ApiStrictSearch'.
postStrictSearch
:: SearchQuery
-> Handler (Envelope '[BadSearchTermErr, IncorrectCapitalization] SearchResponse)
postStrictSearch query =
runEnvelopeT $ do
res <- checkQueryInDb query
relaxEnvT $ doubleCheckCapitalization query
doubleCheckCapitalizationGeneral query
pure res
-- | Check that the input 'SearchQuery' is in the DB. (The DB is just a list
-- of greetings.)
--
-- If the input 'SearchQuery' is in the DB, return the string @"greeting"@.
-- Otherwise, throw an envelope error 'BadSearchTermErr'.
--
-- Note that this function says that it might also return an
-- 'IncorrectCapitalization' error, but it doesn't actually. This is just for
-- demonstration.
--
-- Also note that our underlying Monad here is 'Handler'.
checkQueryInDb
:: SearchQuery
-> EnvelopeT '[BadSearchTermErr, IncorrectCapitalization] Handler SearchResponse
checkQueryInDb (SearchQuery query) = do
liftIO $ putStrLn "querying DB..."
let lowerQuery = fmap toLower query
case lowerQuery `elem` ["hello", "goodbye", "goodnight"] of
True -> pure "greeting"
False -> throwErrEnvT BadSearchTermErr
-- | Check the first letter of 'SearchQuery' to make sure that it is lowercase.
--
-- If it is not lowercase, throw 'IncorrectCapitalization'.
--
-- Note that 'relaxEnvT' needs to be called on the result of this to use it in
-- do notation with the above 'checkQueryInDb'. This is because the result of
-- this is technically a different monad than the result of 'checkQueryInDb',
-- because the error types are different.
--
-- Also note that since we only use 'MonadIO' for this, our underlying monad is
-- polymorphic. We don't specialize it to 'Handler'.
doubleCheckCapitalization
:: MonadIO m => SearchQuery -> EnvelopeT '[IncorrectCapitalization] m ()
doubleCheckCapitalization (SearchQuery []) = liftIO $ putStrLn "search query empty"
doubleCheckCapitalization (SearchQuery (q:query)) =
if toLower q == q
then pure ()
else throwErrEnvT IncorrectCapitalization
-- | This is just like 'doubleCheckCapitalization' above, but we use the
-- 'IsMember' constraint to make this function more general. In
-- 'postStrictSearch', you can see that we don't have to call 'relaxEnvT' when
-- using this function.
doubleCheckCapitalizationGeneral
:: IsMember IncorrectCapitalization es => SearchQuery -> EnvelopeT es Handler ()
doubleCheckCapitalizationGeneral _ = throwErrEnvT IncorrectCapitalization
-- | This doesn't do anything interesting.
--
-- See 'postLaxSearch' in @./Server.hs@ for an example using 'Envelope'.
postLaxSearch
:: SearchQuery
-> Handler (Envelope '[BadSearchTermErr] SearchResponse)
postLaxSearch _ = pureSuccEnvelope "good"
-- | This doesn't do anything interesting.
--
-- See 'postNoErrSearch' in @./Server.hs@ for an example using 'Envelope'.
postNoErrSearch :: SearchQuery -> Handler (Envelope '[] SearchResponse)
postNoErrSearch (SearchQuery _) = pureSuccEnvelope "good"
-- | Create a WAI 'Application'.
app :: Application
app = serve (Proxy :: Proxy Api) serverRoot
-- | Run the WAI 'Application' using 'run' on the port defined by 'port'.
main :: IO ()
main = run port app