connection-string-0.1.0.0: src/Data/ConnectionString.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE GADTs #-}
{-|
Module: Data.ConnectionString
== Introduction
This module is intended for parsing connection strings in a manner
that is consistent with .NET's <https://docs.microsoft.com/en/dotnet/api/system.data.common.dbconnectionstringbuilder DbConnectionStringBuilder>
class.
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")])
However, the format can be more complicated than expected.
== Examples
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")])
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")])
Quotes of both kinds may be present in keys:
>>> parse "'quote\"=value"
Right (fromList [("'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")])
Equals signs may be escaped in keys by doubling them:
>>> parse "1==2=false"
Right (fromList [("1=2","false")])
Later values override earlier ones:
>>> parse "key=value;key=value2"
Right (fromList [("key","value2")])
Assigning a key no value will remove it:
>>> parse "key=value;key="
Right (fromList [])
However, you can assign an empty value by giving it a quoted value:
>>> parse "key=value;key=''"
Right (fromList [("key","")])
On the other hand, not providing a key doesn't make any sense:
>>> parse "key=value;=value"
Left "1:11:\nunexpected '='\nexpecting ';', end of input, or white space\n"
>>> parse "=value"
Left "1:1:\nunexpected '='\nexpecting ';', end of input, or white space\n"
This module implements all of these quirks for you!
-}
module Data.ConnectionString
( ConnectionString
, parse
, Parseable
, parser
)
where
import Data.Char (isSpace)
import Data.List (dropWhileEnd)
import qualified Data.Map as Map
import Data.String (IsString(..))
import Data.Void (Void)
import Control.Applicative.Combinators (many, sepEndBy, skipMany, skipSome, some, (<|>))
import qualified Text.Megaparsec as P
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
-- | 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")])
--
-- 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")])
--
-- In either case, 'parse' will produce a 'ConnectionString' that
-- has values of the same type as the input.
parse :: Parseable s => s -> Either String (ConnectionString s)
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))
-- | 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)
-- | 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 = skipMany semiColon *> keyValue `sepEndBy` (skipSome semiColon)
where
semiColon = P.between space space (char ';')
keyValue :: Parseable s => Parsec Void s (s, Maybe s)
keyValue = (,) <$> (key <?> "key") <* equals <*> (value <?> "value")
where
equals = P.between space space (char '=')
key =
fromString . dropWhileEnd isSpace <$>
some (notChar '=' <|> try (char '=' *> char '='))
value = fmap fromString <$> (sQuoted <|> dQuoted <|> unQuoted)
sQuoted =
Just <$>
P.between (char '\'') (char '\'') (many (notChar '\'' <|> try (char '\'' *> char '\'')))
dQuoted =
Just <$>
P.between (char '"') (char '"') (many (notChar '"' <|> try (char '"' *> char '"')))
unQuoted =
-- a completely empty value indicates we should remove it (see docs
-- above), so we return Nothing in this case
(\s -> if null s then Nothing else Just s) . dropWhileEnd isSpace <$>
(space *> many (notChar ';') <* space)