packages feed

postgresql-connection-string-0.1: src/library/PostgresqlConnectionString/Types.hs

-- |
-- Module: PostgresqlConnectionString.Types
-- Description: Core data types for PostgreSQL connection strings
--
-- This module defines the internal representation of PostgreSQL connection strings.
-- Users typically don't need to import this module directly; use "ConnectionString" instead.
module PostgresqlConnectionString.Types where

import qualified Data.Map.Strict as Map
import Platform.Prelude
import qualified PostgresqlConnectionString.Types.Gens as Gens
import qualified Test.QuickCheck as QuickCheck

-- | A PostgreSQL connection string.
--
-- This type represents all the components of a PostgreSQL connection string as defined in:
-- <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS>
--
-- 'ConnectionString' has 'Semigroup' and 'Monoid' instances that allow combining
-- connection strings. When combining, the right-hand side takes precedence for
-- scalar values, while the list of hosts gets concatenated.
data ConnectionString
  = ConnectionString
      -- | Username for authentication.
      (Maybe Text)
      -- | Password for authentication.
      (Maybe Text)
      -- | List of host specifications (for failover/load balancing).
      [Host]
      -- | Database name to connect to.
      (Maybe Text)
      -- | Additional connection parameters.
      (Map.Map Text Text)
  deriving stock (Eq, Ord, Generic)
  deriving anyclass (Hashable)

-- | A host specification consisting of a hostname\/IP and optional port.
data Host
  = Host
      -- | Host domain name or IP address.
      Text
      -- | Port number. If 'Nothing', PostgreSQL's default port (5432) is used.
      (Maybe Word16)
  deriving stock (Show, Eq, Ord, Generic)
  deriving anyclass (Hashable)

-- | Combine two connection strings.
--
-- When combining, the following rules apply:
--
-- * For hosts: concatenated in order.
-- * For scalar values and params: right-hand side takes precedence for duplicate keys, which gives you override behaviour.
instance Semigroup ConnectionString where
  ConnectionString user1 password1 hosts1 dbname1 params1 <> ConnectionString user2 password2 hosts2 dbname2 params2 =
    ConnectionString
      (user2 <|> user1)
      (password2 <|> password1)
      (hosts1 <> hosts2)
      (dbname2 <|> dbname1)
      (Map.union params2 params1)

instance Monoid ConnectionString where
  mempty = ConnectionString Nothing Nothing [] Nothing Map.empty

instance QuickCheck.Arbitrary ConnectionString where
  arbitrary = QuickCheck.sized \size -> do
    user <- Gens.genMaybeText size
    -- Password only makes sense if there's a user
    password <- case user of
      Nothing -> pure Nothing
      Just _ -> Gens.genMaybeText size
    hosts <- QuickCheck.scale (`div` 2) (QuickCheck.listOf QuickCheck.arbitrary)
    dbname <- Gens.genMaybeText size
    params <- Gens.genParams size
    pure (ConnectionString user password hosts dbname params)

instance QuickCheck.Arbitrary Host where
  arbitrary = do
    hostname <- Gens.genHostname
    port <- QuickCheck.oneof [pure Nothing, Just <$> QuickCheck.arbitrary]
    pure (Host hostname port)