packages feed

hpgsql-simple-compat-0.1.0.0: src/Database/PostgreSQL/Simple/HStore/Implementation.hs

------------------------------------------------------------------------------

------------------------------------------------------------------------------

-- |
-- Module:      Database.PostgreSQL.Simple.HStore.Implementation
-- Copyright:   (c) 2013 Leon P Smith
-- License:     BSD3
-- Maintainer:  Leon P Smith <leon@melding-monads.com>
-- Stability:   experimental
--
-- This code has yet to be profiled and optimized.
module Database.PostgreSQL.Simple.HStore.Implementation where

import Control.Applicative
import qualified Data.Attoparsec.ByteString as P
import qualified Data.Attoparsec.ByteString.Char8 as P (isSpace_w8)
import qualified Data.ByteString as BS
import Data.ByteString.Builder (Builder, byteString, char8)
import qualified Data.ByteString.Builder as BU
import Data.ByteString.Internal (c2w, w2c)
import qualified Data.ByteString.Lazy as BL
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as TS
import qualified Data.Text.Encoding as TS
import Data.Text.Encoding.Error (UnicodeException)
import qualified Data.Text.Lazy as TL
import Data.Typeable

class ToHStore a where
  toHStore :: a -> HStoreBuilder

-- | Represents valid hstore syntax.
data HStoreBuilder
  = Empty
  | Comma !Builder
  deriving (Typeable)

instance ToHStore HStoreBuilder where
  toHStore = id

toBuilder :: HStoreBuilder -> Builder
toBuilder x = case x of
  Empty -> mempty
  Comma c -> c

toLazyByteString :: HStoreBuilder -> BL.ByteString
toLazyByteString x = case x of
  Empty -> BL.empty
  Comma c -> BU.toLazyByteString c

instance Semigroup HStoreBuilder where
  Empty <> x = x
  Comma a <> x =
    Comma
      ( a `mappend` case x of
          Empty -> mempty
          Comma b -> char8 ',' `mappend` b
      )

instance Monoid HStoreBuilder where
  mempty = Empty

class ToHStoreText a where
  toHStoreText :: a -> HStoreText

-- | Represents escape text, ready to be the key or value to a hstore value
newtype HStoreText = HStoreText Builder
  deriving stock (Typeable)
  deriving newtype (Semigroup, Monoid)

instance ToHStoreText HStoreText where
  toHStoreText = id

-- | Assumed to be UTF-8 encoded
instance ToHStoreText BS.ByteString where
  toHStoreText str = HStoreText (escapeAppend str mempty)

-- | Assumed to be UTF-8 encoded
instance ToHStoreText BL.ByteString where
  toHStoreText = HStoreText . BL.foldrChunks escapeAppend mempty

instance ToHStoreText TS.Text where
  toHStoreText str = HStoreText (escapeAppend (TS.encodeUtf8 str) mempty)

instance ToHStoreText TL.Text where
  toHStoreText = HStoreText . TL.foldrChunks (escapeAppend . TS.encodeUtf8) mempty

escapeAppend :: BS.ByteString -> Builder -> Builder
escapeAppend = loop
  where
    loop (BS.break quoteNeeded -> (a, b)) rest =
      byteString a
        `mappend` case BS.uncons b of
          Nothing -> rest
          Just (c, d) -> quoteChar c `mappend` loop d rest

    quoteNeeded c = c == c2w '\"' || c == c2w '\\'
    quoteChar c
      | c == c2w '\"' = byteString "\\\""
      | otherwise = byteString "\\\\"

hstore :: (ToHStoreText a, ToHStoreText b) => a -> b -> HStoreBuilder
hstore (toHStoreText -> (HStoreText key)) (toHStoreText -> (HStoreText val)) =
  Comma
    ( char8 '"'
        `mappend` key
        `mappend` byteString "\"=>\""
        `mappend` val
        `mappend` char8 '"'
    )

newtype HStoreList = HStoreList {fromHStoreList :: [(Text, Text)]} deriving (Typeable, Show)

-- | hstore
instance ToHStore HStoreList where
  toHStore (HStoreList xs) = mconcat (map (uncurry hstore) xs)

newtype HStoreMap = HStoreMap {fromHStoreMap :: Map Text Text} deriving (Eq, Ord, Typeable, Show)

instance ToHStore HStoreMap where
  toHStore (HStoreMap xs) = Map.foldrWithKey f mempty xs
    where
      f k v xs' = hstore k v `mappend` xs'

parseHStoreList :: BS.ByteString -> Either String HStoreList
parseHStoreList dat =
  case P.parseOnly (parseHStore <* P.endOfInput) dat of
    Left err -> Left (show err)
    Right (Left err) -> Left (show err)
    Right (Right val) -> Right val

parseHStore :: P.Parser (Either UnicodeException HStoreList)
parseHStore = do
  kvs <-
    P.sepBy'
      (skipWhiteSpace *> parseHStoreKeyVal)
      (skipWhiteSpace *> P.word8 (c2w ','))
  return $ HStoreList <$> sequence kvs

parseHStoreKeyVal :: P.Parser (Either UnicodeException (Text, Text))
parseHStoreKeyVal = do
  mkey <- parseHStoreText
  case mkey of
    Left err -> return (Left err)
    Right key -> do
      skipWhiteSpace
      _ <- P.string "=>"
      skipWhiteSpace
      mval <- parseHStoreText
      case mval of
        Left err -> return (Left err)
        Right val -> return (Right (key, val))

skipWhiteSpace :: P.Parser ()
skipWhiteSpace = P.skipWhile P.isSpace_w8

parseHStoreText :: P.Parser (Either UnicodeException Text)
parseHStoreText = do
  _ <- P.word8 (c2w '"')
  mtexts <- parseHStoreTexts id
  case mtexts of
    Left err -> return (Left err)
    Right texts -> do
      _ <- P.word8 (c2w '"')
      return (Right (TS.concat texts))

parseHStoreTexts ::
  ([Text] -> [Text]) ->
  P.Parser (Either UnicodeException [Text])
parseHStoreTexts acc = do
  mchunk <- TS.decodeUtf8' <$> P.takeWhile (not . isSpecialChar)
  case mchunk of
    Left err -> return (Left err)
    Right chunk ->
      ( do
          _ <- P.word8 (c2w '\\')
          c <- TS.singleton . w2c <$> P.satisfy isSpecialChar
          parseHStoreTexts (acc . (chunk :) . (c :))
      )
        <|> return (Right (acc [chunk]))
  where
    isSpecialChar c = c == c2w '\\' || c == c2w '"'