2024-04-30 19:09:12 +00:00
|
|
|
module Node.Stream.CSV.Stringify where
|
|
|
|
|
|
|
|
import Prelude
|
|
|
|
|
|
|
|
import Control.Monad.Rec.Class (whileJust)
|
|
|
|
import Control.Monad.ST.Global as ST
|
2024-05-01 13:52:13 +00:00
|
|
|
import Data.Array as Array
|
2024-04-30 19:09:12 +00:00
|
|
|
import Data.Array.ST as Array.ST
|
|
|
|
import Data.CSV.Record (class WriteCSVRecord, writeCSVRecord)
|
|
|
|
import Data.Either (Either(..), blush)
|
|
|
|
import Data.Foldable (class Foldable, fold)
|
|
|
|
import Data.Maybe (Maybe(..))
|
|
|
|
import Data.String.Regex (Regex)
|
|
|
|
import Data.Traversable (for_)
|
|
|
|
import Effect (Effect)
|
|
|
|
import Effect.Aff (Aff, makeAff)
|
|
|
|
import Effect.Class (liftEffect)
|
|
|
|
import Foreign (Foreign, unsafeToForeign)
|
|
|
|
import Foreign.Object (Object)
|
|
|
|
import Foreign.Object as Object
|
|
|
|
import Node.EventEmitter as Event
|
|
|
|
import Node.Stream (Read, Stream, Write)
|
|
|
|
import Node.Stream as Stream
|
|
|
|
import Prim.Row (class Union)
|
|
|
|
import Prim.RowList (class RowToList)
|
2024-05-01 13:52:13 +00:00
|
|
|
import Record.Extra (class Keys, keys)
|
|
|
|
import Type.Prelude (Proxy(..))
|
2024-04-30 19:09:12 +00:00
|
|
|
import Unsafe.Coerce (unsafeCoerce)
|
|
|
|
|
|
|
|
data CSVWrite
|
|
|
|
|
|
|
|
-- | Stream transforming rows of stringified CSV values
|
|
|
|
-- | to CSV-formatted rows.
|
|
|
|
-- |
|
|
|
|
-- | Write rows to the stream using `write`.
|
|
|
|
-- |
|
|
|
|
-- | Stringified rows are emitted on the `Readable` end as string
|
|
|
|
-- | chunks, meaning it can be treated as a `Node.Stream.Readable`
|
|
|
|
-- | that has had `setEncoding UTF8` invoked on it.
|
|
|
|
type CSVStringifier :: Row Type -> Row Type -> Type
|
|
|
|
type CSVStringifier a r = Stream (read :: Read, write :: Write, csv :: CSVWrite | r)
|
|
|
|
|
|
|
|
-- | https://csv.js.org/stringify/options/
|
|
|
|
type Config r =
|
|
|
|
( bom :: Boolean
|
|
|
|
, delimiter :: String
|
|
|
|
, record_delimiter :: String
|
|
|
|
, escape :: String
|
|
|
|
, escape_formulas :: Boolean
|
|
|
|
, header :: Boolean
|
|
|
|
, quote :: String
|
|
|
|
, quoted :: Boolean
|
|
|
|
, quoted_empty :: Boolean
|
|
|
|
, quoted_match :: Regex
|
|
|
|
, quoted_string :: Boolean
|
|
|
|
| r
|
|
|
|
)
|
|
|
|
|
|
|
|
foreign import makeImpl :: forall r. Foreign -> Effect (Stream r)
|
|
|
|
foreign import writeImpl :: forall r. Stream r -> Array String -> Effect Unit
|
|
|
|
|
|
|
|
recordToForeign :: forall r. Record r -> Object Foreign
|
|
|
|
recordToForeign = unsafeCoerce
|
|
|
|
|
|
|
|
-- | Create a CSVStringifier
|
2024-05-01 13:52:13 +00:00
|
|
|
make :: forall @r rl @config @missing @extra. Keys rl => RowToList r rl => WriteCSVRecord r rl => Union config missing (Config extra) => { | config } -> Effect (CSVStringifier r ())
|
|
|
|
make = makeImpl <<< unsafeToForeign <<< Object.union (recordToForeign {columns: Array.fromFoldable $ keys (Proxy @r)}) <<< recordToForeign
|
2024-04-30 19:09:12 +00:00
|
|
|
|
|
|
|
-- | Synchronously stringify a collection of records
|
2024-05-01 13:52:13 +00:00
|
|
|
stringify :: forall @r rl f @config missing extra. Keys rl => Foldable f => RowToList r rl => WriteCSVRecord r rl => Union config missing (Config extra) => { | config } -> f { | r } -> Aff String
|
2024-04-30 19:09:12 +00:00
|
|
|
stringify config records = do
|
|
|
|
stream <- liftEffect $ make @r @config @missing @extra config
|
|
|
|
liftEffect $ for_ records \r -> write stream r
|
|
|
|
liftEffect $ Stream.end stream
|
|
|
|
readAll stream
|
|
|
|
|
|
|
|
-- | Write a record to a CSVStringifier.
|
|
|
|
-- |
|
|
|
|
-- | The record will be emitted on the `Readable` end
|
|
|
|
-- | of the stream as a string chunk.
|
|
|
|
write :: forall @r rl a. RowToList r rl => WriteCSVRecord r rl => CSVStringifier r a -> { | r } -> Effect Unit
|
|
|
|
write s = writeImpl s <<< writeCSVRecord @r @rl
|
|
|
|
|
2024-05-01 00:09:52 +00:00
|
|
|
-- | Loop until the stream is closed, invoking the callback with each chunk of stringified CSV text.
|
|
|
|
foreach :: forall r x. CSVStringifier r x -> (String -> Aff Unit) -> Aff Unit
|
|
|
|
foreach stream cb = whileJust do
|
|
|
|
isReadable <- liftEffect $ Stream.readable stream
|
|
|
|
when (not isReadable) $ makeAff \res -> mempty <* flip (Event.once Stream.readableH) stream $ res $ Right unit
|
|
|
|
whileJust do
|
|
|
|
s <- liftEffect $ (join <<< map blush) <$> Stream.readEither stream
|
|
|
|
for_ s cb
|
|
|
|
pure $ void s
|
|
|
|
isClosed <- liftEffect $ Stream.closed stream
|
|
|
|
pure $ if isClosed then Nothing else Just unit
|
|
|
|
|
2024-04-30 19:09:12 +00:00
|
|
|
-- | Read the stringified chunks until end-of-stream, returning the entire CSV string.
|
|
|
|
readAll :: forall r a. CSVStringifier r a -> Aff String
|
|
|
|
readAll stream = do
|
|
|
|
chunks <- liftEffect $ ST.toEffect $ Array.ST.new
|
2024-05-01 00:09:52 +00:00
|
|
|
foreach stream $ void <<< liftEffect <<< ST.toEffect <<< flip Array.ST.push chunks
|
2024-04-30 19:09:12 +00:00
|
|
|
chunks' <- liftEffect $ ST.toEffect $ Array.ST.unsafeFreeze chunks
|
|
|
|
pure $ fold chunks'
|