commit 8c578b42bd994fc0ff986ce2dfa65b54020edb68 Author: Orion Kindel Date: Tue Apr 30 13:39:51 2024 -0500 feat: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c470536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ + +bower_components/ +node_modules/ +.pulp-cache/ +output/ +output-es/ +generated-docs/ +.psc-package/ +.psc* +.purs* +.psa* +.spago diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..2cab1c4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +purescript 0.15.16-1 diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..8cae2be Binary files /dev/null and b/bun.lockb differ diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..f48b93c --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "types": ["bun-types"], + "lib": ["esnext"], + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "jsx": "react", + "allowJs": true, + "checkJs": true, + "noEmit": true, + "strict": true + }, + "include": ["src/**/*.js", "bun/**/*.js"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0420d88 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "purescript-csv-stream", + "dependencies": { + "csv-parse": "^5.5.5", + "csv-stringify": "^6.4.6" + }, + "devDependencies": { + "typescript": "^5.4.5" + } +} diff --git a/spago.lock b/spago.lock new file mode 100644 index 0000000..3b9f93a --- /dev/null +++ b/spago.lock @@ -0,0 +1,760 @@ +workspace: + packages: + csv-stream: + path: ./ + dependencies: + - aff + - arrays + - bifunctors + - datetime + - effect + - either + - exceptions + - foldable-traversable + - foreign + - foreign-object + - integers + - lists + - maybe + - newtype + - node-event-emitter + - node-streams + - nullable + - numbers + - precise-datetime + - prelude + - record + - st + - strings + - tailrec + - transformers + - typelevel-prelude + - unsafe-coerce + test_dependencies: + - console + build_plan: + - aff + - arraybuffer-types + - arrays + - bifunctors + - console + - const + - contravariant + - control + - datetime + - decimals + - distributive + - effect + - either + - enums + - exceptions + - exists + - fixed-points + - foldable-traversable + - foreign + - foreign-object + - formatters + - functions + - functors + - gen + - identity + - integers + - invariant + - js-date + - lazy + - lists + - maybe + - newtype + - node-buffer + - node-event-emitter + - node-streams + - nonempty + - now + - nullable + - numbers + - ordered-collections + - orders + - parallel + - parsing + - partial + - precise-datetime + - prelude + - profunctor + - record + - refs + - safe-coerce + - st + - strings + - tailrec + - transformers + - tuples + - type-equality + - typelevel-prelude + - unfoldable + - unicode + - unsafe-coerce + extra_packages: {} +packages: + aff: + type: registry + version: 7.1.0 + integrity: sha256-7hOC6uQO9XBAI5FD8F33ChLjFAiZVfd4BJMqlMh7TNU= + dependencies: + - arrays + - bifunctors + - control + - datetime + - effect + - either + - exceptions + - foldable-traversable + - functions + - maybe + - newtype + - parallel + - prelude + - refs + - tailrec + - transformers + - unsafe-coerce + arraybuffer-types: + type: registry + version: 3.0.2 + integrity: sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA= + dependencies: [] + arrays: + type: registry + version: 7.3.0 + integrity: sha256-tmcklBlc/muUtUfr9RapdCPwnlQeB3aSrC4dK85gQlc= + dependencies: + - bifunctors + - control + - foldable-traversable + - functions + - maybe + - nonempty + - partial + - prelude + - safe-coerce + - st + - tailrec + - tuples + - unfoldable + - unsafe-coerce + bifunctors: + type: registry + version: 6.0.0 + integrity: sha256-/gZwC9YhNxZNQpnHa5BIYerCGM2jeX9ukZiEvYxm5Nw= + dependencies: + - const + - either + - newtype + - prelude + - tuples + console: + type: registry + version: 6.1.0 + integrity: sha256-CxmAzjgyuGDmt9FZW51VhV6rBPwR6o0YeKUzA9rSzcM= + dependencies: + - effect + - prelude + const: + type: registry + version: 6.0.0 + integrity: sha256-tNrxDW8D8H4jdHE2HiPzpLy08zkzJMmGHdRqt5BQuTc= + dependencies: + - invariant + - newtype + - prelude + contravariant: + type: registry + version: 6.0.0 + integrity: sha256-TP+ooAp3vvmdjfQsQJSichF5B4BPDHp3wAJoWchip6c= + dependencies: + - const + - either + - newtype + - prelude + - tuples + control: + type: registry + version: 6.0.0 + integrity: sha256-sH7Pg9E96JCPF9PIA6oQ8+BjTyO/BH1ZuE/bOcyj4Jk= + dependencies: + - newtype + - prelude + datetime: + type: registry + version: 6.1.0 + integrity: sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI= + dependencies: + - bifunctors + - control + - either + - enums + - foldable-traversable + - functions + - gen + - integers + - lists + - maybe + - newtype + - numbers + - ordered-collections + - partial + - prelude + - tuples + decimals: + type: registry + version: 7.1.0 + integrity: sha256-DriR6lPEfFpjVv7e4JAQkr3ZLf0h17Qg2cAIrhxWV7w= + dependencies: + - maybe + distributive: + type: registry + version: 6.0.0 + integrity: sha256-HTDdmEnzigMl+02SJB88j+gAXDx9VKsbvR4MJGDPbOQ= + dependencies: + - identity + - newtype + - prelude + - tuples + - type-equality + effect: + type: registry + version: 4.0.0 + integrity: sha256-eBtZu+HZcMa5HilvI6kaDyVX3ji8p0W9MGKy2K4T6+M= + dependencies: + - prelude + either: + type: registry + version: 6.1.0 + integrity: sha256-6hgTPisnMWVwQivOu2PKYcH8uqjEOOqDyaDQVUchTpY= + dependencies: + - control + - invariant + - maybe + - prelude + enums: + type: registry + version: 6.0.1 + integrity: sha256-HWaD73JFLorc4A6trKIRUeDMdzE+GpkJaEOM1nTNkC8= + dependencies: + - control + - either + - gen + - maybe + - newtype + - nonempty + - partial + - prelude + - tuples + - unfoldable + exceptions: + type: registry + version: 6.0.0 + integrity: sha256-y/xTAEIZIARCE+50/u1di0ncebJ+CIwNOLswyOWzMTw= + dependencies: + - effect + - either + - maybe + - prelude + exists: + type: registry + version: 6.0.0 + integrity: sha256-A0JQHpTfo1dNOj9U5/Fd3xndlRSE0g2IQWOGor2yXn8= + dependencies: + - unsafe-coerce + fixed-points: + type: registry + version: 7.0.0 + integrity: sha256-hTl5fzeG4mzAOFzEzAeNH7kJvJgYCH7x3v2NdX9pOE4= + dependencies: + - exists + - newtype + - prelude + - transformers + foldable-traversable: + type: registry + version: 6.0.0 + integrity: sha256-fLeqRYM4jUrZD5H4WqcwUgzU7XfYkzO4zhgtNc3jcWM= + dependencies: + - bifunctors + - const + - control + - either + - functors + - identity + - maybe + - newtype + - orders + - prelude + - tuples + foreign: + type: registry + version: 7.0.0 + integrity: sha256-1ORiqoS3HW+qfwSZAppHPWy4/6AQysxZ2t29jcdUMNA= + dependencies: + - either + - functions + - identity + - integers + - lists + - maybe + - prelude + - strings + - transformers + foreign-object: + type: registry + version: 4.1.0 + integrity: sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo= + dependencies: + - arrays + - foldable-traversable + - functions + - gen + - lists + - maybe + - prelude + - st + - tailrec + - tuples + - typelevel-prelude + - unfoldable + formatters: + type: registry + version: 7.0.0 + integrity: sha256-5JaC9d2p0xoqJWjWxlHH19R4iJwFTBr4j7SlYcLgicE= + dependencies: + - datetime + - fixed-points + - lists + - numbers + - parsing + - prelude + - transformers + functions: + type: registry + version: 6.0.0 + integrity: sha256-adMyJNEnhGde2unHHAP79gPtlNjNqzgLB8arEOn9hLI= + dependencies: + - prelude + functors: + type: registry + version: 5.0.0 + integrity: sha256-zfPWWYisbD84MqwpJSZFlvM6v86McM68ob8p9s27ywU= + dependencies: + - bifunctors + - const + - contravariant + - control + - distributive + - either + - invariant + - maybe + - newtype + - prelude + - profunctor + - tuples + - unsafe-coerce + gen: + type: registry + version: 4.0.0 + integrity: sha256-f7yzAXWwr+xnaqEOcvyO3ezKdoes8+WXWdXIHDBCAPI= + dependencies: + - either + - foldable-traversable + - identity + - maybe + - newtype + - nonempty + - prelude + - tailrec + - tuples + - unfoldable + identity: + type: registry + version: 6.0.0 + integrity: sha256-4wY0XZbAksjY6UAg99WkuKyJlQlWAfTi2ssadH0wVMY= + dependencies: + - control + - invariant + - newtype + - prelude + integers: + type: registry + version: 6.0.0 + integrity: sha256-sf+sK26R1hzwl3NhXR7WAu9zCDjQnfoXwcyGoseX158= + dependencies: + - maybe + - numbers + - prelude + invariant: + type: registry + version: 6.0.0 + integrity: sha256-RGWWyYrz0Hs1KjPDA+87Kia67ZFBhfJ5lMGOMCEFoLo= + dependencies: + - control + - prelude + js-date: + type: registry + version: 8.0.0 + integrity: sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo= + dependencies: + - datetime + - effect + - exceptions + - foreign + - integers + - now + lazy: + type: registry + version: 6.0.0 + integrity: sha256-lMsfFOnlqfe4KzRRiW8ot5ge6HtcU3Eyh2XkXcP5IgU= + dependencies: + - control + - foldable-traversable + - invariant + - prelude + lists: + type: registry + version: 7.0.0 + integrity: sha256-EKF15qYqucuXP2lT/xPxhqy58f0FFT6KHdIB/yBOayI= + dependencies: + - bifunctors + - control + - foldable-traversable + - lazy + - maybe + - newtype + - nonempty + - partial + - prelude + - tailrec + - tuples + - unfoldable + maybe: + type: registry + version: 6.0.0 + integrity: sha256-5cCIb0wPwbat2PRkQhUeZO0jcAmf8jCt2qE0wbC3v2Q= + dependencies: + - control + - invariant + - newtype + - prelude + newtype: + type: registry + version: 5.0.0 + integrity: sha256-gdrQu8oGe9eZE6L3wOI8ql/igOg+zEGB5ITh2g+uttw= + dependencies: + - prelude + - safe-coerce + node-buffer: + type: registry + version: 9.0.0 + integrity: sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4= + dependencies: + - arraybuffer-types + - effect + - maybe + - nullable + - st + - unsafe-coerce + node-event-emitter: + type: registry + version: 3.0.0 + integrity: sha256-Qw0MjsT4xRH2j2i4K8JmRjcMKnH5z1Cw39t00q4LE4w= + dependencies: + - effect + - either + - functions + - maybe + - nullable + - prelude + - unsafe-coerce + node-streams: + type: registry + version: 9.0.0 + integrity: sha256-2n6dq7YWleTDmD1Kur/ul7Cn08IvWrScgPf+0PgX2TQ= + dependencies: + - aff + - effect + - either + - exceptions + - node-buffer + - node-event-emitter + - nullable + - prelude + nonempty: + type: registry + version: 7.0.0 + integrity: sha256-54ablJZUHGvvlTJzi3oXyPCuvY6zsrWJuH/dMJ/MFLs= + dependencies: + - control + - foldable-traversable + - maybe + - prelude + - tuples + - unfoldable + now: + type: registry + version: 6.0.0 + integrity: sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY= + dependencies: + - datetime + - effect + nullable: + type: registry + version: 6.0.0 + integrity: sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o= + dependencies: + - effect + - functions + - maybe + numbers: + type: registry + version: 9.0.1 + integrity: sha256-/9M6aeMDBdB4cwYDeJvLFprAHZ49EbtKQLIJsneXLIk= + dependencies: + - functions + - maybe + ordered-collections: + type: registry + version: 3.2.0 + integrity: sha256-o9jqsj5rpJmMdoe/zyufWHFjYYFTTsJpgcuCnqCO6PM= + dependencies: + - arrays + - foldable-traversable + - gen + - lists + - maybe + - partial + - prelude + - st + - tailrec + - tuples + - unfoldable + orders: + type: registry + version: 6.0.0 + integrity: sha256-nBA0g3/ai0euH8q9pSbGqk53W2q6agm/dECZTHcoink= + dependencies: + - newtype + - prelude + parallel: + type: registry + version: 6.0.0 + integrity: sha256-VJbkGD0rAKX+NUEeBJbYJ78bEKaZbgow+QwQEfPB6ko= + dependencies: + - control + - effect + - either + - foldable-traversable + - functors + - maybe + - newtype + - prelude + - profunctor + - refs + - transformers + parsing: + type: registry + version: 10.2.0 + integrity: sha256-ZDIdMFAKkst57x6BVa1aUWJnS8smoZnXsZ339Aq1mPA= + dependencies: + - arrays + - control + - effect + - either + - enums + - foldable-traversable + - functions + - identity + - integers + - lazy + - lists + - maybe + - newtype + - nullable + - numbers + - partial + - prelude + - st + - strings + - tailrec + - transformers + - tuples + - unfoldable + - unicode + - unsafe-coerce + partial: + type: registry + version: 4.0.0 + integrity: sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4= + dependencies: [] + precise-datetime: + type: registry + version: 7.0.0 + integrity: sha256-F7tzZ7++Ihtg3xjumzwaHQvGQg61UtEAe5MWeOlTzRY= + dependencies: + - arrays + - datetime + - decimals + - either + - enums + - foldable-traversable + - formatters + - integers + - js-date + - lists + - maybe + - newtype + - numbers + - prelude + - strings + - tuples + - unicode + prelude: + type: registry + version: 6.0.1 + integrity: sha256-o8p6SLYmVPqzXZhQFd2hGAWEwBoXl1swxLG/scpJ0V0= + dependencies: [] + profunctor: + type: registry + version: 6.0.1 + integrity: sha256-E58hSYdJvF2Qjf9dnWLPlJKh2Z2fLfFLkQoYi16vsFk= + dependencies: + - control + - distributive + - either + - exists + - invariant + - newtype + - prelude + - tuples + record: + type: registry + version: 4.0.0 + integrity: sha256-Za5U85bTRJEfGK5Sk4hM41oXy84YQI0I8TL3WUn1Qzg= + dependencies: + - functions + - prelude + - unsafe-coerce + refs: + type: registry + version: 6.0.0 + integrity: sha256-Vgwne7jIbD3ZMoLNNETLT8Litw6lIYo3MfYNdtYWj9s= + dependencies: + - effect + - prelude + safe-coerce: + type: registry + version: 2.0.0 + integrity: sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU= + dependencies: + - unsafe-coerce + st: + type: registry + version: 6.2.0 + integrity: sha256-z9X0WsOUlPwNx9GlCC+YccCyz8MejC8Wb0C4+9fiBRY= + dependencies: + - partial + - prelude + - tailrec + - unsafe-coerce + strings: + type: registry + version: 6.0.1 + integrity: sha256-WssD3DbX4OPzxSdjvRMX0yvc9+pS7n5gyPv5I2Trb7k= + dependencies: + - arrays + - control + - either + - enums + - foldable-traversable + - gen + - integers + - maybe + - newtype + - nonempty + - partial + - prelude + - tailrec + - tuples + - unfoldable + - unsafe-coerce + tailrec: + type: registry + version: 6.1.0 + integrity: sha256-Xx19ECVDRrDWpz9D2GxQHHV89vd61dnXxQm0IcYQHGk= + dependencies: + - bifunctors + - effect + - either + - identity + - maybe + - partial + - prelude + - refs + transformers: + type: registry + version: 6.0.0 + integrity: sha256-Pzw40HjthX77tdPAYzjx43LK3X5Bb7ZspYAp27wksFA= + dependencies: + - control + - distributive + - effect + - either + - exceptions + - foldable-traversable + - identity + - lazy + - maybe + - newtype + - prelude + - tailrec + - tuples + - unfoldable + tuples: + type: registry + version: 7.0.0 + integrity: sha256-1rXgTomes9105BjgXqIw0FL6Fz1lqqUTLWOumhWec1M= + dependencies: + - control + - invariant + - prelude + type-equality: + type: registry + version: 4.0.1 + integrity: sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw= + dependencies: [] + typelevel-prelude: + type: registry + version: 7.0.0 + integrity: sha256-uFF2ph+vHcQpfPuPf2a3ukJDFmLhApmkpTMviHIWgJM= + dependencies: + - prelude + - type-equality + unfoldable: + type: registry + version: 6.0.0 + integrity: sha256-JtikvJdktRap7vr/K4ITlxUX1QexpnqBq0G/InLr6eg= + dependencies: + - foldable-traversable + - maybe + - partial + - prelude + - tuples + unicode: + type: registry + version: 6.0.0 + integrity: sha256-QJnTVZpmihEAUiMeYrfkusVoziJWp2hJsgi9bMQLue8= + dependencies: + - foldable-traversable + - maybe + - strings + unsafe-coerce: + type: registry + version: 6.0.0 + integrity: sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0= + dependencies: [] diff --git a/spago.yaml b/spago.yaml new file mode 100644 index 0000000..dddf13e --- /dev/null +++ b/spago.yaml @@ -0,0 +1,45 @@ +package: + name: csv-stream + publish: + version: '1.0.0' + license: 'GPL-3.0-or-later' + location: + githubOwner: 'cakekindel' + githubRepo: 'purescript-csv-stream' + build: + strict: true + pedanticPackages: true + dependencies: + - aff: ">=7.1.0 <8.0.0" + - arrays: ">=7.3.0 <8.0.0" + - bifunctors: ">=6.0.0 <7.0.0" + - datetime: ">=6.1.0 <7.0.0" + - effect: ">=4.0.0 <5.0.0" + - either: ">=6.1.0 <7.0.0" + - exceptions: ">=6.0.0 <7.0.0" + - foldable-traversable: ">=6.0.0 <7.0.0" + - foreign: ">=7.0.0 <8.0.0" + - foreign-object: ">=4.1.0 <5.0.0" + - integers: ">=6.0.0 <7.0.0" + - lists: ">=7.0.0 <8.0.0" + - maybe: ">=6.0.0 <7.0.0" + - newtype: ">=5.0.0 <6.0.0" + - node-event-emitter: ">=3.0.0 <4.0.0" + - node-streams: ">=9.0.0 <10.0.0" + - nullable: ">=6.0.0 <7.0.0" + - numbers: ">=9.0.1 <10.0.0" + - precise-datetime: ">=7.0.0 <8.0.0" + - prelude: ">=6.0.1 <7.0.0" + - record: ">=4.0.0 <5.0.0" + - st: ">=6.2.0 <7.0.0" + - strings: ">=6.0.1 <7.0.0" + - tailrec: ">=6.1.0 <7.0.0" + - transformers: ">=6.0.0 <7.0.0" + - typelevel-prelude: ">=7.0.0 <8.0.0" + - unsafe-coerce: ">=6.0.0 <7.0.0" + test: + main: Test.Main + dependencies: + - console +workspace: + extraPackages: {} diff --git a/src/Data.CSV.Record.purs b/src/Data.CSV.Record.purs new file mode 100644 index 0000000..9c3101a --- /dev/null +++ b/src/Data.CSV.Record.purs @@ -0,0 +1,44 @@ +module Data.CSV.Record where + +import Prelude + +import Control.Monad.Error.Class (liftMaybe) +import Control.Monad.Except (Except) +import Data.Array as Array +import Data.CSV (class ReadCSV, class WriteCSV, readCSV, writeCSV) +import Data.List.NonEmpty (NonEmptyList) +import Data.Maybe (fromMaybe) +import Data.Symbol (class IsSymbol) +import Foreign (ForeignError(..)) +import Prim.Row (class Cons, class Lacks) +import Prim.RowList (class RowToList, Cons, Nil, RowList) +import Record as Record +import Type.Prelude (Proxy(..)) + +class WriteCSVRecord :: Row Type -> RowList Type -> Constraint +class RowToList r rl <= WriteCSVRecord r rl | rl -> r where + writeCSVRecord :: { | r } -> Array String + +instance (RowToList r (Cons k v tailrl), IsSymbol k, WriteCSV v, Lacks k tail, Cons k v tail r, WriteCSVRecord tail tailrl) => WriteCSVRecord r (Cons k v tailrl) where + writeCSVRecord r = let + val = writeCSV $ Record.get (Proxy @k) r + tail = writeCSVRecord @tail @tailrl $ Record.delete (Proxy @k) r + in + [val] <> tail + +instance WriteCSVRecord () Nil where + writeCSVRecord _ = [] + +class ReadCSVRecord :: Row Type -> RowList Type -> Constraint +class RowToList r rl <= ReadCSVRecord r rl | rl -> r where + readCSVRecord :: Array String -> Except (NonEmptyList ForeignError) { | r } + +instance (RowToList r (Cons k v tailrl), IsSymbol k, ReadCSV v, Lacks k tail, Cons k v tail r, ReadCSVRecord tail tailrl) => ReadCSVRecord r (Cons k v tailrl) where + readCSVRecord vals = do + valraw <- liftMaybe (pure $ ForeignError "unexpected end of record") $ Array.head vals + val <- readCSV @v valraw + tail <- readCSVRecord @tail @tailrl (fromMaybe [] $ Array.tail vals) + pure $ Record.insert (Proxy @k) val tail + +instance ReadCSVRecord () Nil where + readCSVRecord _ = pure {} diff --git a/src/Data.CSV.purs b/src/Data.CSV.purs new file mode 100644 index 0000000..baed5a2 --- /dev/null +++ b/src/Data.CSV.purs @@ -0,0 +1,73 @@ +module Data.CSV where + +import Prelude + +import Control.Monad.Error.Class (liftMaybe, throwError) +import Control.Monad.Except (Except) +import Data.DateTime (DateTime) +import Data.Int as Int +import Data.List.NonEmpty (NonEmptyList) +import Data.Maybe (Maybe(..), maybe) +import Data.Newtype (unwrap) +import Data.Number.Format (toString) as Number +import Data.PreciseDateTime (fromDateTime, fromRFC3339String, toDateTimeLossy, toRFC3339String) +import Data.RFC3339String (RFC3339String(..)) +import Data.String as String +import Foreign (ForeignError(..), readInt, readNumber, unsafeToForeign) + +class ReadCSV a where + readCSV :: String -> Except (NonEmptyList ForeignError) a + +class WriteCSV a where + writeCSV :: a -> String + +instance ReadCSV Int where + readCSV = readInt <<< unsafeToForeign + +instance ReadCSV Number where + readCSV = readNumber <<< unsafeToForeign + +instance ReadCSV String where + readCSV = pure + +instance ReadCSV DateTime where + readCSV s = map toDateTimeLossy $ liftMaybe (pure $ ForeignError $ "invalid ISO date string: " <> s) $ fromRFC3339String $ RFC3339String s + +instance ReadCSV Boolean where + readCSV s = + let + inner "t" = pure true + inner "true" = pure true + inner "yes" = pure true + inner "y" = pure true + inner "1" = pure true + inner "f" = pure false + inner "false" = pure false + inner "no" = pure false + inner "n" = pure false + inner "0" = pure false + inner _ = throwError $ pure $ ForeignError $ "invalid boolean value: " <> s + in + inner $ String.toLower s + +instance ReadCSV a => ReadCSV (Maybe a) where + readCSV "" = pure Nothing + readCSV s = Just <$> readCSV s + +instance WriteCSV Int where + writeCSV = Int.toStringAs Int.decimal + +instance WriteCSV Number where + writeCSV = Number.toString + +instance WriteCSV String where + writeCSV = identity + +instance WriteCSV DateTime where + writeCSV = unwrap <<< toRFC3339String <<< fromDateTime + +instance WriteCSV Boolean where + writeCSV = show + +instance WriteCSV a => WriteCSV (Maybe a) where + writeCSV = maybe "" writeCSV diff --git a/src/Node.Stream.CSV.Readable.js b/src/Node.Stream.CSV.Readable.js new file mode 100644 index 0000000..a9bd20c --- /dev/null +++ b/src/Node.Stream.CSV.Readable.js @@ -0,0 +1,7 @@ +import {parse} from 'csv-parse' + +/** @type {(s: import('csv-parse').Options) => () => import('csv-parse').Parser} */ +export const makeImpl = c => () => parse(c) + +/** @type {(s: import('stream').Duplex) => () => string[] | null} */ +export const readImpl = s => () => s.read(); diff --git a/src/Node.Stream.CSV.Readable.purs b/src/Node.Stream.CSV.Readable.purs new file mode 100644 index 0000000..f478ebd --- /dev/null +++ b/src/Node.Stream.CSV.Readable.purs @@ -0,0 +1,121 @@ +module Node.Stream.CSV.Readable where + +import Prelude + +import Control.Monad.Error.Class (liftEither) +import Control.Monad.Except (runExcept) +import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT) +import Control.Monad.Rec.Class (whileJust) +import Control.Monad.ST.Global as ST +import Data.Array.ST as Array.ST +import Data.Bifunctor (lmap) +import Data.CSV.Record (class ReadCSVRecord, readCSVRecord) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Data.Nullable (Nullable) +import Data.Nullable as Nullable +import Data.Traversable (for_) +import Effect (Effect) +import Effect.Aff (Aff, makeAff) +import Effect.Class (liftEffect) +import Effect.Exception (error) +import Effect.Uncurried (mkEffectFn1) +import Foreign (Foreign, unsafeToForeign) +import Foreign.Object (Object) +import Foreign.Object as Object +import Node.EventEmitter (EventHandle(..)) +import Node.EventEmitter as Event +import Node.EventEmitter.UtilTypes (EventHandle1) +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 CSVRead + +-- | Stream transforming chunks of a CSV file +-- | into parsed purescript objects. +-- | +-- | The CSV contents may be piped into this stream +-- | as Buffer or String encoded chunks. +-- | +-- | Records can be read with `read` when `Node.Stream.readable` +-- | is true. +type CSVParser :: Row Type -> Row Type -> Type +type CSVParser a r = Stream (read :: Read, write :: Write, csv :: CSVRead | r) + +-- | https://csv.js.org/parse/options/ +type Config r = + ( bom :: Boolean + , group_columns_by_name :: Boolean + , comment :: String + , comment_no_infix :: Boolean + , delimiter :: String + , encoding :: String + , escape :: String + , from :: Int + , from_line :: Int + , ignore_last_delimiters :: Boolean + , info :: Boolean + , max_record_size :: Int + , quote :: String + , raw :: Boolean + , record_delimiter :: String + , relax_column_count :: Boolean + , skip_empty_lines :: Boolean + , skip_records_with_empty_values :: Boolean + , skip_records_with_error :: Boolean + , to :: Int + , to_line :: Int + , trim :: Boolean + , ltrim :: Boolean + , rtrim :: Boolean + | r + ) + +make :: forall @r rl config missing extra. 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 + +-- | Reads a parsed record from the stream. +-- | +-- | Returns `Nothing` when either: +-- | - The internal buffer of parsed records has been exhausted, but there will be more (`Node.Stream.readable` and `Node.Stream.closed` are both `false`) +-- | - All records have been processed (`Node.Stream.closed` is `true`) +read :: forall @r rl a. RowToList r rl => ReadCSVRecord r rl => CSVParser r a -> Effect (Maybe { | r }) +read stream = runMaybeT do + raw :: Array String <- MaybeT $ Nullable.toMaybe <$> readImpl stream + liftEither $ lmap (error <<< show) $ runExcept $ readCSVRecord @r @rl raw + +-- | Collect all parsed records into an array +readAll :: forall @r rl a. RowToList r rl => ReadCSVRecord r rl => CSVParser r a -> Aff (Array { | r }) +readAll stream = do + records <- liftEffect $ ST.toEffect $ Array.ST.new + + whileJust do + isReadable <- liftEffect $ Stream.readable stream + when (not isReadable) $ makeAff \res -> mempty <* flip (Event.once Stream.readableH) stream $ res $ Right unit + liftEffect $ whileJust do + r <- read @r stream + for_ r \r' -> ST.toEffect $ Array.ST.push r' records + pure $ void r + + isClosed <- liftEffect $ Stream.closed stream + pure $ if isClosed then Nothing else Just unit + + liftEffect $ ST.toEffect $ Array.ST.unsafeFreeze records + +-- | `data` event. Emitted when a CSV record has been parsed. +dataH :: forall r a. EventHandle1 (CSVParser r a) { | r } +dataH = EventHandle "data" mkEffectFn1 + +-- | FFI +foreign import makeImpl :: forall r. Foreign -> Effect (Stream r) + +-- | FFI +foreign import readImpl :: forall r. Stream r -> Effect (Nullable (Array String)) + +-- | FFI +recordToForeign :: forall r. Record r -> Object Foreign +recordToForeign = unsafeCoerce + diff --git a/src/Node.Stream.CSV.Writable.js b/src/Node.Stream.CSV.Writable.js new file mode 100644 index 0000000..bf3a296 --- /dev/null +++ b/src/Node.Stream.CSV.Writable.js @@ -0,0 +1,7 @@ +import {stringify} from 'csv-stringify' + +/** @type {(c: import('csv-stringify').Options) => () => import('csv-stringify').Stringifier} */ +export const makeImpl = c => () => stringify(c) + +/** @type {(s: import('csv-stringify').Stringifier) => (vals: Array) => () => void} */ +export const writeImpl = s => vals => () => s.write(vals) diff --git a/src/Node.Stream.CSV.Writable.purs b/src/Node.Stream.CSV.Writable.purs new file mode 100644 index 0000000..c9d75b8 --- /dev/null +++ b/src/Node.Stream.CSV.Writable.purs @@ -0,0 +1,60 @@ +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 diff --git a/src/Node.Stream.CSV.purs b/src/Node.Stream.CSV.purs new file mode 100644 index 0000000..b2550bc --- /dev/null +++ b/src/Node.Stream.CSV.purs @@ -0,0 +1,2 @@ +module Node.Stream.CSV where + diff --git a/test/Test/Main.purs b/test/Test/Main.purs new file mode 100644 index 0000000..e616930 --- /dev/null +++ b/test/Test/Main.purs @@ -0,0 +1,12 @@ +module Test.Main where + +import Prelude + +import Effect (Effect) +import Effect.Class.Console (log) + +main :: Effect Unit +main = do + log "🍕" + log "You should add some tests." +