postgresql-simple-opts (empty) → 0.1.0.0
raw patch · 5 files changed
+373/−0 lines, 5 filesdep +basedep +bytestringdep +hspecsetup-changed
Dependencies added: base, bytestring, hspec, markdown-unlit, optparse-applicative, optparse-generic, postgresql-simple, postgresql-simple-opts
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- postgresql-simple-opts.cabal +49/−0
- src/Database/PostgreSQL/Simple/Options.lhs +207/−0
- test/Spec.hs +85/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Author name here (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Author name here nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ postgresql-simple-opts.cabal view
@@ -0,0 +1,49 @@+name: postgresql-simple-opts+version: 0.1.0.0+synopsis: An optparse-applicative parser for postgresql-simple's connection options+description: Please 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+category: Database+build-type: Simple+-- extra-source-files:+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Database.PostgreSQL.Simple.Options+ build-depends: base >= 4.6 && < 5+ , postgresql-simple+ , optparse-applicative >=0.11.0 && <0.13+ , bytestring+ , optparse-generic >= 1.0.1 && <1.2+ , markdown-unlit >= 0.4.0 && <0.5+ default-language: Haskell2010+ ghc-options: -pgmL markdown-unlit+ -Wall+ -fno-warn-unused-do-bind++test-suite postgresql-simple-opts-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base+ , postgresql-simple-opts+ , hspec+ , postgresql-simple+ , optparse-applicative+ , bytestring+ ghc-options: -Wall+ -fno-warn-unused-do-bind+ -threaded+ -rtsopts+ -with-rtsopts=-N+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/jfischoff/postgresql-simple-opts
+ src/Database/PostgreSQL/Simple/Options.lhs view
@@ -0,0 +1,207 @@+### 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 the database connection info for [`postgresql-simple`](https://hackage.haskell.org/package/postgresql-simple-0.5.2.1/) many times and faced with the prospect of doing it again I opted to make a library. 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+{-# LANGUAGE RecordWildCards, LambdaCase, DeriveGeneric, DeriveDataTypeable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving, CPP #-}+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+#if !MIN_VERSION_base(4,8,0)+import Control.Applicative+import Data.Monoid+#endif+```++### <a name="partial"> The "Partial" Option Types++In general, options types are built from many optional fields. Additionally, multiple options sets can be combined (e.g. 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+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 Gaberiel Gonzales called [`optparse-generic`](https://hackage.haskell.org/package/optparse-generic-1.1.3) to generate the parser for use from the records 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)+```++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+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+defaultPartialConnectInfo :: PartialConnectInfo+defaultPartialConnectInfo = PartialConnectInfo+ { host = return $ connectHost defaultConnectInfo+ , port = return $ fromIntegral $ connectPort defaultConnectInfo+ , user = return $ connectUser defaultConnectInfo+ , password = return $ connectPassword defaultConnectInfo+ , database = return $ connectDatabase defaultConnectInfo+ }+```++We can now complete the `PartialConnectInfo` to get a `ConnectInfo`.++```haskell+completeConnectInfo :: PartialConnectInfo -> ConnectInfo+completeConnectInfo x = case defaultPartialConnectInfo <> x of+ PartialConnectInfo+ { host = Last (Just connectHost )+ , port = Last (Just connectPortInt )+ , user = Last (Just connectUser )+ , password = Last (Just connectPassword)+ , database = Last (Just connectDatabase)+ } -> let connectPort = fromIntegral connectPortInt in ConnectInfo {..}+ _ -> error "Impossible! No options should be required!"+```++Completing a `PartialOptions` to get an `Options` follows straightforwardly ... if you've done this a bunch I suppose.++```haskell+completeOptions :: PartialOptions -> Options+completeOptions = \case+ POConnectString (ConnectString x) -> 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+completeParser :: Parser Options+completeParser = fmap completeOptions parseRecord+```++### <a name="runner"> The Runner++As a convenience, we export the primary use of parsing connection options ... making a connection.++```haskell+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.
+ test/Spec.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE OverloadedStrings #-}+import Test.Hspec+import Database.PostgreSQL.Simple.Options+import Database.PostgreSQL.Simple+import System.Environment+import Options.Applicative+import System.Exit++testParser :: IO Options+testParser = execParser $ info completeParser mempty++main :: IO ()+main = hspec $ describe "Options Parser" $ do+ it "parses all options" $ do+ let testArgs = [ "--host=example.com"+ , "--port=1234"+ , "--user=nobody"+ , "--password=everytimeiclosemyeyes"+ , "--database=future"+ ]++ expected = OConnectInfo $ ConnectInfo+ { connectHost = "example.com"+ , connectPort = 1234+ , connectUser = "nobody"+ , connectPassword = "everytimeiclosemyeyes"+ , connectDatabase = "future"+ }++ actual <- withArgs testArgs testParser+ actual `shouldBe` expected++ it "parses some and uses defaults for others" $ do+ let testArgs = [ "--user=nobody"+ , "--password=everytimeiclosemyeyes"+ , "--database=future"+ ]++ expected = OConnectInfo $ ConnectInfo+ { connectHost = "127.0.0.1"+ , connectPort = 5432+ , connectUser = "nobody"+ , connectPassword = "everytimeiclosemyeyes"+ , connectDatabase = "future"+ }++ actual <- withArgs testArgs testParser+ actual `shouldBe` expected++ it "parses no options and gives defaults" $ do+ let expected = OConnectInfo $ ConnectInfo+ { connectHost = "127.0.0.1"+ , connectPort = 5432+ , connectUser = "postgres"+ , connectPassword = ""+ , connectDatabase = ""+ }++ actual <- withArgs [] testParser+ actual `shouldBe` expected+ it "parses the connection string double quoted" $ do+ let testArgs = ["--connectString=\"a b\""]+ expected = OConnectString "a b"++ actual <- withArgs testArgs testParser+ actual `shouldBe` expected+ it "parses the connection string single quoted" $ do+ let testArgs = ["--connectString='a b'"]+ expected = OConnectString "a b"++ actual <- withArgs testArgs testParser+ actual `shouldBe` expected+ it "parses the connection string no quotes" $ do+ let testArgs = ["--connectString=a_b"]+ expected = OConnectString "a_b"++ actual <- withArgs testArgs testParser+ actual `shouldBe` expected+ it "fails if connectString and other args are passed" $ do+ let testArgs = ["--connectString=a_b", "--port=1234"]+ handler :: ExitCode -> Bool+ handler (ExitFailure 1) = True+ handler _ = False++ shouldThrow (withArgs testArgs testParser) handler