packages feed

Frames-streamly-0.3.1.0: src/Frames/Streamly/Streaming/Common.hs

{-# LANGUAGE DeriveLift #-}

module Frames.Streamly.Streaming.Common where


import  Language.Haskell.TH.Syntax (Lift)
import qualified Data.Text as T

data Separator = CharSeparator Char | TextSeparator Text deriving (Show, Eq, Ord, Lift)

defaultSep :: Separator
defaultSep = CharSeparator ','
{-# INLINEABLE defaultSep #-}

type QuoteChar = Char

data QuotingMode
    -- | No quoting enabled. The separator may not appear in values
  = NoQuoting
    -- | Quoted values with the given quoting character. Quotes are escaped by doubling them.
    -- Mostly RFC4180 compliant, except doesn't support newlines in values
  | RFC4180Quoting QuoteChar
  deriving (Eq, Show, Lift)

defaultQuotingMode :: QuotingMode
defaultQuotingMode = RFC4180Quoting '\"'
{-# INLINEABLE defaultQuotingMode #-}

separatorToText :: Separator -> Text
separatorToText (CharSeparator c) = T.singleton c
separatorToText (TextSeparator t) = t
{-# INLINEABLE separatorToText #-}

textToSeparator :: Text -> Separator
textToSeparator t = if T.length t == 1 then CharSeparator (T.head t) else TextSeparator t
{-# INLINEABLE textToSeparator #-}

handleQuoting :: Separator -> QuotingMode -> [Text] -> [Text]
handleQuoting sep quoting = case quoting of
  NoQuoting-> id
  RFC4180Quoting quote -> reassembleRFC4180QuotedParts (separatorToText sep) quote
{-# INLINEABLE handleQuoting #-}

-- | Helper to split a 'T.Text' on commas and strip leading and
-- trailing whitespace from each resulting chunk.
tokenizeRow :: Separator -> QuotingMode -> T.Text -> [T.Text]
tokenizeRow sep quoting = handleQuoting sep quoting . splitRow sep
{-# INLINEABLE tokenizeRow #-}

splitRow :: Separator -> T.Text -> [T.Text]
splitRow sep = case sep of
  CharSeparator c -> T.split (== c)
  TextSeparator t -> T.splitOn t
{-# INLINEABLE splitRow #-}

-- | Post processing applied to a list of tokens split by the
-- separator which should have quoted sections reassembeld
reassembleRFC4180QuotedParts :: Text -> Char -> [T.Text] -> [T.Text]
reassembleRFC4180QuotedParts sep quoteChar = go
  where go [] = []
        go (part:parts)
          | T.null part = T.empty : go parts
          | prefixQuoted part =
            if suffixQuoted part
            then unescape (T.drop 1 . T.dropEnd 1 $ part) : go parts
            else case break suffixQuoted parts of
                   (h,[]) -> [unescape (T.intercalate sep (T.drop 1 part : h))]
                   (h,t:ts) -> unescape
                                 (T.intercalate
                                    sep
                                    (T.drop 1 part : h ++ [T.dropEnd 1 t]))
                               : go ts
          | otherwise = T.strip part : go parts
        {-# INLINE prefixQuoted #-}
        prefixQuoted t =
          T.head t == quoteChar &&
          T.length (T.takeWhile (== quoteChar) t) `rem` 2 == 1
        {-# INLINE suffixQuoted #-}
        suffixQuoted t =
          quoteText `T.isSuffixOf` t &&
          T.length (T.takeWhileEnd (== quoteChar) t) `rem` 2 == 1
        {-# INLINE quoteText #-}
        quoteText = T.singleton quoteChar
        {-# INLINE unescape #-}
        unescape :: T.Text -> T.Text
        unescape = T.replace q2 quoteText
          where q2 = quoteText <> quoteText
{-# INLINEABLE reassembleRFC4180QuotedParts #-}