postgresql-simple-opts 0.2.0.0 → 0.2.0.1
raw patch · 3 files changed
+142/−243 lines, 3 filesdep −markdown-unlit
Dependencies removed: markdown-unlit
Files
- postgresql-simple-opts.cabal +2/−4
- src/Database/PostgreSQL/Simple/Options.hs +140/−0
- src/Database/PostgreSQL/Simple/Options.lhs +0/−239
postgresql-simple-opts.cabal view
@@ -1,5 +1,5 @@ name: postgresql-simple-opts-version: 0.2.0.0+version: 0.2.0.1 synopsis: An optparse-applicative parser for postgresql-simple's connection options description: This package exports a optparse-applicative parser and type for postgresql-simple's ConnectInfo and connection string. homepage: https://github.com/jfischoff/postgresql-simple-opts#readme@@ -22,11 +22,9 @@ , bytestring , either , optparse-generic >= 1.0.1 && <1.2- , markdown-unlit >= 0.4.0 && <0.5 , data-default default-language: Haskell2010- ghc-options: -pgmL markdown-unlit- -Wall+ ghc-options: -Wall -fno-warn-unused-do-bind test-suite postgresql-simple-opts-test
+ src/Database/PostgreSQL/Simple/Options.hs view
@@ -0,0 +1,140 @@+{-| 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++-- | An optional version of 'ConnectInfo'. This includes an instance of+-- | 'ParseRecord' which provides the optparse-applicative Parser.+data PartialConnectInfo = PartialConnectInfo+ { host :: Last String+ , port :: Last Int+ , user :: Last String+ , password :: Last String+ , database :: Last String+ } deriving (Show, Eq, Read, Ord, Generic, Typeable)++instance ParseRecord PartialConnectInfo++instance Monoid PartialConnectInfo where+ mempty = PartialConnectInfo (Last Nothing) (Last Nothing)+ (Last Nothing) (Last Nothing)+ (Last Nothing)+ mappend x y = PartialConnectInfo+ { 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+ }++newtype ConnectString = ConnectString+ { connectString :: ByteString+ } deriving ( Show, Eq, Read, Ord, Generic, Typeable, IsString )++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++instance ParseRecord ConnectString where+ parseRecord = fmap (ConnectString . BSC.pack)+ $ option ( eitherReader+ $ maybe (Left "Impossible!") Right+ . parseString+ )+ (long "connectString")++data PartialOptions+ = POConnectString ConnectString+ | POPartialConnectInfo PartialConnectInfo+ deriving (Show, Eq, Read, Generic, Typeable)++instance Monoid PartialOptions where+ mempty = POPartialConnectInfo mempty+ mappend a b = case (a, b) of+ (POConnectString x, _) -> POConnectString x+ (POPartialConnectInfo x, POPartialConnectInfo y) ->+ POPartialConnectInfo $ x <> y+ (POPartialConnectInfo _, POConnectString x) -> POConnectString x++instance ParseRecord PartialOptions where+ parseRecord+ = fmap POConnectString parseRecord+ <|> fmap POPartialConnectInfo parseRecord++-- | The main parser to reuse.+parser :: Parser PartialOptions+parser = parseRecord++data Options+ = OConnectString ByteString+ | OConnectInfo ConnectInfo+ deriving (Show, Eq, Read, Generic, Typeable)++mkLast :: a -> Last a+mkLast = Last . Just++-- | The 'PartialConnectInfo' version of 'defaultConnectInfo'+instance Default PartialConnectInfo where+ def = PartialConnectInfo+ { host = mkLast $ connectHost defaultConnectInfo+ , port = mkLast $ fromIntegral $ connectPort defaultConnectInfo+ , user = mkLast $ connectUser defaultConnectInfo+ , password = mkLast $ connectPassword defaultConnectInfo+ , database = mkLast $ connectDatabase defaultConnectInfo+ }++instance Default PartialOptions where+ def = POPartialConnectInfo def++getOption :: String -> Last a -> Validation [String] a+getOption optionName = \case+ Last (Just x) -> pure x+ Last Nothing -> Data.Either.Validation.Failure+ ["Missing " ++ optionName ++ " option"]++completeConnectInfo :: PartialConnectInfo -> Either [String] ConnectInfo+completeConnectInfo PartialConnectInfo {..} = validationToEither $ do+ connectHost <- getOption "host" host+ connectPort <- fromIntegral+ <$> getOption "port" port+ connectUser <- getOption "user" user+ connectPassword <- getOption "password" password+ connectDatabase <- getOption "database" database+ return $ ConnectInfo {..}++-- | mappend with 'defaultPartialConnectInfo' if necessary to create all+-- options+completeOptions :: PartialOptions -> Either [String] Options+completeOptions = \case+ POConnectString (ConnectString x) -> Right $ OConnectString x+ POPartialConnectInfo x -> OConnectInfo <$> completeConnectInfo x++-- | Useful for testing or if only Options are needed.+completeParser :: Parser Options+completeParser =+ fmap (either (error . unlines) id . completeOptions . mappend def) parseRecord++-- | Create a connection with an 'Option'+run :: Options -> IO Connection+run = \case+ OConnectString connString -> connectPostgreSQL connString+ OConnectInfo connInfo -> connect connInfo
− src/Database/PostgreSQL/Simple/Options.lhs
@@ -1,239 +0,0 @@-[](https://hackage.haskell.org/package/postgresql-simple-opts)-[](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 'ConnectInfo'. This includes an instance of--- | 'ParseRecord' which provides the optparse-applicative Parser.-data PartialConnectInfo = PartialConnectInfo- { 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 PartialConnectInfo-```--Now we make `PartialConnectInfo` an instance of `Monoid` so we can combine multiple options together.--```haskell-instance Monoid PartialConnectInfo where- mempty = PartialConnectInfo (Last Nothing) (Last Nothing)- (Last Nothing) (Last Nothing)- (Last Nothing)- mappend x y = PartialConnectInfo- { 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`: `ConnectInfo` and a `ByteString` connection string. We have a partial version of `ConnectInfo` 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 `PartialConnectInfo` type.--```haskell-data PartialOptions- = POConnectString ConnectString- | POPartialConnectInfo PartialConnectInfo- deriving (Show, Eq, Read, Generic, Typeable)--instance Monoid PartialOptions where- mempty = POPartialConnectInfo mempty- mappend a b = case (a, b) of- (POConnectString x, _) -> POConnectString x- (POPartialConnectInfo x, POPartialConnectInfo y) ->- POPartialConnectInfo $ x <> y- (POPartialConnectInfo _, 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 POPartialConnectInfo 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 `ConnectInfo` or a connection string--```haskell-data Options- = OConnectString ByteString- | OConnectInfo ConnectInfo- deriving (Show, Eq, Read, Generic, Typeable)-```--### <a name="completion"> Option "completion"--`postgresql-simple` provides sensible defaults for `ConnectInfo` via `defaultConnectInfo`. We use these as the defaults when parsing. We create a `PartialConnectInfo` with these defaults.--```haskell-mkLast :: a -> Last a-mkLast = Last . Just---- | The 'PartialConnectInfo' version of 'defaultConnectInfo'-instance Default PartialConnectInfo where- def = PartialConnectInfo- { host = mkLast $ connectHost defaultConnectInfo- , port = mkLast $ fromIntegral $ connectPort defaultConnectInfo- , user = mkLast $ connectUser defaultConnectInfo- , password = mkLast $ connectPassword defaultConnectInfo- , database = mkLast $ connectDatabase defaultConnectInfo- }--instance Default PartialOptions where- def = POPartialConnectInfo def-```--We can now complete the `PartialConnectInfo` to get a `ConnectInfo`.--```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"]--completeConnectInfo :: PartialConnectInfo -> Either [String] ConnectInfo-completeConnectInfo PartialConnectInfo {..} = validationToEither $ do- connectHost <- getOption "host" host- connectPort <- fromIntegral- <$> getOption "port" port- connectUser <- getOption "user" user- connectPassword <- getOption "password" password- connectDatabase <- getOption "database" database- return $ ConnectInfo {..}-```--Completing a `PartialOptions` to get an `Options` follows straightforwardly ... if you've done this a bunch I suppose.--```haskell--- | mappend with 'defaultPartialConnectInfo' if necessary to create all--- options-completeOptions :: PartialOptions -> Either [String] Options-completeOptions = \case- POConnectString (ConnectString x) -> Right $ OConnectString x- POPartialConnectInfo x -> OConnectInfo <$> completeConnectInfo 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- OConnectInfo 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.