git.iko.soy/typeable/cassava-streams
on trunk help sign in
23 files changed+1046−23
since splitsnapshots
unifiedsplit
.envrc+10 .github/workflows/analysis.yml+22 .github/workflows/tests.yml+63 .gitignore+5−2 .gitmodules CHANGES.md+34 LICENSE+1−1 README.md+10 bin/tutorial.hs+29 cassava-streams.cabal+88−20 data/HLint.hs+3 default.nix+11 nix/sources.json+38 nix/sources.nix+134 shell.nix+2 src/System/IO/Streams/Csv.hs+60 src/System/IO/Streams/Csv/Decode.hs+166 src/System/IO/Streams/Csv/Encode.hs+91 src/System/IO/Streams/Csv/Tutorial.hs+140 test/header.csv+1 test/make-huge-csv.sh+30 test/simple.csv+3 test/test.hs+105
.envrcnew file+10
@@ -0,0 +1,10 @@
1+# -*- sh -*-
2+
3+# Don't let cabal leak state to other projects:
4+export CABAL_DIR=$(pwd)/.cabal
5+
6+# Load in the shell.nix file:
7+use nix
8+
9+# Reload if these files change:
10+watch_file nix/sources.json
.github/workflows/analysis.ymlnew file+22
@@ -0,0 +1,22 @@
1+name: Analysis
2+on:
3+ pull_request:
4+jobs:
5+ check:
6+ name: pre-merge-checks
7+ runs-on: ubuntu-latest
8+ steps:
9+ - name: Clone Repository
10+ uses: actions/checkout@v2
11+
12+ - name: Setup Haskell
13+ id: setup-haskell-cabal
14+ uses: actions/setup-haskell@v1.1.3
15+ with:
16+ ghc-version: 8.8
17+
18+ - name: Cabal Check
19+ run: cabal check
20+
21+ - name: Lint
22+ run: curl -sSL https://raw.github.com/ndmitchell/hlint/master/misc/run.sh | sh -s .
.github/workflows/tests.ymlnew file+63
@@ -0,0 +1,63 @@
1+name: CI
2+on:
3+ pull_request:
4+jobs:
5+ cabal:
6+ name: ${{ matrix.os }} / ghc-${{ matrix.ghc }}
7+ runs-on: ${{ matrix.os }}
8+ strategy:
9+ fail-fast: false
10+ matrix:
11+ os: [ubuntu-latest, macOS-latest, windows-latest]
12+ cabal: ["3.2"]
13+ ghc: ["8.6.5", "8.8.4", "8.10.2"]
14+ # Newer versions of GHC are broken on Windows:
15+ # https://gitlab.haskell.org/ghc/ghc/-/issues/17926
16+ exclude:
17+ - os: windows-latest
18+ ghc: "8.8.4"
19+ - os: windows-latest
20+ ghc: "8.10.2"
21+ steps:
22+ - name: Clone Repository
23+ uses: actions/checkout@v2
24+
25+ - name: Setup Haskell
26+ id: setup-haskell-cabal
27+ uses: actions/setup-haskell@v1.1.3
28+ with:
29+ ghc-version: ${{ matrix.ghc }}
30+ cabal-version: ${{ matrix.cabal }}
31+
32+ - name: Configure and Freeze for Cache
33+ run: |
34+ cabal configure --enable-tests --enable-benchmarks --test-show-details=direct -ftutorial
35+ cabal freeze
36+
37+ - name: Cache ~/.cabal/store
38+ uses: actions/cache@v1
39+ with:
40+ path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
41+ key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
42+
43+ - name: Build
44+ run: cabal build '--build-log=logs/$pkgid'
45+
46+ - name: Test
47+ run: cabal test
48+
49+ - name: Upload Build Logs on Failure
50+ uses: actions/upload-artifact@v2
51+ if: failure()
52+ with:
53+ name: logs-${{ matrix.os }}-ghc${{ matrix.ghc }}
54+ path: logs/
55+
56+ - name: Install
57+ run: cabal install --installdir=artifacts --install-method=copy
58+
59+ - name: Upload Artifacts
60+ uses: actions/upload-artifact@v2
61+ with:
62+ name: ${{ matrix.os }}-ghc${{ matrix.ghc }}
63+ path: artifacts/
.gitignore+5−2
@@ -1,3 +1,6 @@
1+/test/massive.csv
2+/.stack-work
3+/build/.stack-work
14/dist
2−/.cabal-sandbox
3−/cabal.sandbox.config
5+/result
6+/.cabal/
.gitmodulesnew file

No changes to the content.

CHANGES.mdnew file+34
@@ -0,0 +1,34 @@
1+# Change Log / Release Notes
2+
3+## 0.3 (August 11, 2016)
4+
5+ * Add some additional (convenience) exports from `Data.Csv`. (Thanks
6+ goes to Florian Hofmann.)
7+
8+ * Added `data/HLint.hs`
9+
10+ * Updated to LTS Haskell 6.7
11+
12+ * Minor updates:
13+
14+ - 0.3.0.1 (April 14, 2017): Updated dependencies
15+ - 0.3.0.2 (March 20, 2018): Updated dependencies
16+ - 0.3.0.3 (October 09, 2018): Updated dependencies
17+ - 0.3.0.4 (April 15, 2019): Updated dependencies
18+ - 0.3.0.5 (October 29, 2020): Update dependencies
19+
20+## 0.2.0.0 (February 8, 2016)
21+
22+ * Compile with GHC 7.10.3
23+
24+ * Update dependency versions
25+
26+ * Build with stack and LTS Haskell 5.2
27+
28+## 0.1.1.0 (November 11, 2014)
29+
30+ * `System/IO/Streams/Csv/Decode.hs` added more descriptive error messages.
31+
32+## 0.1.0.0 (April 30, 2014)
33+
34+ * cassava-streams initial release
LICENSE+1−1
@@ -1,4 +1,4 @@
1−Copyright (c) 2014 Peter Jones <pjones@devalot.com>
1+Copyright (c) 2014-2020 Peter Jones <pjones@devalot.com>
22
33All rights reserved.
44
README.md+10
@@ -0,0 +1,10 @@
1+# cassava-streams
2+
3+[![CI](https://github.com/pjones/cassava-streams/workflows/CI/badge.svg)](https://github.com/pjones/cassava-streams/actions)
4+[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/pjones/cassava-streams?label=release)](https://github.com/pjones/cassava-streams/releases)
5+[![Hackage](https://img.shields.io/hackage/v/cassava-streams)](https://hackage.haskell.org/package/cassava-streams)
6+
7+[io-streams][] interface for the [cassava][] CSV library.
8+
9+[io-streams]: http://hackage.haskell.org/package/io-streams
10+[cassava]: https://hackage.haskell.org/package/cassava
bin/tutorial.hsnew file+29
@@ -0,0 +1,29 @@
1+{-
2+
3+This file is part of the Haskell package cassava-streams. It is
4+subject to the license terms in the LICENSE file found in the
5+top-level directory of this distribution and at
6+git://pmade.com/cassava-streams/LICENSE. No part of cassava-streams
7+package, including this file, may be copied, modified, propagated, or
8+distributed except according to the terms contained in the LICENSE
9+file.
10+
11+-}
12+
13+--------------------------------------------------------------------------------
14+module Main (main) where
15+
16+--------------------------------------------------------------------------------
17+import System.Environment
18+import System.IO
19+import System.IO.Streams.Csv.Tutorial
20+
21+--------------------------------------------------------------------------------
22+main :: IO ()
23+main = do
24+ args <- getArgs
25+
26+ case args of
27+ ["todo"] -> onlyTodo stdin stdout
28+ ["done", x] -> markDone x stdin stdout
29+ _ -> fail "give one of todo or done"
cassava-streams.cabal+88−20
@@ -1,23 +1,91 @@
1−name: cassava-streams
2−version: 0.1.0.0
3−synopsis: io-streams interface for the cassava CSV library.
4−license: BSD3
5−license-file: LICENSE
6−author: Peter Jones <pjones@devalot.com>
7−maintainer: Peter Jones <pjones@devalot.com>
8−copyright: Copyright (c) 2013,2014 Peter Jones
9−category: Data, Text, CSV, IO-Streams
10−build-type: Simple
11−cabal-version: >=1.10
12−-- description:
13−-- extra-source-files:
1+cabal-version: 2.2
2+name: cassava-streams
3+version: 0.3.0.5
4+synopsis: io-streams interface for the cassava CSV library.
5+license: BSD-3-Clause
6+license-file: LICENSE
7+author: Peter Jones <pjones@devalot.com>
8+maintainer: Peter Jones <pjones@devalot.com>
9+copyright: Copyright (c) 2014-2020 Peter Jones
10+category: Data, Text, CSV, IO-Streams
11+build-type: Simple
12+homepage: https://github.com/pjones/cassava-streams
13+bug-reports: https://github.com/pjones/cassava-streams/issues
14+tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.2
15+description:
16+ The cassava-streams library glues togeter the cassava CSV library
17+ and the io-streams streaming library.
18+ .
19+ See the "System.IO.Streams.Csv.Tutorial" module for a simple tutorial.
1420
21+--------------------------------------------------------------------------------
22+extra-source-files:
23+ README.md
24+ CHANGES.md
25+ test/*.csv
26+
27+--------------------------------------------------------------------------------
28+source-repository head
29+ type: git
30+ location: https://github.com/pjones/cassava-streams.git
31+
32+--------------------------------------------------------------------------------
33+flag tutorial
34+ description: Build the tutorial binary (useful for profiling).
35+ manual: True
36+ default: False
37+
38+--------------------------------------------------------------------------------
39+common dependencies
40+ build-depends:
41+ , base >=4.8 && <5.0
42+ , bytestring >=0.10 && <0.12
43+ , cassava >=0.4 && <0.6
44+ , io-streams >=1.1 && <1.6
45+ , vector >=0.10 && <0.14
46+
47+--------------------------------------------------------------------------------
1548library
16− -- exposed-modules:
17− -- other-modules:
18− -- other-extensions:
19− hs-source-dirs: src
49+ import: dependencies
50+ exposed-modules:
51+ System.IO.Streams.Csv
52+ System.IO.Streams.Csv.Tutorial
53+
54+ other-modules:
55+ System.IO.Streams.Csv.Decode
56+ System.IO.Streams.Csv.Encode
57+
58+ hs-source-dirs: src
59+ default-language: Haskell2010
60+ ghc-options: -Wall -fwarn-incomplete-uni-patterns
61+ build-depends:
62+
63+--------------------------------------------------------------------------------
64+executable tutorial
65+ default-language: Haskell2010
66+ hs-source-dirs: bin
67+ main-is: tutorial.hs
68+ ghc-options: -Wall -fwarn-incomplete-uni-patterns -rtsopts
69+
70+ if !flag(tutorial)
71+ buildable: False
72+
73+ else
74+ build-depends:
75+ , base
76+ , cassava-streams
77+ , io-streams
78+
79+--------------------------------------------------------------------------------
80+test-suite test
81+ import: dependencies
82+ type: exitcode-stdio-1.0
2083 default-language: Haskell2010
21− build-depends: base >= 4.6 && < 5.0
22− , cassava >= 0.3 && < 0.4
23− , io-streams >= 1.1.0 && < 2.0
84+ hs-source-dirs: test
85+ main-is: test.hs
86+ ghc-options: -Wall -fwarn-incomplete-uni-patterns
87+ build-depends:
88+ , cassava-streams
89+ , QuickCheck >=2.7 && <3.0
90+ , tasty >=0.8 && <1.5
91+ , tasty-quickcheck >=0.8 && <1.3
data/HLint.hsnew file+3
@@ -0,0 +1,3 @@
1+module HLint.HLint where
2+import "hint" HLint.Builtin.All
3+ignore "Use import/export shortcut"
default.nixnew file+11
@@ -0,0 +1,11 @@
1+{ sources ? import ./nix/sources.nix
2+, pkgs ? import sources.nixpkgs { }
3+, nix-hs ? import sources.nix-hs { inherit pkgs; }
4+, ghc ? "default"
5+}:
6+
7+nix-hs {
8+ cabal = ./cassava-streams.cabal;
9+ flags = [ "tutorial" ];
10+ compiler = ghc;
11+}
nix/sources.jsonnew file+38
@@ -0,0 +1,38 @@
1+{
2+ "niv": {
3+ "branch": "master",
4+ "description": "Easy dependency management for Nix projects",
5+ "homepage": "https://github.com/nmattia/niv",
6+ "owner": "nmattia",
7+ "repo": "niv",
8+ "rev": "f73bf8d584148677b01859677a63191c31911eae",
9+ "sha256": "0jlmrx633jvqrqlyhlzpvdrnim128gc81q5psz2lpp2af8p8q9qs",
10+ "type": "tarball",
11+ "url": "https://github.com/nmattia/niv/archive/f73bf8d584148677b01859677a63191c31911eae.tar.gz",
12+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
13+ },
14+ "nix-hs": {
15+ "branch": "release-20.09",
16+ "description": "Haskell + nixpkgs = nix-hs",
17+ "homepage": "",
18+ "owner": "pjones",
19+ "repo": "nix-hs",
20+ "rev": "67163bb02a8791d79136ba6914b6d6dad594533b",
21+ "sha256": "0f7sjh45wxbf2m76l37f4l4v4dkq32dm599p85acx53324nr9y8z",
22+ "type": "tarball",
23+ "url": "https://github.com/pjones/nix-hs/archive/67163bb02a8791d79136ba6914b6d6dad594533b.tar.gz",
24+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
25+ },
26+ "nixpkgs": {
27+ "branch": "nixos-20.09",
28+ "description": "Nix Packages collection",
29+ "homepage": "",
30+ "owner": "NixOS",
31+ "repo": "nixpkgs",
32+ "rev": "edb26126d98bc696f4f3e206583faa65d3d6e818",
33+ "sha256": "1cl4ka4kk7kh3bl78g06dhiidazf65q8miyzaxi9930d6gwyzkci",
34+ "type": "tarball",
35+ "url": "https://github.com/NixOS/nixpkgs/archive/edb26126d98bc696f4f3e206583faa65d3d6e818.tar.gz",
36+ "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
37+ }
38+}
nix/sources.nixnew file+134
@@ -0,0 +1,134 @@
1+# This file has been generated by Niv.
2+
3+let
4+
5+ #
6+ # The fetchers. fetch_<type> fetches specs of type <type>.
7+ #
8+
9+ fetch_file = pkgs: spec:
10+ if spec.builtin or true then
11+ builtins_fetchurl { inherit (spec) url sha256; }
12+ else
13+ pkgs.fetchurl { inherit (spec) url sha256; };
14+
15+ fetch_tarball = pkgs: spec:
16+ if spec.builtin or true then
17+ builtins_fetchTarball { inherit (spec) url sha256; }
18+ else
19+ pkgs.fetchzip { inherit (spec) url sha256; };
20+
21+ fetch_git = spec:
22+ builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
23+
24+ fetch_builtin-tarball = spec:
25+ builtins.trace
26+ ''
27+ WARNING:
28+ The niv type "builtin-tarball" will soon be deprecated. You should
29+ instead use `builtin = true`.
30+
31+ $ niv modify <package> -a type=tarball -a builtin=true
32+ ''
33+ builtins_fetchTarball { inherit (spec) url sha256; };
34+
35+ fetch_builtin-url = spec:
36+ builtins.trace
37+ ''
38+ WARNING:
39+ The niv type "builtin-url" will soon be deprecated. You should
40+ instead use `builtin = true`.
41+
42+ $ niv modify <package> -a type=file -a builtin=true
43+ ''
44+ (builtins_fetchurl { inherit (spec) url sha256; });
45+
46+ #
47+ # Various helpers
48+ #
49+
50+ # The set of packages used when specs are fetched using non-builtins.
51+ mkPkgs = sources:
52+ let
53+ sourcesNixpkgs =
54+ import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {};
55+ hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
56+ hasThisAsNixpkgsPath = <nixpkgs> == ./.;
57+ in
58+ if builtins.hasAttr "nixpkgs" sources
59+ then sourcesNixpkgs
60+ else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
61+ import <nixpkgs> {}
62+ else
63+ abort
64+ ''
65+ Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
66+ add a package called "nixpkgs" to your sources.json.
67+ '';
68+
69+ # The actual fetching function.
70+ fetch = pkgs: name: spec:
71+
72+ if ! builtins.hasAttr "type" spec then
73+ abort "ERROR: niv spec ${name} does not have a 'type' attribute"
74+ else if spec.type == "file" then fetch_file pkgs spec
75+ else if spec.type == "tarball" then fetch_tarball pkgs spec
76+ else if spec.type == "git" then fetch_git spec
77+ else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec
78+ else if spec.type == "builtin-url" then fetch_builtin-url spec
79+ else
80+ abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
81+
82+ # Ports of functions for older nix versions
83+
84+ # a Nix version of mapAttrs if the built-in doesn't exist
85+ mapAttrs = builtins.mapAttrs or (
86+ f: set: with builtins;
87+ listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
88+ );
89+
90+ # fetchTarball version that is compatible between all the versions of Nix
91+ builtins_fetchTarball = { url, sha256 }@attrs:
92+ let
93+ inherit (builtins) lessThan nixVersion fetchTarball;
94+ in
95+ if lessThan nixVersion "1.12" then
96+ fetchTarball { inherit url; }
97+ else
98+ fetchTarball attrs;
99+
100+ # fetchurl version that is compatible between all the versions of Nix
101+ builtins_fetchurl = { url, sha256 }@attrs:
102+ let
103+ inherit (builtins) lessThan nixVersion fetchurl;
104+ in
105+ if lessThan nixVersion "1.12" then
106+ fetchurl { inherit url; }
107+ else
108+ fetchurl attrs;
109+
110+ # Create the final "sources" from the config
111+ mkSources = config:
112+ mapAttrs (
113+ name: spec:
114+ if builtins.hasAttr "outPath" spec
115+ then abort
116+ "The values in sources.json should not have an 'outPath' attribute"
117+ else
118+ spec // { outPath = fetch config.pkgs name spec; }
119+ ) config.sources;
120+
121+ # The "config" used by the fetchers
122+ mkConfig =
123+ { sourcesFile ? ./sources.json
124+ , sources ? builtins.fromJSON (builtins.readFile sourcesFile)
125+ , pkgs ? mkPkgs sources
126+ }: rec {
127+ # The sources, i.e. the attribute set of spec name to spec
128+ inherit sources;
129+
130+ # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
131+ inherit pkgs;
132+ };
133+in
134+mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }
shell.nixnew file+2
@@ -0,0 +1,2 @@
1+# Load an interactive environment:
2+(import ./. {}).interactive
src/System/IO/Streams/Csv.hsnew file+60
@@ -0,0 +1,60 @@
1+{-
2+
3+This file is part of the Haskell package cassava-streams. It is
4+subject to the license terms in the LICENSE file found in the
5+top-level directory of this distribution and at
6+git://pmade.com/cassava-streams/LICENSE. No part of cassava-streams
7+package, including this file, may be copied, modified, propagated, or
8+distributed except according to the terms contained in the LICENSE
9+file.
10+
11+-}
12+
13+--------------------------------------------------------------------------------
14+-- | This module exports functions which can be used to read instances
15+-- of the cassava classes @FromRecord@ and @FromNamedRecord@ from an
16+-- io-streams @InputStream ByteString@.
17+--
18+-- It also exports functions which can write instances of @ToRecord@
19+-- and @ToNamedRecord@ to an io-streams @OutputStream ByteString@.
20+--
21+-- See the "System.IO.Streams.Csv.Tutorial" module for a simple tutorial.
22+module System.IO.Streams.Csv
23+ ( -- * Decoding CSV
24+ -- | These functions convert an io-streams @InputStream
25+ -- ByteString@ stream into one that decodes CSV records and
26+ -- produces these decoded records.
27+ --
28+ -- Each of the decoding functions produce an @InputStream@
29+ -- which yields an @Either@ value. @Left String@ represents
30+ -- a record which failed type conversion. @Right a@ is a
31+ -- successfully decoded record.
32+ --
33+ -- See the tutorial in "System.IO.Streams.Csv.Tutorial" for
34+ -- details on how to use the 'onlyValidRecords' function to
35+ -- transform the decoding streams so that they only produce
36+ -- valid records and throw exceptions for bad records.
37+ module System.IO.Streams.Csv.Decode
38+
39+ -- * Encoding CSV
40+ -- | These functions convert an io-streams @OutputStream
41+ -- ByteString@ stream into one that encodes records into CSV
42+ -- format before sending them downstream.
43+ , module System.IO.Streams.Csv.Encode
44+
45+ -- * Convenience Exports
46+ -- | Export data types from Data.Csv
47+ , module Data.Csv
48+ ) where
49+
50+--------------------------------------------------------------------------------
51+import System.IO.Streams.Csv.Decode
52+import System.IO.Streams.Csv.Encode
53+
54+import Data.Csv ( HasHeader(..)
55+ , defaultEncodeOptions
56+ , defaultDecodeOptions
57+ , DecodeOptions(..)
58+ , EncodeOptions(..)
59+ , Quoting(..)
60+ )
src/System/IO/Streams/Csv/Decode.hsnew file+166
@@ -0,0 +1,166 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+{-
5+
6+This file is part of the Haskell package cassava-streams. It is
7+subject to the license terms in the LICENSE file found in the
8+top-level directory of this distribution and at
9+git://pmade.com/cassava-streams/LICENSE. No part of cassava-streams
10+package, including this file, may be copied, modified, propagated, or
11+distributed except according to the terms contained in the LICENSE
12+file.
13+
14+-}
15+
16+--------------------------------------------------------------------------------
17+module System.IO.Streams.Csv.Decode
18+ ( StreamDecodingError (..)
19+ , decodeStream
20+ , decodeStreamWith
21+ , decodeStreamByName
22+ , decodeStreamByNameWith
23+ , onlyValidRecords
24+ ) where
25+
26+--------------------------------------------------------------------------------
27+import Control.Exception
28+import Data.ByteString (ByteString)
29+import qualified Data.ByteString as BS
30+import Data.Csv hiding (Parser, decodeWith, decodeByNameWith)
31+import Data.Csv.Incremental
32+import Data.IORef
33+import Data.Typeable
34+import System.IO.Streams (InputStream, makeInputStream)
35+import qualified System.IO.Streams as Streams
36+
37+--------------------------------------------------------------------------------
38+-- | Exception thrown when stream decoding cannot continue due to an
39+-- error.
40+newtype StreamDecodingError = StreamDecodingError String
41+ deriving (Typeable, Show)
42+
43+instance Exception StreamDecodingError
44+
45+--------------------------------------------------------------------------------
46+-- | Create an @InputStream@ which decodes CSV records from the given
47+-- upstream data source.
48+--
49+-- Equivalent to @decodeStreamWith defaultDecodeOptions@.
50+decodeStream :: (FromRecord a)
51+ => HasHeader
52+ -- ^ Whether to skip a header or not.
53+ -> InputStream ByteString
54+ -- ^ Upstream.
55+ -> IO (InputStream (Either String a))
56+ -- ^ An @InputStream@ which produces records.
57+decodeStream = decodeStreamWith defaultDecodeOptions
58+
59+--------------------------------------------------------------------------------
60+-- | Create an @InputStream@ which decodes CSV records from the given
61+-- upstream data source.
62+decodeStreamWith :: (FromRecord a)
63+ => DecodeOptions
64+ -- ^ CSV decoding options.
65+ -> HasHeader
66+ -- ^ Whether to skip a header or not.
67+ -> InputStream ByteString
68+ -- ^ Upstream.
69+ -> IO (InputStream (Either String a))
70+ -- ^ An @InputStream@ which produces records.
71+decodeStreamWith ops hdr input = do
72+ queue <- newIORef []
73+ parser <- newIORef $ Just (decodeWith ops hdr)
74+ makeInputStream (dispatch queue parser input)
75+
76+--------------------------------------------------------------------------------
77+-- | Create an @InputStream@ which decodes CSV records from the given
78+-- upstream data source. Data should be preceded by a header.
79+--
80+-- Equivalent to @decodeStreamByNameWith defaultDecodeOptions@.
81+decodeStreamByName :: (FromNamedRecord a)
82+ => InputStream ByteString
83+ -- ^ Upstream.
84+ -> IO (InputStream (Either String a))
85+ -- ^ An @InputStream@ which produces records.
86+decodeStreamByName = decodeStreamByNameWith defaultDecodeOptions
87+
88+--------------------------------------------------------------------------------
89+-- | Create an @InputStream@ which decodes CSV records from the given
90+-- upstream data source. Data should be preceded by a header.
91+decodeStreamByNameWith :: (FromNamedRecord a)
92+ => DecodeOptions
93+ -- ^ CSV decoding options.
94+ -> InputStream ByteString
95+ -- ^ Upstream.
96+ -> IO (InputStream (Either String a))
97+ -- ^ An @InputStream@ which produces records.
98+decodeStreamByNameWith ops input = go (decodeByNameWith ops) where
99+ -- Dispatch on the HeaderParser type.
100+ go (FailH _ e) = bomb e
101+ go (PartialH f) = Streams.read input >>= go . maybe (f BS.empty) f
102+ go (DoneH _ p) = do
103+ queue <- newIORef []
104+ parser <- newIORef (Just p)
105+ makeInputStream (dispatch queue parser input)
106+
107+--------------------------------------------------------------------------------
108+-- | Creates a new @InputStream@ which only sends valid CSV records
109+-- downstream. The first invalid record will throw an exception.
110+onlyValidRecords :: InputStream (Either String a)
111+ -- ^ Upstream.
112+ -> IO (InputStream a)
113+ -- ^ An @InputStream@ which only produces valid
114+ -- records.
115+onlyValidRecords input = makeInputStream $ do
116+ upstream <- Streams.read input
117+
118+ case upstream of
119+ Nothing -> return Nothing
120+ Just (Left err) -> bomb ("invalid CSV row: " ++ err)
121+ Just (Right x) -> return (Just x)
122+
123+--------------------------------------------------------------------------------
124+-- | Internal function which feeds data to the CSV parser.
125+dispatch :: forall a. IORef [Either String a]
126+ -- ^ List of queued CSV records.
127+ -> IORef (Maybe (Parser a))
128+ -- ^ Current CSV parser state.
129+ -> InputStream ByteString
130+ -- ^ Upstream.
131+ -> IO (Maybe (Either String a))
132+ -- ^ Data feed downstream.
133+dispatch queueRef parserRef input = do
134+ queue <- readIORef queueRef
135+
136+ case queue of
137+ [] -> do
138+ parser <- readIORef parserRef
139+ case parser of
140+ Nothing -> return Nothing
141+ Just (Fail _ e) -> bomb ("input data malformed: " ++ e)
142+ Just (Many [] f) -> more f
143+ Just (Many xs f) -> writeIORef parserRef (Just $ Many [] f) >> feed xs
144+ Just (Done xs ) -> writeIORef parserRef Nothing >> feed xs
145+
146+ (x:xs) -> do
147+ writeIORef queueRef xs
148+ return (Just x)
149+
150+ where
151+ -- Send more data to the CSV parser. If there is no more data
152+ -- from upstream then send an empty @ByteString@.
153+ more :: (ByteString -> Parser a) -> IO (Maybe (Either String a))
154+ more f = do bs <- Streams.read input
155+ writeIORef parserRef (Just $ maybe (f BS.empty) f bs)
156+ dispatch queueRef parserRef input
157+
158+ -- Feed records downstream.
159+ feed :: [Either String a] -> IO (Maybe (Either String a))
160+ feed xs = do writeIORef queueRef xs
161+ dispatch queueRef parserRef input
162+
163+--------------------------------------------------------------------------------
164+-- | Throw an exception.
165+bomb :: String -> IO a
166+bomb = throwIO . StreamDecodingError
src/System/IO/Streams/Csv/Encode.hsnew file+91
@@ -0,0 +1,91 @@
1+{-
2+
3+This file is part of the Haskell package cassava-streams. It is
4+subject to the license terms in the LICENSE file found in the
5+top-level directory of this distribution and at
6+git://pmade.com/cassava-streams/LICENSE. No part of cassava-streams
7+package, including this file, may be copied, modified, propagated, or
8+distributed except according to the terms contained in the LICENSE
9+file.
10+
11+-}
12+
13+--------------------------------------------------------------------------------
14+module System.IO.Streams.Csv.Encode
15+ ( encodeStream
16+ , encodeStreamWith
17+ , encodeStreamByName
18+ , encodeStreamByNameWith
19+ ) where
20+
21+--------------------------------------------------------------------------------
22+import Control.Monad (when)
23+import Data.ByteString (ByteString)
24+import qualified Data.ByteString.Lazy as BL
25+import Data.Csv
26+import Data.IORef
27+import System.IO.Streams (OutputStream, makeOutputStream)
28+import qualified System.IO.Streams as Streams
29+
30+--------------------------------------------------------------------------------
31+-- | Create a new @OutputStream@ that can be fed @ToRecord@ values
32+-- which are converted to CSV. The records are encoded into
33+-- @ByteString@s and passed on to the given downstream @OutputStream@.
34+--
35+-- Equivalent to @encodeStreamWith defaultEncodeOptions@.
36+encodeStream :: ToRecord a
37+ => OutputStream ByteString -- ^ Downstream.
38+ -> IO (OutputStream a) -- ^ New @OutputStream@.
39+encodeStream = encodeStreamWith defaultEncodeOptions
40+
41+--------------------------------------------------------------------------------
42+-- | Create a new @OutputStream@ that can be fed @ToRecord@ values
43+-- which are converted to CSV. The records are encoded into
44+-- @ByteString@s and passed on to the given downstream @OutputStream@.
45+encodeStreamWith :: ToRecord a
46+ => EncodeOptions -- ^ Encoding options.
47+ -> OutputStream ByteString -- ^ Downstream.
48+ -> IO (OutputStream a) -- ^ New @OutputStream@.
49+encodeStreamWith opts output = do
50+ ref <- newIORef opts
51+ makeOutputStream (dispatch encodeWith ref output)
52+
53+--------------------------------------------------------------------------------
54+-- | Create a new @OutputStream@ which can be fed @ToNamedRecord@
55+-- values that will be converted into CSV. The records are encoded
56+-- into @ByteString@s and passed on to the given downstream
57+-- @OutputStream@.
58+--
59+-- Equivalent to @encodeStreamByNameWith defaultEncodeOptions@.
60+encodeStreamByName :: ToNamedRecord a
61+ => Header -- ^ CSV Header.
62+ -> OutputStream ByteString -- ^ Downstream.
63+ -> IO (OutputStream a) -- ^ New @OutputStream@.
64+encodeStreamByName = encodeStreamByNameWith defaultEncodeOptions
65+
66+--------------------------------------------------------------------------------
67+-- | Create a new @OutputStream@ which can be fed @ToNamedRecord@
68+-- values that will be converted into CSV. The records are encoded
69+-- into @ByteString@s and passed on to the given downstream
70+-- @OutputStream@.
71+encodeStreamByNameWith :: ToNamedRecord a
72+ => EncodeOptions -- ^ Encoding options.
73+ -> Header -- ^ CSV Header.
74+ -> OutputStream ByteString -- ^ Downstream.
75+ -> IO (OutputStream a) -- ^ New @OutputStream@.
76+encodeStreamByNameWith opts hdr output = do
77+ ref <- newIORef opts
78+ makeOutputStream $ dispatch (`encodeByNameWith` hdr) ref output
79+
80+--------------------------------------------------------------------------------
81+-- | Encode records, ensuring that the header is written no more than once.
82+dispatch :: (EncodeOptions -> [a] -> BL.ByteString) -- ^ Encoding function.
83+ -> IORef EncodeOptions -- ^ Encoding options.
84+ -> OutputStream ByteString -- ^ Downstream.
85+ -> Maybe a -- ^ Record to write.
86+ -> IO ()
87+dispatch _ _ output Nothing = Streams.write Nothing output
88+dispatch enc ref output (Just x) = do
89+ opts <- readIORef ref
90+ when (encIncludeHeader opts) $ writeIORef ref (opts {encIncludeHeader = False})
91+ Streams.writeLazyByteString (enc opts [x]) output
src/System/IO/Streams/Csv/Tutorial.hsnew file+140
@@ -0,0 +1,140 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+{-
4+
5+This file is part of the Haskell package cassava-streams. It is
6+subject to the license terms in the LICENSE file found in the
7+top-level directory of this distribution and at
8+git://pmade.com/cassava-streams/LICENSE. No part of cassava-streams
9+package, including this file, may be copied, modified, propagated, or
10+distributed except according to the terms contained in the LICENSE
11+file.
12+
13+-}
14+
15+--------------------------------------------------------------------------------
16+-- | A simple tutorial on using the cassava-streams library to glue
17+-- together cassava and io-streams.
18+--
19+-- Note: if you're reading this on Hackage or in Haddock then you
20+-- should switch to source view with the \"Source\" link at the top of
21+-- this page or open this file in your favorite text editor.
22+module System.IO.Streams.Csv.Tutorial
23+ ( -- * Types representing to-do items and their state
24+ Item (..)
25+ , TState (..)
26+
27+ -- * Functions which use cassava-streams functions
28+ , onlyTodo
29+ , markDone
30+ ) where
31+
32+--------------------------------------------------------------------------------
33+import Control.Monad
34+import Data.Csv
35+import qualified Data.Vector as V
36+import System.IO
37+import qualified System.IO.Streams as Streams
38+import System.IO.Streams.Csv
39+
40+--------------------------------------------------------------------------------
41+-- | A to-do item.
42+data Item = Item
43+ { title :: String -- ^ Title.
44+ , state :: TState -- ^ State.
45+ , time :: Maybe Double -- ^ Seconds taken to complete.
46+ } deriving (Show, Eq)
47+
48+instance FromNamedRecord Item where
49+ parseNamedRecord m = Item <$> m .: "Title"
50+ <*> m .: "State"
51+ <*> m .: "Time"
52+
53+instance ToNamedRecord Item where
54+ toNamedRecord (Item t s tm) =
55+ namedRecord [ "Title" .= t
56+ , "State" .= s
57+ , "Time" .= tm
58+ ]
59+
60+--------------------------------------------------------------------------------
61+-- | Possible states for a to-do item.
62+data TState = Todo -- ^ Item needs to be completed.
63+ | Done -- ^ Item has been finished.
64+ deriving (Show, Eq)
65+
66+instance FromField TState where
67+ parseField "TODO" = return Todo
68+ parseField "DONE" = return Done
69+ parseField _ = mzero
70+
71+instance ToField TState where
72+ toField Todo = "TODO"
73+ toField Done = "DONE"
74+
75+--------------------------------------------------------------------------------
76+-- | The @onlyTodo@ function reads to-do 'Item's from the given input
77+-- handle (in CSV format) and writes them back to the output handle
78+-- (also in CSV format), but only if the items are in the @Todo@
79+-- state. In another words, the CSV data is filtered so that the
80+-- output handle only receives to-do 'Item's which haven't been
81+-- completed.
82+--
83+-- The io-streams @handleToInputStream@ function is used to create an
84+-- @InputStream ByteString@ stream from the given input handle.
85+--
86+-- That stream is then given to the cassava-streams function
87+-- 'decodeStreamByName' which converts the @InputStream ByteString@
88+-- stream into an @InputStream Item@ stream.
89+--
90+-- Notice that the cassava-streams function 'onlyValidRecords' is used
91+-- to transform the decoding stream into one that only produces valid
92+-- records. Any records which fail type conversion (via
93+-- @FromNamedRecord@ or @FromRecord@) will not escape from
94+-- 'onlyValidRecords' but instead will throw an exception.
95+--
96+-- Finally the io-streams @filter@ function is used to filter the
97+-- input stream so that it only produces to-do items which haven't
98+-- been completed.
99+onlyTodo :: Handle -- ^ Input handle where CSV data can be read.
100+ -> Handle -- ^ Output handle where CSV data can be written.
101+ -> IO ()
102+onlyTodo inH outH = do
103+ -- A stream which produces items which are not 'Done'.
104+ input <- Streams.handleToInputStream inH >>=
105+ decodeStreamByName >>= onlyValidRecords >>=
106+ Streams.filter (\item -> state item /= Done)
107+
108+ -- A stream to write items into. They will be converted to CSV.
109+ output <- Streams.handleToOutputStream outH >>=
110+ encodeStreamByName (V.fromList ["State", "Time", "Title"])
111+
112+ -- Connect the input and output streams.
113+ Streams.connect input output
114+
115+--------------------------------------------------------------------------------
116+-- | The @markDone@ function will read to-do items from the given
117+-- input handle and mark any matching items as @Done@. All to-do
118+-- items are written to the given output handle.
119+markDone :: String -- ^ Items with this title are marked as @Done@.
120+ -> Handle -- ^ Input handle where CSV data can be read.
121+ -> Handle -- ^ Output handle where CSV data can be written.
122+ -> IO ()
123+markDone titleOfItem inH outH = do
124+ -- Change matching items to the 'Done' state.
125+ let markDone' item = if title item == titleOfItem
126+ then item {state = Done}
127+ else item
128+
129+ -- A stream which produces items and converts matching items to the
130+ -- 'Done' state.
131+ input <- Streams.handleToInputStream inH >>=
132+ decodeStreamByName >>= onlyValidRecords >>=
133+ Streams.map markDone'
134+
135+ -- A stream to write items into. They will be converted to CSV.
136+ output <- Streams.handleToOutputStream outH >>=
137+ encodeStreamByName (V.fromList ["State", "Time", "Title"])
138+
139+ -- Connect the input and output streams.
140+ Streams.connect input output
test/header.csvnew file+1
@@ -0,0 +1 @@
1+State,Time,Title
test/make-huge-csv.shnew file+30
@@ -0,0 +1,30 @@
1+#!/bin/sh
2+
3+################################################################################
4+PROF_ROW_SIZE=512
5+PROF_ROW_COUNT=10000 # Actual rows will be this number * 100
6+
7+################################################################################
8+OUT_FILE=$1
9+CSV_HEADER=$2
10+
11+################################################################################
12+cp $CSV_HEADER $OUT_FILE
13+
14+################################################################################
15+for i in `seq $PROF_ROW_COUNT`; do
16+ title=`openssl rand -base64 $PROF_ROW_SIZE | paste -sd/`
17+ time=`echo|awk 'srand() {print rand()}'`
18+
19+ if [ `expr $i % 2` -eq 0 ]; then
20+ row="TODO,,$title"
21+ else
22+ row="TODO,$time,$title"
23+ fi
24+
25+ for j in `seq 100`; do echo "$row" >> $OUT_FILE; done
26+ printf .
27+done
28+
29+################################################################################
30+echo # Print blank line to terminate all those dots.
test/simple.csvnew file+3
@@ -0,0 +1,3 @@
1+State,Title
2+TODO,Make a bigger file
3+TODO,Release this package
test/test.hsnew file+105
@@ -0,0 +1,105 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+{-
4+
5+This file is part of the Haskell package cassava-streams. It is
6+subject to the license terms in the LICENSE file found in the
7+top-level directory of this distribution and at
8+git://pmade.com/cassava-streams/LICENSE. No part of cassava-streams
9+package, including this file, may be copied, modified, propagated, or
10+distributed except according to the terms contained in the LICENSE
11+file.
12+
13+-}
14+
15+--------------------------------------------------------------------------------
16+module Main (main) where
17+
18+--------------------------------------------------------------------------------
19+import Control.Monad
20+import Data.ByteString (ByteString)
21+import qualified Data.ByteString as BS
22+import Data.Csv hiding (Record, NamedRecord, record, header)
23+import qualified Data.Vector as V
24+import System.IO.Streams (InputStream, OutputStream)
25+import qualified System.IO.Streams as Streams
26+import qualified System.IO.Streams.Csv as CSV
27+import Test.QuickCheck.Monadic (monadicIO, run, assert)
28+import Test.Tasty
29+import Test.Tasty.QuickCheck as QC
30+
31+--------------------------------------------------------------------------------
32+-- | Fake record to encode and decode. This works well because
33+-- Cassava and QuickCheck already have the necessary instances for
34+-- triples.
35+type Record = (Int, String, String)
36+
37+--------------------------------------------------------------------------------
38+-- | But, Cassava doesn't have ToNamedRecord, FromNamedRecord
39+-- instances for triples so we have to work around there here.
40+newtype NamedRecord = NamedRecord {record :: Record}
41+
42+instance ToNamedRecord NamedRecord where
43+ toNamedRecord (NamedRecord (a, b, c)) =
44+ namedRecord ["a" .= a, "b" .= b, "c" .= c]
45+
46+instance FromNamedRecord NamedRecord where
47+ parseNamedRecord m = do
48+ a <- m .: "a"
49+ b <- m .: "b"
50+ c <- m .: "c"
51+ return $ NamedRecord (a, b, c)
52+
53+
54+--------------------------------------------------------------------------------
55+header :: Header
56+header = V.fromList ["a", "b", "c"]
57+
58+--------------------------------------------------------------------------------
59+-- | Given a list of records generated by QuickCheck, encode those
60+-- records into a CSV ByteString then decode them back into records.
61+roundTrip :: (InputStream ByteString -> IO (InputStream a)) -- ^ Decoder.
62+ -> (OutputStream ByteString -> IO (OutputStream a)) -- ^ Encoder.
63+ -> [a] -- ^ Records.
64+ -> IO [a]
65+roundTrip is os recs = do
66+ -- Encode records to a ByteString.
67+ sourceList <- Streams.fromList recs
68+ (collector, encoded) <- Streams.listOutputStream
69+ encoder <- os collector
70+ Streams.connect sourceList encoder
71+
72+ -- Decode from ByteString.
73+ decoder <- (encoded >>= Streams.fromByteString . BS.concat) >>= is
74+ (decodeStream, decoded) <- Streams.listOutputStream
75+ Streams.connect decoder decodeStream
76+ decoded
77+
78+--------------------------------------------------------------------------------
79+prop_namedRoundTrip :: [Record] -> Property
80+prop_namedRoundTrip recsIn = not (null recsIn) ==> monadicIO $ do
81+ recsOut <- run $ roundTrip is os (map NamedRecord recsIn)
82+ assert $ recsIn == map record recsOut
83+ where
84+ is = CSV.decodeStreamByName >=> CSV.onlyValidRecords
85+ os = CSV.encodeStreamByName header
86+
87+--------------------------------------------------------------------------------
88+prop_indexedRoundTrip :: [Record] -> Property
89+prop_indexedRoundTrip recsIn = not (null recsIn) ==> monadicIO $ do
90+ recsOut <- run $ roundTrip is os recsIn
91+ assert $ recsIn == recsOut
92+ where
93+ is = CSV.decodeStream NoHeader >=> CSV.onlyValidRecords
94+ os = CSV.encodeStream
95+
96+--------------------------------------------------------------------------------
97+tests :: TestTree
98+tests = testGroup "Tests"
99+ [ QC.testProperty "namedRoundTrip" prop_namedRoundTrip
100+ , QC.testProperty "indexedRoundTrip" prop_indexedRoundTrip
101+ ]
102+
103+--------------------------------------------------------------------------------
104+main :: IO ()
105+main = defaultMain tests