connection-string 0.1.0.0 → 0.2.0.0
raw patch · 2 files changed
+178/−76 lines, 2 filesdep +case-insensitivePVP ok
version bump matches the API change (PVP)
Dependencies added: case-insensitive
API changes (from Hackage documentation)
- Data.ConnectionString: type ConnectionString s = Map s s
+ Data.ConnectionString: (!) :: (FoldCase s, Ord s, IsList s, Item s ~ Char) => ConnectionString s -> s -> Maybe s
+ Data.ConnectionString: data ConnectionString s
+ Data.ConnectionString: instance (GHC.Show.Show s, Data.String.IsString s, GHC.Exts.IsList s, GHC.Exts.Item s ~ GHC.Types.Char) => GHC.Show.Show (Data.ConnectionString.ConnectionString s)
+ Data.ConnectionString: instance GHC.Classes.Eq s => GHC.Classes.Eq (Data.ConnectionString.ConnectionString s)
+ Data.ConnectionString: instance GHC.Classes.Eq s => GHC.Classes.Eq (Data.ConnectionString.Key s)
+ Data.ConnectionString: instance GHC.Classes.Ord s => GHC.Classes.Ord (Data.ConnectionString.Key s)
+ Data.ConnectionString: keys :: ConnectionString s -> [s]
+ Data.ConnectionString: toList :: ConnectionString s -> [(s, s)]
+ Data.ConnectionString: toString :: forall s. (IsString s, IsList s, Item s ~ Char) => ConnectionString s -> s
+ Data.ConnectionString: values :: ConnectionString s -> [s]
- Data.ConnectionString: parser :: Parseable s => Parsec Void s [(s, Maybe s)]
+ Data.ConnectionString: parser :: Parseable s => Parsec Void s [(Key s, Maybe s)]
- Data.ConnectionString: type Parseable s = (Stream s, Token s ~ Char, IsString s, Ord s)
+ Data.ConnectionString: type Parseable s = (Stream s, Token s ~ Char, IsString s, Ord s, FoldCase s)
Files
- connection-string.cabal +48/−47
- src/Data/ConnectionString.hs +130/−29
connection-string.cabal view
@@ -1,47 +1,48 @@-name: connection-string-version: 0.1.0.0-synopsis: A library for parsing connection strings.-description:- Parses connection strings in a manner that agrees with- the format that .NET accepts (e.g. ADO.NET).-homepage: https://github.com/Porges/connection-string-license: BSD3-license-file: LICENSE-author: George Pollard <porges@porg.es>-maintainer: George Pollard <porges@porg.es>-copyright: 2018 George Pollard-category: Text-build-type: Simple-cabal-version: >=1.10-extra-source-files: README.md--source-repository head- type: git- location: git://github.com/Porges/connection-string-hs.git--source-repository this- type: git- location: git://github.com/Porges/connection-string-hs.git- tag: v0.1.0.0--library- exposed-modules: Data.ConnectionString- hs-source-dirs: src- ghc-options: -Wall- default-language: Haskell2010- build-depends: base >= 4.7 && < 5- , containers- , megaparsec- , parser-combinators--test-suite doctests- type: exitcode-stdio-1.0- ghc-options: -threaded- default-language: Haskell2010- hs-source-dirs: tests- main-is: doctests.hs- build-depends:- base >= 4 && < 5,- doctest >= 0.8 && < 0.15,- text-+name: connection-string +version: 0.2.0.0 +synopsis: A library for parsing connection strings. +description: + Parses connection strings in a manner that agrees with + the format that .NET accepts (e.g. ADO.NET). +homepage: https://github.com/Porges/connection-string-hs +license: BSD3 +license-file: LICENSE +author: George Pollard <porges@porg.es> +maintainer: George Pollard <porges@porg.es> +copyright: 2018 George Pollard +category: Text +build-type: Simple +cabal-version: >=1.10 +extra-source-files: README.md + +source-repository head + type: git + location: git://github.com/Porges/connection-string-hs.git + +source-repository this + type: git + location: git://github.com/Porges/connection-string-hs.git + tag: v0.2.0.0 + +library + exposed-modules: Data.ConnectionString + hs-source-dirs: src + ghc-options: -Wall + default-language: Haskell2010 + build-depends: base >= 4.7 && < 5 + , case-insensitive + , containers + , megaparsec + , parser-combinators + +test-suite doctests + type: exitcode-stdio-1.0 + ghc-options: -threaded + default-language: Haskell2010 + hs-source-dirs: tests + main-is: doctests.hs + build-depends: + base >= 4 && < 5, + doctest >= 0.8 && < 0.15, + text +
src/Data/ConnectionString.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns #-} {-| Module: Data.ConnectionString@@ -13,8 +16,8 @@ The syntax of a connection string appears quite simple at first glance, and consists of a list of key-value pairs separated by semicolons: ->>> parse "key=value; key2 = value2"-Right (fromList [("key","value"),("key2","value2")])+>>> toList <$> parse "key=value; key2 = value2"+Right [("key","value"),("key2","value2")] However, the format can be more complicated than expected. @@ -23,45 +26,57 @@ A value may be single-quoted (single quotes can be escaped inside a single-quoted string by doubling them): ->>> parse "squote='value with '' quotes'"-Right (fromList [("squote","value with ' quotes")])+>>> toList <$> parse "squote='value with '' quotes'"+Right [("squote","value with ' quotes")] Or double-quoted (double quotes can also be escaped inside a double-quoted string by doubling them): ->>> parse "dquote=\"value with \"\" quotes\""-Right (fromList [("dquote","value with \" quotes")])+>>> let (Right quoted) = parse "dquote=\"value with \"\" quotes\""+>>> toList quoted+[("dquote","value with \" quotes")]+>>> quoted+"dquote='value with \" quotes'" +-- ^ TODO: Note that this is for exact compatibility+ Quotes of both kinds may be present in keys: ->>> parse "'quote\"=value"-Right (fromList [("'quote\"","value")])+>>> toList <$> parse "'quote\"=value"+Right [("'quote\"","value")] Whitespace is ignored everywhere except in quoted strings and inside keys or unquoted values: ->>> parse "; a key = v v\t\n;\t key 2 = \"v v\"\n;\t key 3 = 'v v'; "-Right (fromList [("a key","v v"),("key 2","v v"),("key 3","v v")])+>>> toList <$> parse "; a key = v v\t\n;\t key 2 = \"v v\"\n;\t key 3 = 'v v'; "+Right [("a key","v v"),("key 2","v v"),("key 3","v v")] Equals signs may be escaped in keys by doubling them: ->>> parse "1==2=false"-Right (fromList [("1=2","false")])+>>> toList <$> parse "1==2=false"+Right [("1=2","false")] +Keys are case-insensitive (and converted to lower-case on output):++>>> toList <$> parse "BIG=small"+Right [("big","small")]+ Later values override earlier ones: ->>> parse "key=value;key=value2"-Right (fromList [("key","value2")])+>>> toList <$> parse "key=value;key=value2"+Right [("key","value2")] Assigning a key no value will remove it: ->>> parse "key=value;key="-Right (fromList [])+>>> toList <$> parse "key=value;key="+Right [] However, you can assign an empty value by giving it a quoted value: ->>> parse "key=value;key=''"-Right (fromList [("key","")])+>>> toList <$> parse "key=value;key=''"+Right [("key","")] +-- TODO ^ there appears to be a bug here in .NET+ On the other hand, not providing a key doesn't make any sense: >>> parse "key=value;=value"@@ -70,11 +85,21 @@ >>> parse "=value" Left "1:1:\nunexpected '='\nexpecting ';', end of input, or white space\n" +Another quirk is that keys can contain semicolons:++>>> toList <$> parse "key=value;key2;extended=value"+Right [("key","value"),("key2;extended","value")]+ This module implements all of these quirks for you! -} module Data.ConnectionString ( ConnectionString+ , toList+ , keys+ , values+ , (!)+ , toString , parse , Parseable , parser@@ -82,10 +107,15 @@ where import Data.Char (isSpace)-import Data.List (dropWhileEnd)-import qualified Data.Map as Map+import qualified Data.CaseInsensitive as CI+import Data.Function ((&))+import Data.List (dropWhileEnd, intersperse)+import qualified Data.Map.Strict as Map+import Data.Monoid ((<>)) import Data.String (IsString(..)) import Data.Void (Void)+import qualified GHC.Exts as Exts+import GHC.Exts (IsList(Item)) import Control.Applicative.Combinators (many, sepEndBy, skipMany, skipSome, some, (<|>)) @@ -93,22 +123,93 @@ import Text.Megaparsec (Parsec, try, Stream, Token, (<?>)) import Text.Megaparsec.Char (char, notChar, space) --- | A connection string is a set of keys and values.-type ConnectionString s = Map.Map s s+-- | A connection string is a set of keys that map to values.+newtype ConnectionString s = ConnectionString (Map.Map (Key s) s)+ deriving (Eq) +instance (Show s, IsString s, IsList s, Item s ~ Char) => Show (ConnectionString s) where+ show = show . toString++toList :: ConnectionString s -> [(s, s)]+toList (ConnectionString cs) =+ Map.toList cs+ & map (\(Key k, v) -> (CI.foldedCase k, v))++keys :: ConnectionString s -> [s]+keys (ConnectionString cs) =+ Map.keys cs+ & map (\(Key k) -> CI.foldedCase k)++values :: ConnectionString s -> [s]+values (ConnectionString cs) = Map.elems cs++-- | Tries to find the given key in the connection string,+-- and returns either the value or 'Nothing'.+(!) :: (CI.FoldCase s, Ord s, IsList s, Item s ~ Char) => ConnectionString s -> s -> Maybe s+ConnectionString cs ! key = cs Map.!? toKey key++-- | A key is case-insensitive and does not start+-- or end with whitespace characters.+newtype Key s = Key (CI.CI s)+ deriving (Eq, Ord)++-- | Converts the given string-like type to a key. (Does not fail.)+toKey :: (CI.FoldCase s, IsList s, Item s ~ Char) => s -> Key s+toKey s =+ Exts.toList s+ & dropWhile isSpace+ & dropWhileEnd isSpace+ & Exts.fromList+ & CI.mk+ & Key++-- | Render a 'ConnectionString' as the string type.+toString :: forall s. (IsString s, IsList s, Item s ~ Char) => ConnectionString s -> s+toString (ConnectionString cs) =+ Map.toList cs+ & map (\(k, v) -> encodeKey k <> "=" <> encodeValue v)+ & intersperse ";" + & mconcat+ & Exts.fromList++ where+ encodeKey :: Key s -> String+ encodeKey (Key (CI.foldedCase -> k)) =+ replace '=' "==" (Exts.toList k)++ encodeValue :: s -> String+ encodeValue (Exts.toList -> v)+ | v == "" = "''"+ | hasDquote && not hasSquote = "'" <> v <> "'"+ | hasSemiColon || hasSpace || hasDquote || hasSquote+ = "\"" <> replace '"' "\"\"" v <> "\""+ | otherwise = v+ where+ hasSquote = '\'' `elem` v+ hasDquote = '"' `elem` v+ hasSemiColon = ';' `elem` v+ hasSpace = any isSpace v++ replace :: Char -> String -> String -> String+ replace _ _ [] = []+ replace from to (x:xs)+ | from == x = to ++ replace from to xs+ | otherwise = x : replace from to xs++ -- | Parses a connection string, or fails with an error. -- -- You can parse 'String' inputs: -- -- >>> parse ("key=value;key2=value2")--- Right (fromList [("key","value"),("key2","value2")])+-- Right "key=value;key2=value2" -- -- Or you can parse 'Data.Text.Text' inputs: -- -- >>> :set -XOverloadedStrings -- >>> import Data.Text -- >>> parse ("key=value;key2=value2" :: Text)--- Right (fromList [("key","value"),("key2","value2")])+-- Right "key=value;key2=value2" -- -- In either case, 'parse' will produce a 'ConnectionString' that -- has values of the same type as the input.@@ -116,28 +217,28 @@ parse cs = case (P.parse (parser <* P.eof) "" cs) of Left err -> Left (P.parseErrorPretty err)- Right kvs -> Right (Map.mapMaybe id (Map.fromList kvs))+ Right kvs -> Right (ConnectionString (Map.mapMaybe id (Map.fromList kvs))) -- | The constraints on things that 'parse' can handle. -- (Essentially this means "either 'String' or 'Data.Text.Text'".)-type Parseable s = (Stream s, Token s ~ Char, IsString s, Ord s)+type Parseable s = (Stream s, Token s ~ Char, IsString s, Ord s, CI.FoldCase s) -- | A reusable ("Text.Megaparsec") parser for connection strings. -- -- A 'Nothing' in the output list indicates that the corresponding -- key should be deleted from the set.-parser :: Parseable s => Parsec Void s [(s, Maybe s)]+parser :: Parseable s => Parsec Void s [(Key s, Maybe s)] parser = skipMany semiColon *> keyValue `sepEndBy` (skipSome semiColon) where semiColon = P.between space space (char ';') -keyValue :: Parseable s => Parsec Void s (s, Maybe s)+keyValue :: Parseable s => Parsec Void s (Key s, Maybe s) keyValue = (,) <$> (key <?> "key") <* equals <*> (value <?> "value") where equals = P.between space space (char '=') key =- fromString . dropWhileEnd isSpace <$>+ Key . CI.mk . fromString . dropWhileEnd isSpace <$> some (notChar '=' <|> try (char '=' *> char '=')) value = fmap fromString <$> (sQuoted <|> dQuoted <|> unQuoted)