module Hasql.Connection.Settings
  ( -- * Settings
    Settings,

    -- * Constructors
    host,
    hostAndPort,
    user,
    password,
    dbname,
    applicationName,
    other,
    noPreparedStatements,
    connectionString,
  )
where

import Data.Text qualified as Text
import Hasql.Connection.Config qualified as Config
import Hasql.Platform.Prelude
import PostgresqlConnectionString qualified as ConnectionString

-- | Connection settings.
--
-- This is a monoid, so you can combine multiple settings using 'mappend' ('<>').
-- The rightmost setting takes precedence in case of conflicts.
--
-- With OverloadedStrings, you can declare settings using connection strings directly.
--
-- For example, using a key-value format:
--
-- >>> "host=localhost port=5432 user=myuser dbname=mydb" :: Settings
-- "postgresql://myuser@localhost:5432/mydb"
--
-- Or using a URI format:
--
-- >>> "postgresql://myuser@localhost:5432/mydb" :: Settings
-- "postgresql://myuser@localhost:5432/mydb"
--
-- You can achieve the same effect by constructing from a 'Text' value:
--
-- >>> connectionString "host=localhost port=5432 user=myuser dbname=mydb"
-- "postgresql://myuser@localhost:5432/mydb"
--
-- Or use the provided constructors for better type safety and clarity:
--
-- >>> hostAndPort "localhost" 5432 <> user "myuser" <> dbname "mydb"
-- "postgresql://myuser@localhost:5432/mydb"
newtype Settings
  = Settings ConnectionString.ConnectionString
  deriving newtype (Settings -> Settings -> Bool
(Settings -> Settings -> Bool)
-> (Settings -> Settings -> Bool) -> Eq Settings
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Settings -> Settings -> Bool
== :: Settings -> Settings -> Bool
$c/= :: Settings -> Settings -> Bool
/= :: Settings -> Settings -> Bool
Eq, String -> Settings
(String -> Settings) -> IsString Settings
forall a. (String -> a) -> IsString a
$cfromString :: String -> Settings
fromString :: String -> Settings
IsString, Int -> Settings -> ShowS
[Settings] -> ShowS
Settings -> String
(Int -> Settings -> ShowS)
-> (Settings -> String) -> ([Settings] -> ShowS) -> Show Settings
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Settings -> ShowS
showsPrec :: Int -> Settings -> ShowS
$cshow :: Settings -> String
show :: Settings -> String
$cshowList :: [Settings] -> ShowS
showList :: [Settings] -> ShowS
Show, NonEmpty Settings -> Settings
Settings -> Settings -> Settings
(Settings -> Settings -> Settings)
-> (NonEmpty Settings -> Settings)
-> (forall b. Integral b => b -> Settings -> Settings)
-> Semigroup Settings
forall b. Integral b => b -> Settings -> Settings
forall a.
(a -> a -> a)
-> (NonEmpty a -> a)
-> (forall b. Integral b => b -> a -> a)
-> Semigroup a
$c<> :: Settings -> Settings -> Settings
<> :: Settings -> Settings -> Settings
$csconcat :: NonEmpty Settings -> Settings
sconcat :: NonEmpty Settings -> Settings
$cstimes :: forall b. Integral b => b -> Settings -> Settings
stimes :: forall b. Integral b => b -> Settings -> Settings
Semigroup, Semigroup Settings
Settings
Semigroup Settings =>
Settings
-> (Settings -> Settings -> Settings)
-> ([Settings] -> Settings)
-> Monoid Settings
[Settings] -> Settings
Settings -> Settings -> Settings
forall a.
Semigroup a =>
a -> (a -> a -> a) -> ([a] -> a) -> Monoid a
$cmempty :: Settings
mempty :: Settings
$cmappend :: Settings -> Settings -> Settings
mappend :: Settings -> Settings -> Settings
$cmconcat :: [Settings] -> Settings
mconcat :: [Settings] -> Settings
Monoid)

-- | This instance allows interfacing with the internal Config.Config type without affecting the public API or requiring complex module hierarchies.
instance Config.Constructs Settings where
  construct :: Settings -> Config
construct (Settings ConnectionString
connectionString) =
    case Text -> ConnectionString -> Maybe (Text, ConnectionString)
ConnectionString.interceptParam Text
"no_prepared_statements" ConnectionString
connectionString of
      Just (Text
value, ConnectionString
connectionString) ->
        let noPreparedStatements :: Bool
noPreparedStatements = Text -> Bool
interpretTextAsBool Text
value
         in ConnectionString -> Bool -> Config
pack ConnectionString
connectionString Bool
noPreparedStatements
      Maybe (Text, ConnectionString)
Nothing -> ConnectionString -> Bool -> Config
pack ConnectionString
connectionString Bool
False
    where
      interpretTextAsBool :: Text -> Bool
interpretTextAsBool Text
value = case Text -> Text
Text.toLower Text
value of
        Text
"1" -> Bool
True
        Text
"true" -> Bool
True
        Text
"t" -> Bool
True
        Text
"yes" -> Bool
True
        Text
"y" -> Bool
True
        Text
"on" -> Bool
True
        Text
_ -> Bool
False

      pack :: ConnectionString -> Bool -> Config
pack ConnectionString
connectionString Bool
noPreparedStatements =
        Config.Config
          { connectionString :: ByteString
connectionString =
              let textUrl :: Text
textUrl = ConnectionString -> Text
ConnectionString.toUrl ConnectionString
connectionString
               in Text -> ByteString
encodeUtf8 Text
textUrl,
            Bool
noPreparedStatements :: Bool
noPreparedStatements :: Bool
noPreparedStatements
          }

-- * Constructors

-- | Whether prepared statements are disabled.
--
-- 'False' by default.
--
-- When 'True', even the statements marked as preparable will be executed without preparation at the cost of reduced performance.
--
-- This is useful when dealing with proxying applications like @pgbouncer@, which may be incompatible with prepared statements.
-- Consult their docs or just provide this setting to stay on the safe side.
-- It should be noted that starting from version @1.21.0@ @pgbouncer@ now does provide support for prepared statements.
noPreparedStatements :: Bool -> Settings
noPreparedStatements :: Bool -> Settings
noPreparedStatements =
  ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Bool -> ConnectionString) -> Bool -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text -> ConnectionString
ConnectionString.param Text
"no_prepared_statements" (Text -> ConnectionString)
-> (Bool -> Text) -> Bool -> ConnectionString
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text -> Bool -> Text
forall a. a -> a -> Bool -> a
bool Text
"false" Text
"true"

-- | Host domain name or IP-address.
--
-- To specify multiple alternate hosts, combine the produced settings via 'Monoid'.
host :: Text -> Settings
host :: Text -> Settings
host Text
host =
  ConnectionString -> Settings
Settings (Text -> ConnectionString
ConnectionString.host Text
host)

-- | Host domain name or IP-address and port.
--
-- Specifying a port without a host is not allowed due to how PostgreSQL handles connection strings.
--
-- This function creates a single host-port pair. To specify multiple alternate hosts, combine the results of multiple 'hostAndPort' calls using the 'Monoid' instance.
hostAndPort :: Text -> Word16 -> Settings
hostAndPort :: Text -> Word16 -> Settings
hostAndPort Text
host Word16
port =
  ConnectionString -> Settings
Settings (Text -> Word16 -> ConnectionString
ConnectionString.hostAndPort Text
host Word16
port)

-- | User name.
user :: Text -> Settings
user :: Text -> Settings
user = ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Text -> ConnectionString) -> Text -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> ConnectionString
ConnectionString.user

-- | Password.
password :: Text -> Settings
password :: Text -> Settings
password = ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Text -> ConnectionString) -> Text -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> ConnectionString
ConnectionString.password

-- | Database name.
dbname :: Text -> Settings
dbname :: Text -> Settings
dbname = ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Text -> ConnectionString) -> Text -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> ConnectionString
ConnectionString.dbname

-- | Application name.
applicationName :: Text -> Settings
applicationName :: Text -> Settings
applicationName = ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Text -> ConnectionString) -> Text -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text -> ConnectionString
ConnectionString.param Text
"application_name"

-- | Other param.
other :: Text -> Text -> Settings
other :: Text -> Text -> Settings
other Text
key = ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Text -> ConnectionString) -> Text -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Text -> ConnectionString
ConnectionString.param Text
key

-- | Construct from a connection string in either URI or key-value format.
--
-- See the [PostgreSQL documentation](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for details on the format.
--
-- If the connection string is invalid, it will be treated as empty.
connectionString :: Text -> Settings
connectionString :: Text -> Settings
connectionString = ConnectionString -> Settings
Settings (ConnectionString -> Settings)
-> (Text -> ConnectionString) -> Text -> Settings
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. (Text -> ConnectionString)
-> (ConnectionString -> ConnectionString)
-> Either Text ConnectionString
-> ConnectionString
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either (ConnectionString -> Text -> ConnectionString
forall a b. a -> b -> a
const ConnectionString
forall a. Monoid a => a
mempty) ConnectionString -> ConnectionString
forall a. a -> a
forall {k} (cat :: k -> k -> *) (a :: k). Category cat => cat a a
id (Either Text ConnectionString -> ConnectionString)
-> (Text -> Either Text ConnectionString)
-> Text
-> ConnectionString
forall b c a. (b -> c) -> (a -> b) -> a -> c
forall {k} (cat :: k -> k -> *) (b :: k) (c :: k) (a :: k).
Category cat =>
cat b c -> cat a b -> cat a c
. Text -> Either Text ConnectionString
ConnectionString.parse