module Database.PostgreSQL.Simple.Options
( Options(..)
, defaultOptions
, toConnectionString
, parseConnectionString
) where
import Data.Maybe (Maybe, maybeToList)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Data.Monoid.Generic
import Data.List (intercalate)
import Data.List.Split (splitOn)
import Text.Read (readMaybe)
import URI.ByteString as URI
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.Monoid
import Control.Monad ((<=<), foldM)
import Control.Applicative
data Options = Options
{ host :: Last String
, hostaddr :: Last String
, port :: Last Int
, user :: Last String
, password :: Last String
, dbname :: Last String
, connectTimeout :: Last Int
, clientEncoding :: Last String
, options :: Last String
, fallbackApplicationName :: Last String
, keepalives :: Last Int
, keepalivesIdle :: Last Int
, keepalivesCount :: Last Int
, sslmode :: Last String
, requiressl :: Last Int
, sslcompression :: Last Int
, sslcert :: Last String
, sslkey :: Last String
, sslrootcert :: Last String
, requirepeer :: Last String
, krbsrvname :: Last String
, gsslib :: Last String
, service :: Last String
} deriving stock (Show, Eq, Read, Ord, Generic, Typeable)
deriving Semigroup via GenericSemigroup Options
deriving Monoid via GenericMonoid Options
toConnectionString :: Options -> ByteString
toConnectionString Options {..} = BSC.pack $ unwords $ map (\(k, v) -> k <> "=" <> v)
$ maybeToPairStr "host" host
<> maybeToPairStr "hostaddr" hostaddr
<> maybeToPairStr "dbname" dbname
<> maybeToPair "port" port
<> maybeToPairStr "password" password
<> maybeToPairStr "user" user
<> maybeToPair "connect_timeout" connectTimeout
<> maybeToPairStr "client_encoding" clientEncoding
<> maybeToPairStr "options" options
<> maybeToPairStr "fallback_applicationName" fallbackApplicationName
<> maybeToPair "keepalives" keepalives
<> maybeToPair "keepalives_idle" keepalivesIdle
<> maybeToPair "keepalives_count" keepalivesCount
<> maybeToPairStr "sslmode" sslmode
<> maybeToPair "requiressl" requiressl
<> maybeToPair "sslcompression" sslcompression
<> maybeToPairStr "sslcert" sslcert
<> maybeToPairStr "sslkey" sslkey
<> maybeToPairStr "sslrootcert" sslrootcert
<> maybeToPairStr "requirepeer" requirepeer
<> maybeToPairStr "krbsrvname" krbsrvname
<> maybeToPairStr "gsslib" gsslib
<> maybeToPairStr "service" service
where
maybeToPairStr :: String -> Last String -> [(String, String)]
maybeToPairStr k mv = (k,) <$> maybeToList (getLast mv)
maybeToPair :: Show a => String -> Last a -> [(String, String)]
maybeToPair k mv = (\v -> (k, show v)) <$> maybeToList (getLast mv)
defaultOptions :: Options
defaultOptions = mempty
{ host = pure "localhost"
, port = pure 5432
, user = pure "postgres"
, dbname = pure "postgres"
}
userInfoToptions :: UserInfo -> Options
userInfoToptions UserInfo {..} = mempty { user = return $ BSC.unpack uiUsername } <> if BS.null uiPassword
then mempty
else mempty { password = return $ BSC.unpack uiPassword }
authorityToOptions :: Authority -> Options
authorityToOptions Authority {..} = maybe mempty userInfoToptions authorityUserInfo <>
mempty { host = return $ BSC.unpack $ hostBS authorityHost } <>
maybe mempty (\p -> mempty { port = return $ portNumber p }) authorityPort
pathToptions :: ByteString -> Options
pathToptions path = case drop 1 $ BSC.unpack path of
"" -> mempty
x -> mempty {dbname = return x }
parseInt :: String -> String -> Either String Int
parseInt msg v = maybe (Left (msg <> " value of: " <> v <> " is not a number")) Right $
readMaybe v
parseString :: String -> Maybe String
parseString x = readMaybe x <|> unSingleQuote x <|> Just x
unSingleQuote :: String -> Maybe String
unSingleQuote (x : xs@(_ : _))
| x == '\'' && last xs == '\'' = Just $ init xs
| otherwise = Nothing
unSingleQuote _ = Nothing
keywordToptions :: String -> String -> Either String Options
keywordToptions k v = case k of
"host" -> return $ mempty { host = return v }
"hostaddress" -> return $ mempty { hostaddr = return v }
"port" -> do
portValue <- parseInt "port" v
return $ mempty { port = return portValue }
"user" -> return $ mempty { user = return v }
"password" -> return $ mempty { password = return v }
"dbname" -> return $ mempty { dbname = return v}
"connect_timeout" -> do
x <- parseInt "connect_timeout" v
return $ mempty { connectTimeout = return x }
"client_encoding" -> return $ mempty { clientEncoding = return v }
"options" -> return $ mempty { options = return v }
"fallback_applicationName" -> return $ mempty { fallbackApplicationName = return v }
"keepalives" -> do
x <- parseInt "keepalives" v
return $ mempty { keepalives = return x }
"keepalives_idle" -> do
x <- parseInt "keepalives_idle" v
return $ mempty { keepalivesIdle = return x }
"keepalives_count" -> do
x <- parseInt "keepalives_count" v
return $ mempty { keepalivesCount = return x }
"sslmode" -> return $ mempty { sslmode = return v }
"requiressl" -> do
x <- parseInt "requiressl" v
return $ mempty { requiressl = return x }
"sslcompression" -> do
x <- parseInt "sslcompression" v
return $ mempty { sslcompression = return x }
"sslcert" -> return $ mempty { sslcert = return v }
"sslkey" -> return $ mempty { sslkey = return v }
"sslrootcert" -> return $ mempty { sslrootcert = return v }
"requirepeer" -> return $ mempty { requirepeer = return v }
"krbsrvname" -> return $ mempty { krbsrvname = return v }
"gsslib" -> return $ mempty { gsslib = return v }
"service" -> return $ mempty { service = return v }
x -> Left $ "Unrecongnized option: " ++ show x
queryToptions :: URI.Query -> Either String Options
queryToptions Query {..} = foldM (\acc (k, v) -> fmap (mappend acc) $ keywordToptions (BSC.unpack k) $ BSC.unpack v) mempty queryPairs
uriToptions :: URIRef Absolute -> Either String Options
uriToptions URI {..} = case schemeBS uriScheme of
"postgresql" -> do
queryParts <- queryToptions uriQuery
return $ maybe mempty authorityToOptions uriAuthority <>
pathToptions uriPath <> queryParts
x -> Left $ "Wrong protocol. Expected \"postgresql\" but got: " ++ show x
parseURIStr :: String -> Either String (URIRef Absolute)
parseURIStr = left show . parseURI strictURIParserOptions . BSC.pack where
left f = \case
Left x -> Left $ f x
Right x -> Right x
parseKeywords :: String -> Either String Options
parseKeywords [] = Left "Failed to parse keywords"
parseKeywords x = fmap mconcat . mapM (uncurry keywordToptions <=< toTuple . splitOn "=") $ words x where
toTuple [k, v] = return (k, v)
toTuple xs = Left $ "invalid opts:" ++ show (intercalate "=" xs)
parseConnectionString :: String -> Either String Options
parseConnectionString url = do
url' <- maybe (Left "failed to parse as string") Right $ parseString url
parseKeywords url' <|> (uriToptions =<< parseURIStr url')