{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Sv.Encode.Core (
Encode (..)
, mkEncodeBS
, mkEncodeWithOpts
, encode
, encodeToHandle
, encodeToFile
, encodeBuilder
, encodeRow
, encodeRowBuilder
, module Data.Sv.Encode.Options
, const
, show
, nop
, empty
, orEmpty
, char
, int
, integer
, float
, double
, boolTrueFalse
, booltruefalse
, boolyesno
, boolYesNo
, boolYN
, bool10
, string
, text
, byteString
, lazyByteString
, row
, (?>)
, (<?)
, (?>>)
, (<<?)
, encodeOf
, encodeOfMay
, unsafeBuilder
, unsafeString
, unsafeText
, unsafeByteString
, unsafeLazyByteString
, unsafeByteStringBuilder
, unsafeConst
) where
import qualified Prelude as P
import Prelude hiding (const, show)
import Control.Lens (Getting, preview, view)
import Control.Monad (join)
import qualified Data.Bool as B (bool)
import qualified Data.ByteString as Strict
import qualified Data.ByteString.Builder as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Foldable (fold)
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Functor.Contravariant.Divisible (Divisible (conquer), Decidable (choose))
import Data.Monoid (Monoid (mempty), First, (<>), mconcat)
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import GHC.Word (Word8)
import System.IO (BufferMode (BlockBuffering), Handle, hClose, hSetBinaryMode, hSetBuffering, openFile, IOMode (WriteMode))
import Data.Sv.Alien.Containers (intersperseSeq)
import Data.Sv.Encode.Options (EncodeOptions (..), HasEncodeOptions (..), HasSeparator (..), defaultEncodeOptions, Quoting (..))
import Data.Sv.Encode.Type (Encode (Encode, getEncode))
import Data.Sv.Structure.Newline (newlineToBuilder)
mkEncodeBS :: (a -> LBS.ByteString) -> Encode a
mkEncodeBS = unsafeBuilder . fmap BS.lazyByteString
mkEncodeWithOpts :: (EncodeOptions -> a -> BS.Builder) -> Encode a
mkEncodeWithOpts = Encode . fmap (fmap pure)
unsafeBuilder :: (a -> BS.Builder) -> Encode a
unsafeBuilder b = Encode (\_ a -> pure (b a))
{-# INLINE unsafeBuilder #-}
encode :: Encode a -> EncodeOptions -> [a] -> LBS.ByteString
encode enc opts = BS.toLazyByteString . encodeBuilder enc opts
encodeToHandle :: Encode a -> EncodeOptions -> [a] -> Handle -> IO ()
encodeToHandle enc opts as h =
BS.hPutBuilder h (encodeBuilder enc opts as)
encodeToFile :: Encode a -> EncodeOptions -> [a] -> FilePath -> IO ()
encodeToFile enc opts as fp = do
h <- openFile fp WriteMode
hSetBuffering h (BlockBuffering Nothing)
hSetBinaryMode h True
encodeToHandle enc opts as h
hClose h
encodeBuilder :: Encode a -> EncodeOptions -> [a] -> BS.Builder
encodeBuilder e opts as =
let enc = encodeRowBuilder e opts
nl = newlineToBuilder (_newline opts)
terminal = if _terminalNewline opts then nl else mempty
in case as of
[] -> terminal
(a:as') -> enc a <> mconcat [nl <> enc a' | a' <- as'] <> terminal
encodeRow :: Encode a -> EncodeOptions -> a -> LBS.ByteString
encodeRow e opts = BS.toLazyByteString . encodeRowBuilder e opts
encodeRowBuilder :: Encode a -> EncodeOptions -> a -> BS.Builder
encodeRowBuilder e opts =
let addSeparators = intersperseSeq (BS.word8 (view separator opts))
in fold . addSeparators . getEncode e opts
const :: Strict.ByteString -> Encode a
const b = contramap (pure b) byteString
show :: Show a => Encode a
show = contramap P.show string
nop :: Encode a
nop = conquer
empty :: Encode a
empty = Encode (pure (pure (pure mempty)))
orEmpty :: Encode a -> Encode (Maybe a)
orEmpty = choose (maybe (Left ()) Right) empty
(?>) :: Encode a -> Encode () -> Encode (Maybe a)
(?>) = flip (<?)
{-# INLINE (?>) #-}
(<?) :: Encode () -> Encode a -> Encode (Maybe a)
(<?) = choose (maybe (Left ()) Right)
{-# INLINE (<?) #-}
(?>>) :: Encode a -> Strict.ByteString -> Encode (Maybe a)
(?>>) a s = a ?> const s
{-# INLINE (?>>) #-}
(<<?) :: Strict.ByteString -> Encode a -> Encode (Maybe a)
(<<?) = flip (?>>)
{-# INLINE (<<?) #-}
row :: Encode s -> Encode [s]
row enc = Encode $ \opts list -> join $ Seq.fromList $ fmap (getEncode enc opts) list
char :: Encode Char
char = escaped BS.charUtf8
quotingIsNecessary :: EncodeOptions -> LBS.ByteString -> Bool
quotingIsNecessary opts bs =
LBS.any p bs
where
sep = _encodeSeparator opts
p :: Word8 -> Bool
p w =
w == sep ||
w == 10 ||
w == 13 ||
w == 34
quote :: LBS.ByteString -> BS.Builder
quote bs =
let q = BS.charUtf8 '"'
bs' = BS.lazyByteString (escapeQuotes bs)
in q <> bs' <> q
escapeQuotes :: LBS.ByteString -> LBS.ByteString
escapeQuotes = LBS.concatMap duplicateQuote
where
duplicateQuote :: Word8 -> LBS.ByteString
duplicateQuote 34 = LBS.pack [34,34]
duplicateQuote c = LBS.singleton c
int :: Encode Int
int = unsafeBuilder BS.intDec
integer :: Encode Integer
integer = unsafeBuilder BS.integerDec
float :: Encode Float
float = unsafeBuilder BS.floatDec
double :: Encode Double
double = unsafeBuilder BS.doubleDec
string :: Encode String
string = escaped BS.stringUtf8
text :: Encode T.Text
text = escaped (BS.byteString . T.encodeUtf8)
byteString :: Encode Strict.ByteString
byteString = escaped BS.byteString
lazyByteString :: Encode LBS.ByteString
lazyByteString = escaped BS.lazyByteString
escaped :: (s -> BS.Builder) -> Encode s
escaped build =
mkEncodeWithOpts $ \opts s ->
let s' = build s
lbs = BS.toLazyByteString s'
quoted = quote lbs
in case _quoting opts of
Never ->
s'
AsNeeded ->
if quotingIsNecessary opts lbs
then quoted
else s'
Always -> quoted
boolTrueFalse :: Encode Bool
boolTrueFalse = mkEncodeBS $ B.bool "False" "True"
booltruefalse :: Encode Bool
booltruefalse = mkEncodeBS $ B.bool "false" "true"
boolyesno :: Encode Bool
boolyesno = mkEncodeBS $ B.bool "no" "yes"
boolYesNo :: Encode Bool
boolYesNo = mkEncodeBS $ B.bool "No" "Yes"
boolYN :: Encode Bool
boolYN = mkEncodeBS $ B.bool "N" "Y"
bool10 :: Encode Bool
bool10 = mkEncodeBS $ B.bool "0" "1"
encodeOf :: Getting (First a) s a -> Encode a -> Encode s
encodeOf g = encodeOfMay g . choose (maybe (Left ()) Right) conquer
encodeOfMay :: Getting (First a) s a -> Encode (Maybe a) -> Encode s
encodeOfMay g x = contramap (preview g) x
unsafeString :: Encode String
unsafeString = unsafeBuilder BS.stringUtf8
unsafeText :: Encode T.Text
unsafeText = unsafeBuilder (BS.byteString . T.encodeUtf8)
unsafeByteStringBuilder :: Encode BS.Builder
unsafeByteStringBuilder = unsafeBuilder id
unsafeByteString :: Encode Strict.ByteString
unsafeByteString = unsafeBuilder BS.byteString
unsafeLazyByteString :: Encode LBS.ByteString
unsafeLazyByteString = unsafeBuilder BS.lazyByteString
unsafeConst :: Strict.ByteString -> Encode a
unsafeConst b = contramap (pure b) unsafeByteString