{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Sv.Encode.Core (
Encode (..)
, mkEncodeBS
, mkEncodeWithOpts
, encode
, encodeNamed
, encodeToHandle
, encodeNamedToHandle
, encodeToFile
, encodeNamedToFile
, encodeBuilder
, encodeNamedBuilder
, encodeRow
, encodeRowBuilder
, module Data.Sv.Encode.Options
, named
, (=:)
, 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 Control.Monad.Writer (runWriter, writer)
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.Compose (ComposeFC (ComposeFC, getComposeFC))
import Data.Functor.Contravariant.Divisible (Divisible (conquer), Decidable (choose))
import Data.Monoid (Monoid (mempty), First, (<>), mconcat)
import Data.Sequence (Seq)
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 (EncodeOptions, _encodeSeparator, _newline, _terminalNewline, _quoting), HasEncodeOptions (), HasSeparator (separator), defaultEncodeOptions, Quoting (Always, AsNeeded, Never))
import Data.Sv.Encode.Type (Encode (Encode, getEncode), NameEncode (NameEncode, unNamedE))
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
encodeNamed :: NameEncode a -> EncodeOptions -> [a] -> LBS.ByteString
encodeNamed enc opts = BS.toLazyByteString . encodeNamedBuilder enc opts
encodeToHandle :: Encode a -> EncodeOptions -> [a] -> Handle -> IO ()
encodeToHandle enc opts as h =
BS.hPutBuilder h (encodeBuilder enc opts as)
encodeNamedToHandle :: NameEncode a -> EncodeOptions -> [a] -> Handle -> IO ()
encodeNamedToHandle enc opts as h =
BS.hPutBuilder h (encodeNamedBuilder enc opts as)
encodeToFile :: Encode a -> EncodeOptions -> [a] -> FilePath -> IO ()
encodeToFile = genericEncodeToFile encodeToHandle
encodeNamedToFile :: NameEncode a -> EncodeOptions -> [a] -> FilePath -> IO ()
encodeNamedToFile = genericEncodeToFile encodeNamedToHandle
genericEncodeToFile
:: (enc -> EncodeOptions -> [a] -> Handle -> IO ())
-> enc -> EncodeOptions -> [a] -> FilePath -> IO ()
genericEncodeToFile encHandle enc opts as fp = do
h <- openFile fp WriteMode
hSetBuffering h (BlockBuffering Nothing)
hSetBinaryMode h True
encHandle enc opts as h
hClose h
{-# INLINE genericEncodeToFile #-}
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
encodeNamedBuilder :: NameEncode a -> EncodeOptions -> [a] -> BS.Builder
encodeNamedBuilder ne opts as =
case runNamed ne of
(e, builders) ->
let mkHeader = fold . addSeparators opts . addQuoting opts
addQuoting = fmap . enquote
nl = newlineToBuilder (_newline opts)
header = mkHeader builders
in header <> case as of
[] -> if _terminalNewline opts then nl else mempty
(_:_) -> nl <> encodeBuilder e opts as
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 =
fold . addSeparators opts . getEncode e opts
addSeparators :: HasSeparator s => s -> Seq BS.Builder -> Seq BS.Builder
addSeparators opts = intersperseSeq (BS.word8 (view separator opts))
{-# INLINE addSeparators #-}
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
enquote :: EncodeOptions -> BS.Builder -> BS.Builder
enquote opts s =
let 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
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 ->
enquote opts (build s)
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"
mkNamed :: Encode a -> Seq BS.Builder -> NameEncode a
mkNamed enc b = NameEncode (ComposeFC (writer (enc, b)))
named :: BS.Builder -> Encode a -> NameEncode a
named name enc = mkNamed enc (pure name)
(=:) :: BS.Builder -> Encode a -> NameEncode a
(=:) = named
runNamed :: NameEncode a -> (Encode a, Seq BS.Builder)
runNamed = runWriter . getComposeFC . unNamedE
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