{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# OPTIONS_HADDOCK prune not-home #-}

{- |
Copyright   : (c) 2020-2021 Tim Emiola
SPDX-License-Identifier: BSD3
Maintainer  : Tim Emiola <[email protected] >

Provides an instance of 'Proc' that launches @postgres@ as a @tmp proc@.

The instance this module provides can be used in integration tests as is.

It's also possible to write other instances that launch @postgres@ in different
ways; for those, this instance can be used as a reference example.
-}
module System.TmpProc.Docker.Postgres
  ( -- * 'Proc' instance
    TmpPostgres (..)

    -- * Useful definitions
  , aProc
  , aHandle

    -- * Re-exports
  , module System.TmpProc
  )
where

import Control.Exception (catch)
import qualified Data.ByteString.Char8 as C8
import Data.String (fromString)
import Data.Text (Text)
import qualified Data.Text as Text
import Database.PostgreSQL.Simple
  ( Connection
  , SqlError
  , close
  , connectPostgreSQL
  , execute_
  )
import System.TmpProc
  ( Connectable (..)
  , HList (..)
  , HandlesOf
  , HostIpAddress
  , Pinged (..)
  , Proc (..)
  , ProcHandle (..)
  , SvcURI
  , only
  , startupAll
  , withTmpConn
  )


-- | A singleton 'HList' containing a 'TmpPostgres'.
aProc :: HList '[TmpPostgres]
aProc :: HList '[TmpPostgres]
aProc = forall x. x -> HList '[x]
only forall a b. (a -> b) -> a -> b
$ [Text] -> TmpPostgres
TmpPostgres []


-- | An 'HList' that contains the handle created from 'aProc'.
aHandle :: IO (HandlesOf '[TmpPostgres])
aHandle :: IO (HandlesOf '[TmpPostgres])
aHandle = forall (procs :: [*]).
AreProcs procs =>
HList procs -> IO (HandlesOf procs)
startupAll HList '[TmpPostgres]
aProc


{- | Provides the capability to launch a Postgres database as a @tmp proc@.

The constructor receives the names of the tables to be dropped on 'reset'.
-}
newtype TmpPostgres = TmpPostgres [Text]


-- | Specifies how to run @postgres@ as a @tmp proc@.
instance Proc TmpPostgres where
  type Image TmpPostgres = "postgres:10.6"
  type Name TmpPostgres = "a-postgres-db"
  uriOf :: Text -> ByteString
uriOf = Text -> ByteString
mkUri'
  runArgs :: [Text]
runArgs = [Text]
runArgs'
  ping :: ProcHandle TmpPostgres -> IO Pinged
ping = forall a. IO a -> IO Pinged
toPinged forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> IO Connection
connectPostgreSQL forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. ProcHandle a -> ByteString
hUri
  reset :: ProcHandle TmpPostgres -> IO ()
reset = ProcHandle TmpPostgres -> IO ()
reset'


-- | Specifies how to connect to a tmp @postgres@ db.
instance Connectable TmpPostgres where
  type Conn TmpPostgres = Connection
  openConn :: ProcHandle TmpPostgres -> IO (Conn TmpPostgres)
openConn = ByteString -> IO Connection
connectPostgreSQL forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. ProcHandle a -> ByteString
hUri
  closeConn :: Conn TmpPostgres -> IO ()
closeConn = Connection -> IO ()
close


-- | Makes a uri whose password matches the one specified in 'runArgs''.
mkUri' :: HostIpAddress -> SvcURI
mkUri' :: Text -> ByteString
mkUri' Text
ip =
  ByteString
"postgres://postgres:"
    forall a. Semigroup a => a -> a -> a
<> ByteString
dbPassword
    forall a. Semigroup a => a -> a -> a
<> ByteString
"@"
    forall a. Semigroup a => a -> a -> a
<> [Char] -> ByteString
C8.pack (Text -> [Char]
Text.unpack Text
ip)
    forall a. Semigroup a => a -> a -> a
<> ByteString
"/postgres"


dbPassword :: C8.ByteString
dbPassword :: ByteString
dbPassword = ByteString
"mysecretpassword"


-- | Match the password used in 'mkUri''.
runArgs' :: [Text]
runArgs' :: [Text]
runArgs' =
  [ Text
"-e"
  , Text
"POSTGRES_PASSWORD=" forall a. Semigroup a => a -> a -> a
<> [Char] -> Text
Text.pack (ByteString -> [Char]
C8.unpack ByteString
dbPassword)
  ]


toPinged :: IO a -> IO Pinged
toPinged :: forall a. IO a -> IO Pinged
toPinged IO a
action =
  ( (IO a
action forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> forall (f :: * -> *) a. Applicative f => a -> f a
pure Pinged
OK)
      forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` (\(SqlError
_ :: SqlError) -> forall (f :: * -> *) a. Applicative f => a -> f a
pure Pinged
NotOK)
  )
    forall e a. Exception e => IO a -> (e -> IO a) -> IO a
`catch` (\(IOError
_ :: IOError) -> forall (f :: * -> *) a. Applicative f => a -> f a
pure Pinged
NotOK)


-- | Empty all rows in the tables, if any are specified.
reset' :: ProcHandle TmpPostgres -> IO ()
reset' :: ProcHandle TmpPostgres -> IO ()
reset' handle :: ProcHandle TmpPostgres
handle@(ProcHandle {TmpPostgres
hProc :: forall a. ProcHandle a -> a
hProc :: TmpPostgres
hProc}) =
  let go :: TmpPostgres -> IO ()
go (TmpPostgres []) = forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
      go (TmpPostgres [Text]
tables) = forall a b.
Connectable a =>
ProcHandle a -> (Conn a -> IO b) -> IO b
withTmpConn ProcHandle TmpPostgres
handle forall a b. (a -> b) -> a -> b
$ \Conn TmpPostgres
c ->
        forall (t :: * -> *) (m :: * -> *) a b.
(Foldable t, Monad m) =>
(a -> m b) -> t a -> m ()
mapM_ (Connection -> Query -> IO Int64
execute_ Conn TmpPostgres
c forall b c a. (b -> c) -> (a -> b) -> a -> c
. (forall a. IsString a => [Char] -> a
fromString forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. [a] -> [a] -> [a]
(++) [Char]
"DELETE FROM ") forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Char]
Text.unpack) [Text]
tables
   in TmpPostgres -> IO ()
go TmpPostgres
hProc