module Data.Sv.Encode (
Encode (..)
, mkEncodeBS
, mkEncodeWithOpts
, unsafeBuilder
, encode
, encodeToHandle
, encodeToFile
, encodeBuilder
, encodeRow
, encodeRowBuilder
, encodeSv
, 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
, unsafeString
, unsafeText
, unsafeByteString
, unsafeLazyByteString
, unsafeByteStringBuilder
, unsafeConst
) where
import qualified Prelude as P
import Prelude hiding (const, show)
import Control.Applicative ((<$>), (<**>))
import Control.Lens (Getting, preview, review, 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, foldMap, toList)
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Functor.Contravariant.Divisible (Divisible (conquer), Decidable (choose))
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Data.Monoid (Monoid (mempty), First, (<>), mconcat)
import Data.Sequence (Seq, ViewL (EmptyL, (:<)), viewl, (<|))
import qualified Data.Sequence as Seq
import qualified Data.Sequence as S (singleton, empty)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import System.IO (BufferMode (BlockBuffering), Handle, hClose, hSetBinaryMode, hSetBuffering, openFile, IOMode (WriteMode))
import Data.Sv.Encode.Options (EncodeOptions (..), HasEncodeOptions (..), HasSeparator (..), defaultEncodeOptions)
import Data.Sv.Encode.Type (Encode (Encode, getEncode))
import Data.Sv.Syntax.Field (Field (Unquoted), SpacedField, unescapedField)
import Data.Sv.Syntax.Record (Record (Record), Records (EmptyRecords), emptyRecord, mkRecords, recordNel)
import Data.Sv.Syntax.Sv (Sv (Sv), Header (Header))
import qualified Data.Vector.NonEmpty as V
import Text.Escape (Escaper, Escaper', Unescaped (Unescaped), escapeChar, escapeString, escapeText, escapeUtf8, escapeUtf8Lazy)
import Text.Newline (newlineToString)
import Text.Space (Spaced (Spaced), spacesString)
import Text.Quote (quoteChar)
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))
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 = newlineToString (_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.charUtf8 (view separator opts))
quotep = foldMap (BS.charUtf8 . review quoteChar) (view quote opts)
addQuotes x = quotep <> x <> quotep
mkSpaces optic = BS.stringUtf8 . review spacesString . view optic $ opts
bspaces = mkSpaces spacingBefore
aspaces = mkSpaces spacingAfter
addSpaces x = bspaces <> x <> aspaces
in fold . addSeparators . fmap (addSpaces . addQuotes) . getEncode e opts
encodeSv :: Encode a -> EncodeOptions -> Maybe (NonEmpty Strict.ByteString) -> [a] -> Sv Strict.ByteString
encodeSv e opts headerStrings as =
let encoded :: [Seq BS.Builder]
encoded = getEncode e opts <$> as
nl = view newline opts
sep = view separator opts
mkSpaced = Spaced (_spacingBefore opts) (_spacingAfter opts)
mkField = maybe Unquoted unescapedField (_quote opts)
mkHeader r = Header r nl
mkRecord :: NonEmpty z -> Record z
mkRecord = recordNel . fmap (mkSpaced . mkField)
header :: Maybe (Header Strict.ByteString)
header = mkHeader . mkRecord <$> headerStrings
rs :: Records Strict.ByteString
rs = l2rs (b2r <$> encoded)
l2rs = maybe EmptyRecords (mkRecords nl) . nonEmpty
terminal = if _terminalNewline opts then [nl] else []
b2f :: BS.Builder -> SpacedField Strict.ByteString
b2f = mkSpaced . mkField . LBS.toStrict . BS.toLazyByteString
b2r :: Seq BS.Builder -> Record Strict.ByteString
b2r = maybe emptyRecord (Record . V.fromNel) . nonEmpty . toList . fmap b2f
in Sv sep header rs terminal
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 (<?)
(<?) :: Encode () -> Encode a -> Encode (Maybe a)
(<?) = choose (maybe (Left ()) Right)
(?>>) :: Encode a -> Strict.ByteString -> Encode (Maybe a)
(?>>) a s = a ?> const s
(<<?) :: Strict.ByteString -> Encode a -> Encode (Maybe a)
(<<?) = flip (?>>)
row :: Encode s -> Encode [s]
row enc = Encode $ \opts list -> join $ Seq.fromList $ fmap (getEncode enc opts) list
char :: Encode Char
char = escaped escapeChar BS.charUtf8 BS.stringUtf8
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' escapeString BS.stringUtf8
text :: Encode T.Text
text = escaped' escapeText (BS.byteString . T.encodeUtf8)
byteString :: Encode Strict.ByteString
byteString = escaped' escapeUtf8 BS.byteString
lazyByteString :: Encode LBS.ByteString
lazyByteString = escaped' escapeUtf8Lazy BS.lazyByteString
escaped :: Escaper s t -> (s -> BS.Builder) -> (t -> BS.Builder) -> Encode s
escaped esc sb tb = mkEncodeWithOpts $ \opts s ->
case _quote opts of
Nothing -> sb s
Just q -> tb $ esc (review quoteChar q) (Unescaped s)
escaped' :: Escaper' s -> (s -> BS.Builder) -> Encode s
escaped' escaper = join (escaped escaper)
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
intersperseSeq :: a -> Seq a -> Seq a
intersperseSeq y xs = case viewl xs of
EmptyL -> S.empty
p :< ps -> p <| (ps <**> (pure y <| S.singleton id))