fix: rename modules, add readme

This commit is contained in:
orion 2024-04-30 14:09:12 -05:00
parent 8c578b42bd
commit 38e4e85859
Signed by: orion
GPG Key ID: 6D4165AE4C928719
10 changed files with 281 additions and 90 deletions

81
README.md Normal file
View 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
View 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
View 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`)

View File

@ -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:

View File

@ -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"

View File

@ -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

View 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'

View File

@ -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