diff --git a/README.md b/README.md index 880782a..dcbe55d 100644 --- a/README.md +++ b/README.md @@ -83,48 +83,6 @@ toDbname :: ConnectionString -> Maybe Text toParams :: ConnectionString -> Map Text Text ``` -### Transforming Connection Strings - -```haskell --- Intercept and remove a parameter -case interceptParam "application_name" connStr of - Just (value, updatedConnStr) -> - -- value is the parameter value, updatedConnStr has it removed - processAppName value - Nothing -> - -- Parameter not found - useDefault -``` - -## Installation - -Add to your `package.yaml` or `.cabal` file: - -```yaml -dependencies: - - postgresql-connection-string -``` - -Or with cabal: - -```cabal -build-depends: - postgresql-connection-string -``` - -## Requirements - -- GHC 8.10 or later -- Standard Haskell dependencies (see cabal file) - ## Related Projects This library was extracted from the [hasql](https://github.com/nikita-volkov/hasql) project to provide a standalone connection string parser and builder that can be used independently of the full hasql ecosystem. - -## License - -MIT License - see LICENSE file for details. - -## Contributing - -Contributions are welcome! Please feel free to submit pull requests or open issues on GitHub. diff --git a/postgresql-connection-string.cabal b/postgresql-connection-string.cabal index cba04e8..7120169 100644 --- a/postgresql-connection-string.cabal +++ b/postgresql-connection-string.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: postgresql-connection-string -version: 0.0.0.0 +version: 0.1.0.3 category: Database, PostgreSQL synopsis: PostgreSQL connection string type, parser and builder description: @@ -99,54 +99,26 @@ library PostgresqlConnectionString other-modules: - PostgresqlConnectionString.Parsers - PostgresqlConnectionString.Types - PostgresqlConnectionString.Types.Gens - - build-depends: - QuickCheck >=2.14 && <2.16, - charset ^>=0.3.12, - containers >=0.6 && <0.9, - megaparsec >=9.2.1 && <10.0, - postgresql-connection-string:percent-encoding, - postgresql-connection-string:platform, - text >=1.2 && <3, - text-builder >=1 && <1.1, - --- Replacement of the "base" library for all the sublibs here. --- Covers such things as Prelude and shared utils. -library platform - import: base - hs-source-dirs: src/platform - exposed-modules: - Platform.Prelude - - build-depends: - base >=4.13 && <5, - bytestring >=0.10 && <0.13, - hashable >=1.2 && <2, - text >=1.2 && <3, - text-builder >=1 && <1.1, - -library percent-encoding - import: base - hs-source-dirs: src/percent-encoding - exposed-modules: PercentEncoding - - other-modules: PercentEncoding.Charsets PercentEncoding.MonadPlus PercentEncoding.Parsers PercentEncoding.TextBuilders PercentEncoding.Utf8CharView + Platform.Prelude + PostgresqlConnectionString.Charsets + PostgresqlConnectionString.Parsers + PostgresqlConnectionString.Types + PostgresqlConnectionString.Types.Gens build-depends: + QuickCheck >=2.14 && <2.16, base >=4.13 && <5, bytestring >=0.10 && <0.13, charset ^>=0.3.12, + containers >=0.6 && <0.9, + hashable >=1.2 && <2, megaparsec >=9.2.1 && <10.0, - postgresql-connection-string:platform, text >=1.2 && <3, text-builder >=1 && <1.1, @@ -157,9 +129,9 @@ test-suite library-tests main-is: Main.hs build-depends: QuickCheck >=2.14 && <2.16, + base >=4.13 && <5, containers >=0.6 && <0.9, hspec ^>=2.11.12, postgresql-connection-string, - postgresql-connection-string:platform, quickcheck-classes >=0.6.5 && <0.7, text >=1.2 && <3, diff --git a/src/library-tests/Main.hs b/src/library-tests/Main.hs index 6037fdb..f69fa85 100644 --- a/src/library-tests/Main.hs +++ b/src/library-tests/Main.hs @@ -1,12 +1,16 @@ module Main where +import Control.Monad +import Data.Either +import Data.Function import qualified Data.Map.Strict as Map +import Data.Proxy import qualified Data.Text as Text -import Platform.Prelude import qualified PostgresqlConnectionString as ConnectionString import Test.Hspec import Test.QuickCheck import qualified Test.QuickCheck.Classes as Laws +import Prelude main :: IO () main = hspec do @@ -23,8 +27,7 @@ main = hspec do it "generates valid postgresql:// URLs" do property \connStr -> let url = ConnectionString.toUrl connStr - urlStr = Text.unpack url - in (url `elem` ["postgresql://", "postgres://"] || "postgresql://" `isPrefixOf` urlStr) + in (url `elem` ["postgresql://", "postgres://"] || "postgresql://" `Text.isPrefixOf` url) & counterexample ("Generated URL: " <> Text.unpack url) it "encodes user correctly" do diff --git a/src/percent-encoding/PercentEncoding.hs b/src/library/PercentEncoding.hs similarity index 100% rename from src/percent-encoding/PercentEncoding.hs rename to src/library/PercentEncoding.hs diff --git a/src/percent-encoding/PercentEncoding/Charsets.hs b/src/library/PercentEncoding/Charsets.hs similarity index 100% rename from src/percent-encoding/PercentEncoding/Charsets.hs rename to src/library/PercentEncoding/Charsets.hs diff --git a/src/percent-encoding/PercentEncoding/MonadPlus.hs b/src/library/PercentEncoding/MonadPlus.hs similarity index 100% rename from src/percent-encoding/PercentEncoding/MonadPlus.hs rename to src/library/PercentEncoding/MonadPlus.hs diff --git a/src/percent-encoding/PercentEncoding/Parsers.hs b/src/library/PercentEncoding/Parsers.hs similarity index 100% rename from src/percent-encoding/PercentEncoding/Parsers.hs rename to src/library/PercentEncoding/Parsers.hs diff --git a/src/percent-encoding/PercentEncoding/TextBuilders.hs b/src/library/PercentEncoding/TextBuilders.hs similarity index 100% rename from src/percent-encoding/PercentEncoding/TextBuilders.hs rename to src/library/PercentEncoding/TextBuilders.hs diff --git a/src/percent-encoding/PercentEncoding/Utf8CharView.hs b/src/library/PercentEncoding/Utf8CharView.hs similarity index 100% rename from src/percent-encoding/PercentEncoding/Utf8CharView.hs rename to src/library/PercentEncoding/Utf8CharView.hs diff --git a/src/platform/Platform/Prelude.hs b/src/library/Platform/Prelude.hs similarity index 100% rename from src/platform/Platform/Prelude.hs rename to src/library/Platform/Prelude.hs diff --git a/src/library/PostgresqlConnectionString.hs b/src/library/PostgresqlConnectionString.hs index c8b33d1..9b12046 100644 --- a/src/library/PostgresqlConnectionString.hs +++ b/src/library/PostgresqlConnectionString.hs @@ -368,7 +368,17 @@ interceptParam key (ConnectionString user password hostspec dbname paramspec) = -- Returns 'Left' with an error message if parsing fails: -- -- >>> parse "invalid://connection" --- Left "parse error message" +-- Left ... +-- +-- The error message is quite detailed (it is produced by Megaparsec): +-- +-- >>> parse "invalid://connection=" & either id (const "") & Data.Text.IO.putStrLn +-- 1:8: +-- | +-- 1 | invalid://connection= +-- | ^ +-- unexpected ':' +-- expecting '=' or Key parse :: Text -> Either Text ConnectionString parse input = Megaparsec.parse megaparsecOf "" input @@ -392,6 +402,11 @@ megaparsecOf = Parsers.getConnectionString -- using the 'Semigroup' instance. -- -- When you need to specify a port, use 'hostAndPort' instead. +-- +-- Examples: +-- +-- >>> host "localhost" +-- "postgresql://localhost" host :: Text -> ConnectionString host hostname = ConnectionString @@ -408,13 +423,10 @@ host hostname = -- -- Examples: -- --- >>> toUrl (host "localhost") --- "postgresql://localhost" --- --- >>> toUrl (hostAndPort "localhost" 5432) +-- >>> hostAndPort "localhost" 5432 -- "postgresql://localhost:5432" -- --- >>> toUrl (mconcat [hostAndPort "host1" 5432, hostAndPort "host2" 5433]) +-- >>> mconcat [hostAndPort "host1" 5432, hostAndPort "host2" 5433] -- "postgresql://host1:5432,host2:5433" hostAndPort :: Text -> Word16 -> ConnectionString hostAndPort host port = @@ -429,10 +441,10 @@ hostAndPort host port = -- -- Examples: -- --- >>> toUrl (user "myuser") +-- >>> user "myuser" -- "postgresql://myuser@" -- --- >>> toUrl (mconcat [user "myuser", host "localhost"]) +-- >>> mconcat [user "myuser", host "localhost"] -- "postgresql://myuser@localhost" user :: Text -> ConnectionString user username = @@ -449,10 +461,10 @@ user username = -- -- Examples: -- --- >>> toUrl (mconcat [user "myuser", password "secret"]) +-- >>> mconcat [user "myuser", password "secret"] -- "postgresql://myuser:secret@" -- --- >>> toUrl (mconcat [user "myuser", password "secret", host "localhost"]) +-- >>> mconcat [user "myuser", password "secret", host "localhost"] -- "postgresql://myuser:secret@localhost" password :: Text -> ConnectionString password pwd = @@ -467,10 +479,10 @@ password pwd = -- -- Examples: -- --- >>> toUrl (dbname "mydb") +-- >>> dbname "mydb" -- "postgresql:///mydb" -- --- >>> toUrl (mconcat [host "localhost", dbname "mydb"]) +-- >>> mconcat [host "localhost", dbname "mydb"] -- "postgresql://localhost/mydb" dbname :: Text -> ConnectionString dbname db = @@ -496,13 +508,13 @@ dbname db = -- -- Examples: -- --- >>> toUrl (param "application_name" "myapp") +-- >>> param "application_name" "myapp" -- "postgresql://?application_name=myapp" -- --- >>> toUrl (mconcat [host "localhost", param "connect_timeout" "10"]) +-- >>> mconcat [host "localhost", param "connect_timeout" "10"] -- "postgresql://localhost?connect_timeout=10" -- --- >>> toUrl (mconcat [param "application_name" "myapp", param "connect_timeout" "10"]) +-- >>> mconcat [param "application_name" "myapp", param "connect_timeout" "10"] -- "postgresql://?application_name=myapp&connect_timeout=10" param :: Text -> Text -> ConnectionString param key value = diff --git a/src/library/PostgresqlConnectionString/Charsets.hs b/src/library/PostgresqlConnectionString/Charsets.hs new file mode 100644 index 0000000..a01b338 --- /dev/null +++ b/src/library/PostgresqlConnectionString/Charsets.hs @@ -0,0 +1,21 @@ +module PostgresqlConnectionString.Charsets where + +import Data.CharSet +import Platform.Prelude hiding (fromList) + +control :: CharSet +control = fromList ":@?/=&," + +paramControl :: CharSet +paramControl = fromList "&" + +keyName :: CharSet +keyName = + fromList + ( mconcat + [ ['a' .. 'z'], + ['A' .. 'Z'], + ['0' .. '9'], + "_" + ] + ) diff --git a/src/library/PostgresqlConnectionString/Parsers.hs b/src/library/PostgresqlConnectionString/Parsers.hs index fcb7448..9f17a70 100644 --- a/src/library/PostgresqlConnectionString/Parsers.hs +++ b/src/library/PostgresqlConnectionString/Parsers.hs @@ -7,6 +7,7 @@ import qualified Data.Map.Strict as Map import qualified Data.Text as Text import qualified PercentEncoding import Platform.Prelude hiding (many, some, try) +import qualified PostgresqlConnectionString.Charsets as Charsets import PostgresqlConnectionString.Types import Text.Megaparsec import Text.Megaparsec.Char @@ -115,17 +116,23 @@ getKeyValueParams = do getKeyValueParam :: P (Text, Text) getKeyValueParam = do - key <- some (satisfy (\c -> c /= '=' && c /= ' ')) + key <- try do + getKeyValueKey char '=' value <- getKeyValueParamValue - pure (fromString key, fromString value) + pure (key, value) + +getKeyValueKey :: P Text +getKeyValueKey = + takeWhile1P (Just "Key") \c -> CharSet.member c Charsets.keyName -getKeyValueParamValue :: P String +getKeyValueParamValue :: P Text getKeyValueParamValue = + -- TODO: Optimize to avoid intermediate String allocation asum [ do -- Quoted value - char '\'' + try (char '\'') chars <- many do asum [ do @@ -139,20 +146,16 @@ getKeyValueParamValue = satisfy (/= '\'') ] char '\'' - pure chars, + pure (fromString chars), -- Unquoted value - some (satisfy (\c -> c /= ' ' && c /= '\n')) + fromString <$> some (satisfy (\c -> c /= ' ' && c /= '\n')) ] getWord :: P Text -getWord = PercentEncoding.parser (flip CharSet.member controlCharset) - where - controlCharset = CharSet.fromList ":@?/=&," +getWord = PercentEncoding.parser (flip CharSet.member Charsets.control) getParamValue :: P Text -getParamValue = PercentEncoding.parser (flip CharSet.member paramControlCharset) - where - paramControlCharset = CharSet.fromList "&" +getParamValue = PercentEncoding.parser (flip CharSet.member Charsets.paramControl) continueAfterHostspec :: Maybe Text -> Maybe Text -> [Host] -> P ConnectionString continueAfterHostspec user password hosts = do diff --git a/src/library/PostgresqlConnectionString/Types/Gens.hs b/src/library/PostgresqlConnectionString/Types/Gens.hs index 8f12752..61dd941 100644 --- a/src/library/PostgresqlConnectionString/Types/Gens.hs +++ b/src/library/PostgresqlConnectionString/Types/Gens.hs @@ -54,6 +54,23 @@ genParams size = do pure (Map.fromList pairs) where genParamPair = do - key <- genSafeText (size `div` 2) + key <- genKey (size `div` 2) value <- genSafeText (size `div` 2) pure (key, value) + +genKey :: Int -> Gen Text +genKey size = do + len <- chooseInt (0, max 1 (size `div` 2)) + head <- elements (['a' .. 'z'] <> ['A' .. 'Z'] <> ['_']) + tail <- vectorOf len do + elements (['a' .. 'z'] <> ['A' .. 'Z'] <> ['0' .. '9'] <> ['_']) + pure (fromString (head : tail)) + +-- | Generate text with safe characters (letters, numbers, basic punctuation) +genValue :: Int -> Gen Text +genValue size = do + len <- chooseInt (0, max 1 (size `div` 2)) + chars <- vectorOf len genSafeChar + pure (fromString chars) + where + genSafeChar = elements (['a' .. 'z'] <> ['A' .. 'Z'] <> ['0' .. '9'] <> ['_', '-', '.', ' '])