fix: rename modules, add readme
This commit is contained in:
parent
8c578b42bd
commit
38e4e85859
81
README.md
Normal file
81
README.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# purescript-csv-stream
|
||||||
|
|
||||||
|
Type-safe bindings for the streaming API of `csv-parse` and `csv-stringify`.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
```bash
|
||||||
|
spago install csv-stream
|
||||||
|
{bun|yarn|npm|pnpm} install csv-parse csv-stringify
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
### Stream
|
||||||
|
```purescript
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Class (liftEffect)
|
||||||
|
import Effect.Aff (launchAff_)
|
||||||
|
import Node.Stream (pipe)
|
||||||
|
import Node.Stream as Stream
|
||||||
|
import Node.Stream.CSV.Stringify as CSV.Stringify
|
||||||
|
import Node.Stream.CSV.Parse as CSV.Parse
|
||||||
|
|
||||||
|
type MyCSVType1 = {a :: Int, b :: Int, bar :: String, baz :: Boolean}
|
||||||
|
type MyCSVType2 = {ab :: Int, bar :: String, baz :: Boolean}
|
||||||
|
|
||||||
|
atob :: MyCSVType1 -> MyCSVType2
|
||||||
|
atob {a, b, bar, baz} = {ab: a + b, bar, baz}
|
||||||
|
|
||||||
|
myCSV :: String
|
||||||
|
myCSV = "a,b,bar,baz\n1,2,\"hello, world!\",true\n3,3,,f"
|
||||||
|
|
||||||
|
main :: Effect Unit
|
||||||
|
main = launchAff_ do
|
||||||
|
parser <- liftEffect $ CSV.Parse.make {}
|
||||||
|
stringifier <- liftEffect $ CSV.Stringify.make {}
|
||||||
|
|
||||||
|
input <- liftEffect $ Stream.readableFromString myCSV
|
||||||
|
liftEffect $ Stream.pipe input parser
|
||||||
|
|
||||||
|
records <- CSV.Parse.readAll parser
|
||||||
|
liftEffect $ for_ records \r -> CSV.Stringify.write $ atob r
|
||||||
|
liftEffect $ Stream.end stringifier
|
||||||
|
|
||||||
|
-- "ab,bar,baz\n3,\"hello, world!\",true\n6,,false"
|
||||||
|
csvString <- CSV.Stringify.readAll stringifier
|
||||||
|
pure unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Synchronous
|
||||||
|
```purescript
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Effect (Effect)
|
||||||
|
import Effect.Class (liftEffect)
|
||||||
|
import Effect.Aff (launchAff_)
|
||||||
|
import Node.Stream (pipe)
|
||||||
|
import Node.Stream as Stream
|
||||||
|
import Node.Stream.CSV.Stringify as CSV.Stringify
|
||||||
|
import Node.Stream.CSV.Parse as CSV.Parse
|
||||||
|
|
||||||
|
type MyCSVType1 = {a :: Int, b :: Int, bar :: String, baz :: Boolean}
|
||||||
|
type MyCSVType2 = {ab :: Int, bar :: String, baz :: Boolean}
|
||||||
|
|
||||||
|
atob :: MyCSVType1 -> MyCSVType2
|
||||||
|
atob {a, b, bar, baz} = {ab: a + b, bar, baz}
|
||||||
|
|
||||||
|
myCSV :: String
|
||||||
|
myCSV = "a,b,bar,baz\n1,2,\"hello, world!\",true\n3,3,,f"
|
||||||
|
|
||||||
|
main :: Effect Unit
|
||||||
|
main = launchAff_ do
|
||||||
|
records :: Array MyCSVType1 <- CSV.Parse.parse myCSV
|
||||||
|
-- "ab,bar,baz\n3,\"hello, world!\",true\n6,,false"
|
||||||
|
csvString <- CSV.Stringify.stringify (atob <$> records)
|
||||||
|
pure unit
|
||||||
|
```
|
27
bun/fmt.js
Normal file
27
bun/fmt.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/** @type {(parser: string, ps: string[]) => import("bun").Subprocess} */
|
||||||
|
const prettier = (parser, ps) =>
|
||||||
|
Bun.spawn(['bun', 'x', 'prettier', '--write', '--parser', parser, ...ps], {
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
})
|
||||||
|
|
||||||
|
const procs = [
|
||||||
|
prettier('babel', ['./src/**/*.js', './bun/**/*.js', './.prettierrc.cjs']),
|
||||||
|
prettier('json', ['./package.json', './jsconfig.json']),
|
||||||
|
Bun.spawn(
|
||||||
|
[
|
||||||
|
'bun',
|
||||||
|
'x',
|
||||||
|
'purs-tidy',
|
||||||
|
'format-in-place',
|
||||||
|
'src/**/*.purs',
|
||||||
|
'test/**/*.purs',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
await Promise.all(procs.map(p => p.exited))
|
32
bun/prepare.js
Normal file
32
bun/prepare.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { readFile, writeFile } from 'fs/promises'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
|
||||||
|
let ver = process.argv[2]
|
||||||
|
if (!ver) {
|
||||||
|
console.error(`tag required: bun bun/prepare.js v1.0.0`)
|
||||||
|
} else if (!/v\d+\.\d+\.\d+/.test(ver)) {
|
||||||
|
console.error(`invalid tag: ${ver}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
ver = (/\d+\.\d+\.\d+/.exec(ver) || [])[0] || ''
|
||||||
|
|
||||||
|
const pkg = await readFile('./package.json', 'utf8')
|
||||||
|
const pkgnew = pkg.replace(/"version": ".+"/, `"version": "v${ver}"`)
|
||||||
|
await writeFile('./package.json', pkgnew)
|
||||||
|
|
||||||
|
const spago = await readFile('./spago.yaml', 'utf8')
|
||||||
|
const spagonew = spago.replace(/version: .+/, `version: '${ver}'`)
|
||||||
|
await writeFile('./spago.yaml', spagonew)
|
||||||
|
|
||||||
|
const readme = await readFile('./README.md', 'utf8')
|
||||||
|
const readmenew = readme.replace(
|
||||||
|
/packages\/purescript-postgresql\/.+?\//g,
|
||||||
|
`/packages/purescript-postgresql/${ver}/`,
|
||||||
|
)
|
||||||
|
await writeFile('./README.md', readmenew)
|
||||||
|
|
||||||
|
execSync(`git add spago.yaml package.json README.md`)
|
||||||
|
execSync(`git commit -m 'chore: prepare v${ver}'`)
|
||||||
|
execSync(`git tag v${ver}`)
|
||||||
|
execSync(`git push --tags`)
|
||||||
|
execSync(`git push --mirror github-mirror`)
|
55
spago.lock
55
spago.lock
@ -3,33 +3,34 @@ workspace:
|
|||||||
csv-stream:
|
csv-stream:
|
||||||
path: ./
|
path: ./
|
||||||
dependencies:
|
dependencies:
|
||||||
- aff
|
- aff: ">=7.1.0 <8.0.0"
|
||||||
- arrays
|
- arrays: ">=7.3.0 <8.0.0"
|
||||||
- bifunctors
|
- bifunctors: ">=6.0.0 <7.0.0"
|
||||||
- datetime
|
- datetime: ">=6.1.0 <7.0.0"
|
||||||
- effect
|
- effect: ">=4.0.0 <5.0.0"
|
||||||
- either
|
- either: ">=6.1.0 <7.0.0"
|
||||||
- exceptions
|
- exceptions: ">=6.0.0 <7.0.0"
|
||||||
- foldable-traversable
|
- foldable-traversable: ">=6.0.0 <7.0.0"
|
||||||
- foreign
|
- foreign: ">=7.0.0 <8.0.0"
|
||||||
- foreign-object
|
- foreign-object: ">=4.1.0 <5.0.0"
|
||||||
- integers
|
- integers: ">=6.0.0 <7.0.0"
|
||||||
- lists
|
- lists: ">=7.0.0 <8.0.0"
|
||||||
- maybe
|
- maybe: ">=6.0.0 <7.0.0"
|
||||||
- newtype
|
- newtype: ">=5.0.0 <6.0.0"
|
||||||
- node-event-emitter
|
- node-buffer
|
||||||
- node-streams
|
- node-event-emitter: ">=3.0.0 <4.0.0"
|
||||||
- nullable
|
- node-streams: ">=9.0.0 <10.0.0"
|
||||||
- numbers
|
- nullable: ">=6.0.0 <7.0.0"
|
||||||
- precise-datetime
|
- numbers: ">=9.0.1 <10.0.0"
|
||||||
- prelude
|
- precise-datetime: ">=7.0.0 <8.0.0"
|
||||||
- record
|
- prelude: ">=6.0.1 <7.0.0"
|
||||||
- st
|
- record: ">=4.0.0 <5.0.0"
|
||||||
- strings
|
- st: ">=6.2.0 <7.0.0"
|
||||||
- tailrec
|
- strings: ">=6.0.1 <7.0.0"
|
||||||
- transformers
|
- tailrec: ">=6.1.0 <7.0.0"
|
||||||
- typelevel-prelude
|
- transformers: ">=6.0.0 <7.0.0"
|
||||||
- unsafe-coerce
|
- typelevel-prelude: ">=7.0.0 <8.0.0"
|
||||||
|
- unsafe-coerce: ">=6.0.0 <7.0.0"
|
||||||
test_dependencies:
|
test_dependencies:
|
||||||
- console
|
- console
|
||||||
build_plan:
|
build_plan:
|
||||||
|
@ -10,6 +10,7 @@ package:
|
|||||||
strict: true
|
strict: true
|
||||||
pedanticPackages: true
|
pedanticPackages: true
|
||||||
dependencies:
|
dependencies:
|
||||||
|
- node-buffer
|
||||||
- aff: ">=7.1.0 <8.0.0"
|
- aff: ">=7.1.0 <8.0.0"
|
||||||
- arrays: ">=7.3.0 <8.0.0"
|
- arrays: ">=7.3.0 <8.0.0"
|
||||||
- bifunctors: ">=6.0.0 <7.0.0"
|
- bifunctors: ">=6.0.0 <7.0.0"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
module Node.Stream.CSV.Readable where
|
module Node.Stream.CSV.Parse where
|
||||||
|
|
||||||
import Prelude
|
import Prelude
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ import Effect.Uncurried (mkEffectFn1)
|
|||||||
import Foreign (Foreign, unsafeToForeign)
|
import Foreign (Foreign, unsafeToForeign)
|
||||||
import Foreign.Object (Object)
|
import Foreign.Object (Object)
|
||||||
import Foreign.Object as Object
|
import Foreign.Object as Object
|
||||||
|
import Node.Encoding (Encoding(..))
|
||||||
import Node.EventEmitter (EventHandle(..))
|
import Node.EventEmitter (EventHandle(..))
|
||||||
import Node.EventEmitter as Event
|
import Node.EventEmitter as Event
|
||||||
import Node.EventEmitter.UtilTypes (EventHandle1)
|
import Node.EventEmitter.UtilTypes (EventHandle1)
|
||||||
@ -74,9 +75,18 @@ type Config r =
|
|||||||
| r
|
| r
|
||||||
)
|
)
|
||||||
|
|
||||||
make :: forall @r rl config missing extra. ReadCSVRecord r rl => Union config missing (Config extra) => { | config } -> Effect (CSVParser r ())
|
-- | Create a CSVParser
|
||||||
|
make :: forall @r rl @config @missing @extra. RowToList r rl => ReadCSVRecord r rl => Union config missing (Config extra) => { | config } -> Effect (CSVParser r ())
|
||||||
make = makeImpl <<< unsafeToForeign <<< Object.union (recordToForeign {columns: true, cast: false, cast_date: false}) <<< recordToForeign
|
make = makeImpl <<< unsafeToForeign <<< Object.union (recordToForeign {columns: true, cast: false, cast_date: false}) <<< recordToForeign
|
||||||
|
|
||||||
|
-- | Synchronously parse a CSV string
|
||||||
|
parse :: forall @r rl @config missing extra. RowToList r rl => ReadCSVRecord r rl => Union config missing (Config extra) => { | config } -> String -> Aff (Array { | r })
|
||||||
|
parse config csv = do
|
||||||
|
stream <- liftEffect $ make @r @config @missing @extra config
|
||||||
|
void $ liftEffect $ Stream.writeString stream UTF8 csv
|
||||||
|
liftEffect $ Stream.end stream
|
||||||
|
readAll stream
|
||||||
|
|
||||||
-- | Reads a parsed record from the stream.
|
-- | Reads a parsed record from the stream.
|
||||||
-- |
|
-- |
|
||||||
-- | Returns `Nothing` when either:
|
-- | Returns `Nothing` when either:
|
||||||
@ -118,4 +128,3 @@ foreign import readImpl :: forall r. Stream r -> Effect (Nullable (Array String)
|
|||||||
-- | FFI
|
-- | FFI
|
||||||
recordToForeign :: forall r. Record r -> Object Foreign
|
recordToForeign :: forall r. Record r -> Object Foreign
|
||||||
recordToForeign = unsafeCoerce
|
recordToForeign = unsafeCoerce
|
||||||
|
|
100
src/Node.Stream.CSV.Stringify.purs
Normal file
100
src/Node.Stream.CSV.Stringify.purs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
module Node.Stream.CSV.Stringify where
|
||||||
|
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
import Control.Monad.Rec.Class (whileJust)
|
||||||
|
import Control.Monad.ST.Global as ST
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
, group_columns_by_name :: 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
|
||||||
|
make :: forall @r rl @config @missing @extra. RowToList r rl => WriteCSVRecord r rl => Union config missing (Config extra) => { | config } -> Effect (CSVStringifier r ())
|
||||||
|
make = makeImpl <<< unsafeToForeign <<< Object.union (recordToForeign {columns: true, cast: false, cast_date: false}) <<< recordToForeign
|
||||||
|
|
||||||
|
-- | Synchronously stringify a collection of records
|
||||||
|
stringify :: forall @r rl f @config missing extra. Foldable f => RowToList r rl => WriteCSVRecord r rl => Union config missing (Config extra) => { | config } -> f { | r } -> Aff String
|
||||||
|
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
|
||||||
|
|
||||||
|
-- | 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
|
||||||
|
|
||||||
|
whileJust do
|
||||||
|
isReadable <- liftEffect $ Stream.readable stream
|
||||||
|
when (not isReadable) $ makeAff \res -> mempty <* flip (Event.on Stream.readableH) stream $ res $ Right unit
|
||||||
|
|
||||||
|
liftEffect $ whileJust do
|
||||||
|
s <- (join <<< map blush) <$> Stream.readEither stream
|
||||||
|
for_ s \s' -> ST.toEffect $ Array.ST.push s' chunks
|
||||||
|
pure $ void s
|
||||||
|
|
||||||
|
isClosed <- liftEffect $ Stream.closed stream
|
||||||
|
pure $ if isClosed then Nothing else Just unit
|
||||||
|
|
||||||
|
chunks' <- liftEffect $ ST.toEffect $ Array.ST.unsafeFreeze chunks
|
||||||
|
pure $ fold chunks'
|
@ -1,60 +0,0 @@
|
|||||||
module Node.Stream.CSV.Writable where
|
|
||||||
|
|
||||||
import Prelude
|
|
||||||
|
|
||||||
import Data.CSV.Record (class WriteCSVRecord, writeCSVRecord)
|
|
||||||
import Data.String.Regex (Regex)
|
|
||||||
import Effect (Effect)
|
|
||||||
import Foreign (Foreign, unsafeToForeign)
|
|
||||||
import Foreign.Object (Object)
|
|
||||||
import Foreign.Object as Object
|
|
||||||
import Node.Stream (Read, Stream, Write)
|
|
||||||
import Prim.Row (class Union)
|
|
||||||
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
|
|
||||||
, group_columns_by_name :: 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
|
|
||||||
make :: forall @r rl config missing extra. WriteCSVRecord r rl => Union config missing (Config extra) => { | config } -> Effect (CSVStringifier r ())
|
|
||||||
make = makeImpl <<< unsafeToForeign <<< Object.union (recordToForeign {columns: true, cast: false, cast_date: false}) <<< recordToForeign
|
|
||||||
|
|
||||||
-- | 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. WriteCSVRecord r rl => CSVStringifier r a -> { | r } -> Effect Unit
|
|
||||||
write s = writeImpl s <<< writeCSVRecord @r @rl
|
|
Loading…
Reference in New Issue
Block a user