{-# LANGUAGE ExistentialQuantification #-}
{- |
Module      :  Servant.Error
Copyright   :  (c) Zalora SEA 2014
License     :  BSD3
Maintainer  :  Alp Mestanogullari <alp@zalora.com>
Stability   :  experimental

Everything you'll need when doing some kind of exception handling
around your \"database operations\".
-}
module Servant.Error
  ( -- * Creating an combining 'ExceptionCatcher's
    ExceptionCatcher
  , noCatch
  , catchAnd
  , combineCatchers

  , -- * Running 'IO' actions against an 'ExceptionCatcher'
    handledWith
  ) where

import Control.Exception
import Data.Monoid

-- | A container for zero or more "exception catchers".
--
--   By exception catcher, we mean here a function that can convert
--   some precise instance of 'Exception' to the error type used in
--   your scotty app (the @e@ type in "Servant.Resource" "Servant.Service")
newtype ExceptionCatcher err =
	EC [Catcher err]

-- a handy type for representing a single function that can catch
-- some (specific) exception.
-- The dictionary for the corresponding 'Exception' instance is packed
-- along with the function in this data type.
data Catcher err = forall exc. Exception exc => Catcher (exc -> err)

-- | Don't catch any exception.
noCatch :: ExceptionCatcher err
noCatch = EC []

-- | Combine two 'ExceptionCatcher'.
--
--   The resulting 'ExceptionCatcher' will be able to catch
--   exceptions using the /catchers/ from both arguments.
combineCatchers :: ExceptionCatcher err
                -> ExceptionCatcher err
                -> ExceptionCatcher err
combineCatchers (EC c1s) (EC c2s) = EC (c1s ++ c2s)

-- | If you're into 'Monoid's, enjoy our
--   'mempty' ('noErrorHandling') and '<>' ('combineCatchers').
instance Monoid (ExceptionCatcher err) where
  mempty = noCatch

  mappend = combineCatchers

-- | Run an 'IO' action by watching for all the exceptions
--   supported by the 'ExceptionCatcher'.
--
--   If an exception is thrown somewhere in the action and is caught
--   by the catcher, it will call the function given to 'catchAnd'
--   to convert the exception value to the error type of your scotty application.
--
--   If an exception is thrown whose type isn't one for which there's a /catcher/
--   in the 'ExceptionCatcher' argument, you'll have to catch it yourself or let it
--   be sent to the default handler.
--
--   Since 'Servant.Service.runService' sets up a 'defaultHandler', you'll most
--   likely want to directly use 'raiseIfExc' which 'raise's the error value
--   you got from the exception if some exception is thrown or just run some
--   scotty action if everything went smoothly. This means writing a handler
--   that'll set up a response with the proper HTTP status and send some JSON
--   to the client with an informative error message, which... isn't a bad thing :-)
handledWith :: IO a -> ExceptionCatcher err -> IO (Either err a)
handledWith act (EC hs) = fmap Right act `catches` map runCatcher hs

  where runCatcher :: Catcher err -> Handler (Either err a)
        runCatcher (Catcher f) = Handler $ return . Left . f

-- | Create an 'ExceptionCatcher' from a function.
--
--   This will make the catcher aware of exceptions of type @except@
--   so that when you'll run a catcher against an 'IO' action
--   using 'raiseIfExc' or 'handledWith', if an exception of this type
--   is thrown, it will be caught and converted to the error type of your
--   web application using the provided function.
catchAnd :: Exception except
         => (except -> err) 
         -> ExceptionCatcher err
catchAnd f = EC [catcher]
  where catcher = Catcher f