packages feed

postgresql-simple-opts 0.5.0.1 → 0.6.0.0

raw patch · 6 files changed

+292/−40 lines, 6 filesdep ~envydep ~postgres-optionsPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: envy, postgres-options

API changes (from Hackage documentation)

Files

+ CHANGELOG.md view
@@ -0,0 +1,2 @@+- 0.6.0.0+  - #4 Update to latest version of `envy` and `postgres-options`.
+ README.md view
@@ -0,0 +1,239 @@+[![Hackage](https://img.shields.io/hackage/v/postgresql-simple-opts.svg)](https://hackage.haskell.org/package/postgresql-simple-opts)+[![Travis CI Status](https://travis-ci.org/jfischoff/postgresql-simple-opts.svg?branch=master)](http://travis-ci.org/jfischoff/postgresql-simple-opts)++### Composable Command Line Parsing with `optparse-applicative`++There are many solutions for parsing command line arguments in Haskell. Personally I like [`optparse-applicative`](https://hackage.haskell.org/package/optparse-applicative-0.12.1.0/), because, like the title suggests, you can compose parsers out of smaller pieces.++I have written command line parsers for [`postgresql-simple's`](https://hackage.haskell.org/package/postgresql-simple-0.5.2.1/) database connection info many times. Faced with the prospect of doing it again I opted to make this library, which is also a single literate Haskell file. This way I could reuse it in web servers, db migrators, db job runners ... those are all the examples I could think of ... just trust me, it's worth it.++### Outline+- [The "Partial" Option Types](#partial)+- [The Composable Parser](#parser)+- [The Complete Option](#option)+- [Option "completion"](#completion)+- [The Option Parser](#option-parser)+- [The Runner](#runner)+- [The Tests](#tests)++### Standard Intro Statements to Ignore++```haskell+{-| A resuable optparse-applicative parser for creating a postgresql-simple+   'Connection'+-}+{-# LANGUAGE RecordWildCards, LambdaCase, DeriveGeneric, DeriveDataTypeable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving, CPP, ApplicativeDo #-}+module Database.PostgreSQL.Simple.Options where+import Database.PostgreSQL.Simple+import Options.Applicative+import Text.Read+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BSC+import GHC.Generics+import Options.Generic+import Data.Typeable+import Data.String+import Data.Monoid+import Data.Either.Validation+import Data.Default+```++### <a name="partial"> The "Partial" Option Types++In general, options types are built from many optional fields. Additionally, multiple options sets can be combined (i.e. command line options, config file, environment vars, defaults, etc). The easiest way to handle this is to create a "partial" option family that can be monoidally composed and is "completed" with a default option value.++```haskell+-- | An optional version of 'Options'. This includes an instance of+-- | 'ParseRecord' which provides the optparse-applicative Parser.+data PartialOptions = PartialOptions+  { host     :: Last String+  , port     :: Last Int+  , user     :: Last String+  , password :: Last String+  , database :: Last String+  } deriving (Show, Eq, Read, Ord, Generic, Typeable)+```++We will utilize a boilerplate prevention library by Gabriel Gonzalez called [`optparse-generic`](https://hackage.haskell.org/package/optparse-generic-1.1.3) which generates a parser from the record field names.++To create the parser we have to merely declare an instance of `ParseRecord`.++```haskell+instance ParseRecord PartialOptions+```++Now we make `PartialOptions` an instance of `Monoid` so we can combine multiple options together.++```haskell+instance Monoid PartialOptions where+  mempty = PartialOptions (Last Nothing) (Last Nothing)+                              (Last Nothing) (Last Nothing)+                              (Last Nothing)+  mappend x y = PartialOptions+    { host     = host     x <> host     y+    , port     = port     x <> port     y+    , user     = user     x <> user     y+    , password = password x <> password y+    , database = database x <> database y+    }+```++As it so happens there are two ways to create a db connection with `postgresql-simple`: `Options` and a `ByteString` connection string. We have a partial version of `Options` but we need something for the connection string.++```haskell+newtype ConnectString = ConnectString+  { connectString :: ByteString+  } deriving ( Show, Eq, Read, Ord, Generic, Typeable, IsString )+```++I don't like the default option parsing for `String` in `optparse-applicative`. I want something that will escape double quotes, remove single quotes or just use the string unaltered. The function `parseString` does this.++```haskell+unSingleQuote :: String -> Maybe String+unSingleQuote (x : xs@(_ : _))+  | x == '\'' && last xs == '\'' = Just $ init xs+  | otherwise                    = Nothing+unSingleQuote _                  = Nothing++parseString :: String -> Maybe String+parseString x = readMaybe x <|> unSingleQuote x <|> Just x+```++We use `parseString` to make a custom instance of `ParseRecord`.++```haskell+instance ParseRecord ConnectString where+  parseRecord =  fmap (ConnectString . BSC.pack)+              $  option ( eitherReader+                        $ maybe (Left "Impossible!") Right+                        . parseString+                        )+                        (long "connectString")+```+Thus, my `PartialOptions` type is either the `ConnectString` or the `PartialOptions` type.++```haskell+data PartialOptions+  = POConnectString      ConnectString+  | POPartialOptions PartialOptions+  deriving (Show, Eq, Read, Generic, Typeable)++instance Monoid PartialOptions where+    mempty = POPartialOptions mempty+    mappend a b = case (a, b) of+        (POConnectString x, _) -> POConnectString x+        (POPartialOptions x, POPartialOptions y) ->+            POPartialOptions $ x <> y+        (POPartialOptions _, POConnectString x) -> POConnectString x+```++There is one wrinkle. `optparse-generic` treats sum types as "commands". This makes sense as a default, but it is not what we want. We want to choose one record or another based on the non-overlapping flags. This is easy enough to do by hand.++```haskell+instance ParseRecord PartialOptions where+  parseRecord+    =  fmap POConnectString      parseRecord+   <|> fmap POPartialOptions parseRecord+```++### <a name="parser"> The Composable Parser++We can use `PartialOptions` as the type of a field in a larger options record defined elsewhere. When defining this more complicated parser, we reuse the work we did here by calling `parseRecord`. To make it even clearer we create an alias called `parser` so clients will know what to use.++```haskell+-- | The main parser to reuse.+parser :: Parser PartialOptions+parser = parseRecord+```++### <a name="option"> The Complete Option++The connection option for `postgresql-simple` is either the record `Options` or a connection string++```haskell+data Options+  = OConnectString ByteString+  | OOptions   Options+  deriving (Show, Eq, Read, Generic, Typeable)+```++### <a name="completion"> Option "completion"++`postgresql-simple` provides sensible defaults for `Options` via `defaultOptions`. We use these as the defaults when parsing. We create a `PartialOptions` with these defaults.++```haskell+mkLast :: a -> Last a+mkLast = Last . Just++-- | The 'PartialOptions' version of 'defaultOptions'+instance Default PartialOptions where+    def = PartialOptions+      { host     = mkLast $                connectHost     defaultOptions+      , port     = mkLast $ fromIntegral $ connectPort     defaultOptions+      , user     = mkLast $                connectUser     defaultOptions+      , password = mkLast $                connectPassword defaultOptions+      , database = mkLast $                connectDatabase defaultOptions+      }++instance Default PartialOptions where+    def = POPartialOptions def+```++We can now complete the `PartialOptions` to get a `Options`.++```haskell+getOption :: String -> Last a -> Validation [String] a+getOption optionName = \case+    Last (Just x) -> pure x+    Last Nothing  -> Data.Either.Validation.Failure+        ["Missing " ++ optionName ++ " option"]++completeOptions :: PartialOptions -> Either [String] Options+completeOptions PartialOptions {..} = validationToEither $ do+  connectHost     <- getOption "host"     host+  connectPort     <- fromIntegral+                 <$> getOption "port"     port+  connectUser     <- getOption "user"     user+  connectPassword <- getOption "password" password+  connectDatabase <- getOption "database" database+  return $ Options {..}+```++Completing a `PartialOptions` to get an `Options` follows straightforwardly ... if you've done this a bunch I suppose.++```haskell+-- | mappend with 'defaultPartialOptions' if necessary to create all+--   options+completeOptions :: PartialOptions -> Either [String] Options+completeOptions = \case+  POConnectString   (ConnectString x) -> Right $ OConnectString x+  POPartialOptions x              -> OOptions <$> completeOptions x+```++### <a name="option-parser"> The Option Parser++Parse a `PartialOptions` and then complete it. This is **not** composable but is convient for testing and if you only need a `Option` type++```haskell+-- | Useful for testing or if only Options are needed.+completeParser :: Parser Options+completeParser =+    fmap (either (error . unlines) id . completeOptions . mappend def) parseRecord+```++### <a name="runner"> The Runner++As a convenience, we export the primary use of parsing connection options ... making a connection.++```haskell+-- | Create a connection with an 'Option'+run :: Options -> IO Connection+run = \case+  OConnectString connString -> connectPostgreSQL connString+  OOptions   connInfo   -> connect           connInfo+```++### <a name="tests"> The tests++Testing is pretty straightforward using `System.Environment.withArgs`. See the [Spec.hs](/test/Spec.hs) for examples of how to test the parsers.
+ development-notes.md view
@@ -0,0 +1,7 @@+# 12/31/2019++- I should get this working the latest stackage nightly and then readd it to stackage.+- The new version of `postgres-options` is really a partial options. The `PartialOptions` is unnecessary.+  I'm leaving it now but I'm probably going to remove it.+- On the otherhand I haven't revisited the decision to make the `Option` type have so+  many optional fields. It seems fine. This could just have orphans..idk...or stay the way it is.
postgresql-simple-opts.cabal view
@@ -1,17 +1,20 @@ name:                postgresql-simple-opts-version:             0.5.0.1-synopsis:            An optparse-applicative and envy parser for postgresql-simple's connection options-description:         This package exports a optparse-applicative and envy parser and type for postgresql-simple's Options and connection string.+version:             0.6.0.0+synopsis:            An optparse-applicative and envy parser for postgres options+description:         An optparse-applicative and envy parser for postgres options. See README.md homepage:            https://github.com/jfischoff/postgresql-simple-opts#readme license:             BSD3 license-file:        LICENSE author:              Jonathan Fischoff maintainer:          jonathangfischoff@gmail.com-copyright:           2016 Jonathan Fischoff+copyright:           2016-2020 Jonathan Fischoff category:            Database build-type:          Simple--- extra-source-files:+extra-source-files: CHANGELOG.md+  , README.md+  , development-notes.md cabal-version:       >=1.10+tested-with: GHC==8.8.2  library   hs-source-dirs:      src@@ -27,8 +30,8 @@                , split                , uri-bytestring                , generic-deriving-               , postgres-options >= 0.1.0.1-               , envy < 2.0.0.0+               , postgres-options >= 0.2.0.0+               , envy   default-language:    Haskell2010   ghc-options: -Wall                -fno-warn-unused-do-bind@@ -39,6 +42,7 @@   main-is:             Spec.hs   build-depends:       base                      , containers+                     , envy                      , postgresql-simple-opts                      , hspec                      , postgresql-simple
src/Database/PostgreSQL/Simple/PartialOptions/Internal.hs view
@@ -14,12 +14,13 @@ import Data.Char (isUpper, toLower) import Data.Default (Default(..)) import qualified Data.Either.Validation as DEV-import Data.Either.Validation (Validation(..), validationToEither)+import Data.Either.Validation (Validation(..)) import Data.List (intercalate) import Data.List.Split (splitOn) import Data.Typeable (Typeable) import Database.PostgreSQL.Simple (ConnectInfo(..), Connection, connectPostgreSQL, defaultConnectInfo)-import Database.PostgreSQL.Simple.Options (Options(..), toConnectionString)+import qualified Database.PostgreSQL.Simple.Options as O+import Database.PostgreSQL.Simple.Options(Options) import GHC.Generics (Generic) import Generics.Deriving.Monoid (Last(..), gmappenddefault, gmemptydefault) import Options.Applicative (Parser, (<|>), eitherReader, long, option)@@ -57,7 +58,7 @@   } deriving (Show, Eq, Read, Ord, Generic, Typeable)  instance FromEnv PartialOptions where-  fromEnv+  fromEnv _       = PartialOptions     <$> env "PGHOST"     <*> env "PGHOSTADDR"@@ -165,32 +166,34 @@ getLast' :: Applicative f => Last a -> f (Maybe a) getLast' = pure . getLast +-- This is pointless. completeOptions :: PartialOptions -> Either [String] Options-completeOptions PartialOptions {..} = validationToEither $-  Options <$> getLast' host-          <*> getLast' hostaddr-          <*> (fmap fromIntegral <$> getLast' port)-          <*> getLast' user-          <*> getLast' password-          <*> getOption "dbname" dbname-          <*> getLast' connectTimeout-          <*> getLast' clientEncoding-          <*> getLast' options-          <*> getLast' fallbackApplicationName-          <*> getLast' keepalives-          <*> getLast' keepalivesIdle-          <*> getLast' keepalivesCount-          <*> getLast' sslmode-          <*> getLast' requiressl-          <*> getLast' sslcompression-          <*> getLast' sslcert-          <*> getLast' sslkey-          <*> getLast' sslrootcert-          <*> getLast' requirepeer-          <*> getLast' krbsrvname-          <*> getLast' gsslib-          <*> getLast' service+completeOptions PartialOptions {..} = pure $+  O.Options host+            hostaddr+            (fromIntegral <$> port)+            user+            password+            dbname+            connectTimeout+            clientEncoding+            options+            fallbackApplicationName+            keepalives+            keepalivesIdle+            keepalivesCount+            sslmode+            requiressl+            sslcompression+            sslcert+            sslkey+            sslrootcert+            requirepeer+            krbsrvname+            gsslib+            service + -- | Useful for testing or if only Options are needed. completeParser :: Parser Options completeParser =@@ -198,7 +201,7 @@  -- | Create a connection with an 'Option' run :: Options -> IO Connection-run = connectPostgreSQL . toConnectionString+run = connectPostgreSQL . O.toConnectionString  userInfoToPartialOptions :: UserInfo -> PartialOptions userInfoToPartialOptions UserInfo {..} = mempty { user = return $ BSC.unpack uiUsername } <> if BS.null uiPassword
test/Spec.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings, CPP #-} import Test.Hspec import Database.PostgreSQL.Simple.PartialOptions-import Database.PostgreSQL.Simple.Options+import qualified Database.PostgreSQL.Simple.Options as O import System.Environment import Options.Applicative import System.Exit@@ -9,7 +9,7 @@ import qualified System.Envy as E import qualified Data.Map as Map -testParser :: IO Options+testParser :: IO O.Options testParser = execParser $ info completeParser mempty  main :: IO ()@@ -244,6 +244,3 @@           { host = return "/var/lib/postgresql"           , dbname = return "dbname"           })---