diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..7374779 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,20 @@ +{ + "bitwise": true, + "eqeqeq": true, + "forin": true, + "freeze": true, + "funcscope": true, + "futurehostile": true, + "globalstrict": true, + "latedef": true, + "maxparams": 1, + "noarg": true, + "nocomma": true, + "nonew": true, + "notypeof": true, + "singleGroups": true, + "undef": true, + "unused": true, + "eqnull": true +} + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..15f5237 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js +sudo: false +node_js: + - 4.2 + - 5.2 +env: + - PATH=$HOME/purescript:$PATH +install: + - TAG=$(wget -q -O - https://github.com/purescript/purescript/releases/latest --server-response --max-redirect 0 2>&1 | sed -n -e 's/.*Location:.*tag\///p') + - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$TAG/linux64.tar.gz + - tar -xvf $HOME/purescript.tar.gz -C $HOME/ + - chmod a+x $HOME/purescript + - npm install +script: + - npm run build diff --git a/README.md b/README.md index f283f22..fb598b4 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,4 @@ A wrapper for Node's Stream API - [Module Documentation](docs/) -- [Example](test/Main.purs) +- [Example](example/Gzip.purs) diff --git a/bower.json b/bower.json index 4bdf5c9..9f30525 100644 --- a/bower.json +++ b/bower.json @@ -14,11 +14,14 @@ "url": "git://github.com/purescript-node/purescript-node-streams.git" }, "devDependencies": { - "purescript-console": "^0.1.0" + "purescript-console": "^0.1.0", + "purescript-assert": "~0.1.1" }, "dependencies": { "purescript-eff": "~0.1.1", "purescript-node-buffer": "~0.2.0", - "purescript-prelude": "~0.1.2" + "purescript-prelude": "~0.1.2", + "purescript-either": "~0.2.3", + "purescript-exceptions": "~0.3.3" } } diff --git a/docs/Node/Stream.md b/docs/Node/Stream.md index 5c040d4..d153d61 100644 --- a/docs/Node/Stream.md +++ b/docs/Node/Stream.md @@ -5,7 +5,7 @@ This module provides a low-level wrapper for the Node Stream API. #### `Stream` ``` purescript -data Stream :: # * -> # ! -> * -> * +data Stream :: # * -> # ! -> * ``` A stream. @@ -14,7 +14,6 @@ The type arguments track, in order: - Whether reading and/or writing from/to the stream are allowed. - Effects associated with reading/writing from/to this stream. -- The type of chunks which will be read from/written to this stream (`String` or `Buffer`). #### `Read` @@ -56,26 +55,52 @@ type Duplex = Stream (read :: Read, write :: Write) A duplex (readable _and_ writable stream) -#### `setEncoding` +#### `onData` ``` purescript -setEncoding :: forall w eff. Readable w eff String -> Encoding -> Eff eff Unit +onData :: forall w eff. Readable w (err :: EXCEPTION | eff) -> (Buffer -> Eff (err :: EXCEPTION | eff) Unit) -> Eff (err :: EXCEPTION | eff) Unit ``` -Set the encoding used to read chunks from the stream. +Listen for `data` events, returning data in a Buffer. Note that this will fail +if `setEncoding` has been called on the stream. -#### `onData` +#### `onDataString` + +``` purescript +onDataString :: forall w eff. Readable w (err :: EXCEPTION | eff) -> Encoding -> (String -> Eff (err :: EXCEPTION | eff) Unit) -> Eff (err :: EXCEPTION | eff) Unit +``` + +Listen for `data` events, returning data in a String, which will be +decoded using the given encoding. Note that this will fail if `setEncoding` +has been called on the stream. + +#### `onDataEither` ``` purescript -onData :: forall w eff a. Readable w eff a -> (a -> Eff eff Unit) -> Eff eff Unit +onDataEither :: forall w eff. Readable w eff -> (Either String Buffer -> Eff eff Unit) -> Eff eff Unit ``` -Listen for `data` events. +Listen for `data` events, returning data in an `Either String Buffer`. This +function is provided for the (hopefully rare) case that `setEncoding` has +been called on the stream. + +#### `setEncoding` + +``` purescript +setEncoding :: forall w eff. Readable w eff -> Encoding -> Eff eff Unit +``` + +Set the encoding used to read chunks as strings from the stream. This +function may be useful when you are passing a readable stream to some other +JavaScript library, which already expects an encoding to be set. + +Where possible, you should try to use `onDataString` instead of this +function. #### `onEnd` ``` purescript -onEnd :: forall w eff a. Readable w eff a -> Eff eff Unit -> Eff eff Unit +onEnd :: forall w eff. Readable w eff -> Eff eff Unit -> Eff eff Unit ``` Listen for `end` events. @@ -83,7 +108,7 @@ Listen for `end` events. #### `onClose` ``` purescript -onClose :: forall w eff a. Readable w eff a -> Eff eff Unit -> Eff eff Unit +onClose :: forall w eff. Readable w eff -> Eff eff Unit -> Eff eff Unit ``` Listen for `close` events. @@ -91,7 +116,7 @@ Listen for `close` events. #### `onError` ``` purescript -onError :: forall w eff a. Readable w eff a -> Eff eff Unit -> Eff eff Unit +onError :: forall w eff. Readable w eff -> Eff eff Unit -> Eff eff Unit ``` Listen for `error` events. @@ -99,7 +124,7 @@ Listen for `error` events. #### `resume` ``` purescript -resume :: forall w eff a. Readable w eff a -> Eff eff Unit +resume :: forall w eff. Readable w eff -> Eff eff Unit ``` Resume reading from the stream. @@ -107,7 +132,7 @@ Resume reading from the stream. #### `pause` ``` purescript -pause :: forall w eff a. Readable w eff a -> Eff eff Unit +pause :: forall w eff. Readable w eff -> Eff eff Unit ``` Pause reading from the stream. @@ -115,7 +140,7 @@ Pause reading from the stream. #### `isPaused` ``` purescript -isPaused :: forall w eff a. Readable w eff a -> Eff eff Boolean +isPaused :: forall w eff. Readable w eff -> Eff eff Boolean ``` Check whether or not a stream is paused for reading. @@ -123,7 +148,7 @@ Check whether or not a stream is paused for reading. #### `pipe` ``` purescript -pipe :: forall r w eff a. Readable w eff a -> Writable r eff a -> Eff eff (Writable r eff a) +pipe :: forall r w eff. Readable w eff -> Writable r eff -> Eff eff (Writable r eff) ``` Read chunks from a readable stream and write them to a writable stream. @@ -131,15 +156,15 @@ Read chunks from a readable stream and write them to a writable stream. #### `write` ``` purescript -write :: forall r eff a. Writable r eff String -> a -> Eff eff Unit -> Eff eff Boolean +write :: forall r eff. Writable r eff -> Buffer -> Eff eff Unit -> Eff eff Boolean ``` -Write a chunk to a writable stream. +Write a Buffer to a writable stream. #### `writeString` ``` purescript -writeString :: forall r eff. Writable r eff String -> Encoding -> String -> Eff eff Unit -> Eff eff Boolean +writeString :: forall r eff. Writable r eff -> Encoding -> String -> Eff eff Unit -> Eff eff Boolean ``` Write a string in the specified encoding to a writable stream. @@ -147,7 +172,7 @@ Write a string in the specified encoding to a writable stream. #### `cork` ``` purescript -cork :: forall r eff a. Writable r eff a -> Eff eff Unit +cork :: forall r eff. Writable r eff -> Eff eff Unit ``` Force buffering of writes. @@ -155,7 +180,7 @@ Force buffering of writes. #### `uncork` ``` purescript -uncork :: forall r eff a. Writable r eff a -> Eff eff Unit +uncork :: forall r eff. Writable r eff -> Eff eff Unit ``` Flush buffered data. @@ -163,15 +188,19 @@ Flush buffered data. #### `setDefaultEncoding` ``` purescript -setDefaultEncoding :: forall r eff. Writable r eff String -> Encoding -> Eff eff Unit +setDefaultEncoding :: forall r eff. Writable r eff -> Encoding -> Eff eff Unit ``` -Set the default encoding used to write chunks to the stream. +Set the default encoding used to write strings to the stream. This function +is useful when you are passing a writable stream to some other JavaScript +library, which already expects a default encoding to be set. It has no +effect on the behaviour of the `writeString` function (because that +function ensures that the encoding is always supplied explicitly). #### `end` ``` purescript -end :: forall r eff a. Writable r eff a -> Eff eff Unit -> Eff eff Unit +end :: forall r eff. Writable r eff -> Eff eff Unit -> Eff eff Unit ``` End writing data to the stream. diff --git a/example/Gzip.js b/example/Gzip.js new file mode 100644 index 0000000..150d693 --- /dev/null +++ b/example/Gzip.js @@ -0,0 +1,9 @@ +/* global exports */ +/* global require */ +"use strict"; + +// module Gzip + +exports.gzip = require('zlib').createGzip; +exports.stdout = process.stdout; +exports.stdin = process.stdin; diff --git a/example/Gzip.purs b/example/Gzip.purs new file mode 100644 index 0000000..d521e47 --- /dev/null +++ b/example/Gzip.purs @@ -0,0 +1,19 @@ +module Gzip where + +import Prelude + +import Node.Stream + +import Control.Monad.Eff +import Control.Monad.Eff.Console + +foreign import data GZIP :: ! + +foreign import gzip :: forall eff. Eff (gzip :: GZIP | eff) (Duplex (gzip :: GZIP | eff)) +foreign import stdin :: forall eff. Readable () (console :: CONSOLE | eff) +foreign import stdout :: forall eff. Writable () (console :: CONSOLE | eff) + +main = do + z <- gzip + stdin `pipe` z + z `pipe` stdout diff --git a/package.json b/package.json new file mode 100644 index 0000000..ef6976b --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "scripts": { + "postinstall": "pulp dep install", + "build": "jshint src && pulp build && rimraf docs && pulp docs" + }, + "devDependencies": { + "jshint": "^2.8.0", + "pulp": "^4.0.2", + "rimraf": "^2.4.1", + "stream-buffers": "^3.0.0" + } +} diff --git a/src/Node/Stream.js b/src/Node/Stream.js index 2e47321..55de1a1 100644 --- a/src/Node/Stream.js +++ b/src/Node/Stream.js @@ -1,3 +1,5 @@ +/* global exports */ +/* global Buffer */ "use strict"; // module Node.Stream @@ -10,12 +12,27 @@ exports.setEncodingImpl = function(s) { }; }; -exports.onData = function(s) { - return function(f) { - return function() { - s.on('data', function(chunk) { - f(chunk)(); - }); +exports.onDataEitherImpl = function(left){ + return function(right){ + return function(s) { + return function(f) { + return function() { + s.on('data', function(chunk) { + if (chunk instanceof Buffer) { + f(right(chunk))(); + } + else if (typeof chunk === "string") { + f(left(chunk))(); + } + else { + throw new Error( + "Node.Stream.onDataEitherImpl: Unrecognised" + + "chunk type; expected String or Buffer, got:" + + chunk); + } + }); + }; + }; }; }; }; diff --git a/src/Node/Stream.purs b/src/Node/Stream.purs index bdc5fc3..0c37173 100644 --- a/src/Node/Stream.purs +++ b/src/Node/Stream.purs @@ -7,8 +7,10 @@ module Node.Stream , Write() , Writable() , Duplex() - , setEncoding , onData + , onDataString + , onDataEither + , setEncoding , onEnd , onClose , onError @@ -26,9 +28,15 @@ module Node.Stream import Prelude +import Control.Bind ((<=<)) +import Data.Either (Either(..)) import Node.Encoding +import Node.Buffer (Buffer()) +import Node.Buffer as Buffer import Control.Monad.Eff +import Control.Monad.Eff.Exception (throw, EXCEPTION()) +import Control.Monad.Eff.Unsafe (unsafeInterleaveEff) -- | A stream. -- | @@ -36,8 +44,7 @@ import Control.Monad.Eff -- | -- | - Whether reading and/or writing from/to the stream are allowed. -- | - Effects associated with reading/writing from/to this stream. --- | - The type of chunks which will be read from/written to this stream (`String` or `Buffer`). -foreign import data Stream :: # * -> # ! -> * -> * +foreign import data Stream :: # * -> # ! -> * -- | A phantom type associated with _readable streams_. data Read @@ -54,56 +61,90 @@ type Writable r = Stream (write :: Write | r) -- | A duplex (readable _and_ writable stream) type Duplex = Stream (read :: Read, write :: Write) -foreign import setEncodingImpl :: forall w eff. Readable w eff String -> String -> Eff eff Unit - --- | Set the encoding used to read chunks from the stream. -setEncoding :: forall w eff. Readable w eff String -> Encoding -> Eff eff Unit +-- | Listen for `data` events, returning data in a Buffer. Note that this will fail +-- | if `setEncoding` has been called on the stream. +onData :: forall w eff. Readable w (err :: EXCEPTION | eff) -> (Buffer -> Eff (err :: EXCEPTION | eff) Unit) -> Eff (err :: EXCEPTION | eff) Unit +onData r cb = + onDataEither r (cb <=< fromEither) + where + fromEither x = + case x of + Left _ -> + throw "Node.Stream.onData: Stream encoding should not be set" + Right buf -> + pure buf + +-- | Listen for `data` events, returning data in a String, which will be +-- | decoded using the given encoding. Note that this will fail if `setEncoding` +-- | has been called on the stream. +onDataString :: forall w eff. Readable w (err :: EXCEPTION | eff) -> Encoding -> (String -> Eff (err :: EXCEPTION | eff) Unit) -> Eff (err :: EXCEPTION | eff) Unit +onDataString r enc cb = onData r (cb <=< unsafeInterleaveEff <<< Buffer.toString enc) + +foreign import onDataEitherImpl :: forall w eff. (forall l r. l -> Either l r) -> (forall l r. r -> Either l r) -> Readable w eff -> (Either String Buffer -> Eff eff Unit) -> Eff eff Unit + +-- | Listen for `data` events, returning data in an `Either String Buffer`. This +-- | function is provided for the (hopefully rare) case that `setEncoding` has +-- | been called on the stream. +onDataEither :: forall w eff. Readable w eff -> (Either String Buffer -> Eff eff Unit) -> Eff eff Unit +onDataEither = onDataEitherImpl Left Right + +foreign import setEncodingImpl :: forall w eff. Readable w eff -> String -> Eff eff Unit + +-- | Set the encoding used to read chunks as strings from the stream. This +-- | function may be useful when you are passing a readable stream to some other +-- | JavaScript library, which already expects an encoding to be set. +-- | +-- | Where possible, you should try to use `onDataString` instead of this +-- | function. +setEncoding :: forall w eff. Readable w eff -> Encoding -> Eff eff Unit setEncoding r enc = setEncodingImpl r (show enc) --- | Listen for `data` events. -foreign import onData :: forall w eff a. Readable w eff a -> (a -> Eff eff Unit) -> Eff eff Unit - -- | Listen for `end` events. -foreign import onEnd :: forall w eff a. Readable w eff a -> Eff eff Unit -> Eff eff Unit +foreign import onEnd :: forall w eff. Readable w eff -> Eff eff Unit -> Eff eff Unit -- | Listen for `close` events. -foreign import onClose :: forall w eff a. Readable w eff a -> Eff eff Unit -> Eff eff Unit +foreign import onClose :: forall w eff. Readable w eff -> Eff eff Unit -> Eff eff Unit -- | Listen for `error` events. -foreign import onError :: forall w eff a. Readable w eff a -> Eff eff Unit -> Eff eff Unit +foreign import onError :: forall w eff. Readable w eff -> Eff eff Unit -> Eff eff Unit -- | Resume reading from the stream. -foreign import resume :: forall w eff a. Readable w eff a -> Eff eff Unit +foreign import resume :: forall w eff. Readable w eff -> Eff eff Unit -- | Pause reading from the stream. -foreign import pause :: forall w eff a. Readable w eff a -> Eff eff Unit +foreign import pause :: forall w eff. Readable w eff -> Eff eff Unit -- | Check whether or not a stream is paused for reading. -foreign import isPaused :: forall w eff a. Readable w eff a -> Eff eff Boolean +foreign import isPaused :: forall w eff. Readable w eff -> Eff eff Boolean -- | Read chunks from a readable stream and write them to a writable stream. -foreign import pipe :: forall r w eff a. Readable w eff a -> Writable r eff a -> Eff eff (Writable r eff a) +foreign import pipe :: forall r w eff. Readable w eff -> Writable r eff -> Eff eff (Writable r eff) --- | Write a chunk to a writable stream. -foreign import write :: forall r eff a. Writable r eff String -> a -> Eff eff Unit -> Eff eff Boolean +-- | Write a Buffer to a writable stream. +foreign import write :: forall r eff. Writable r eff -> Buffer -> Eff eff Unit -> Eff eff Boolean -foreign import writeStringImpl :: forall r eff. Writable r eff String -> String -> String -> Eff eff Unit -> Eff eff Boolean +foreign import writeStringImpl :: forall r eff. Writable r eff -> String -> String -> Eff eff Unit -> Eff eff Boolean -- | Write a string in the specified encoding to a writable stream. -writeString :: forall r eff. Writable r eff String -> Encoding -> String -> Eff eff Unit -> Eff eff Boolean +writeString :: forall r eff. Writable r eff -> Encoding -> String -> Eff eff Unit -> Eff eff Boolean writeString w enc = writeStringImpl w (show enc) -- | Force buffering of writes. -foreign import cork :: forall r eff a. Writable r eff a -> Eff eff Unit +foreign import cork :: forall r eff. Writable r eff -> Eff eff Unit -- | Flush buffered data. -foreign import uncork :: forall r eff a. Writable r eff a -> Eff eff Unit +foreign import uncork :: forall r eff. Writable r eff -> Eff eff Unit -foreign import setDefaultEncodingImpl :: forall r eff. Writable r eff String -> String -> Eff eff Unit +foreign import setDefaultEncodingImpl :: forall r eff. Writable r eff -> String -> Eff eff Unit --- | Set the default encoding used to write chunks to the stream. -setDefaultEncoding :: forall r eff. Writable r eff String -> Encoding -> Eff eff Unit +-- | Set the default encoding used to write strings to the stream. This function +-- | is useful when you are passing a writable stream to some other JavaScript +-- | library, which already expects a default encoding to be set. It has no +-- | effect on the behaviour of the `writeString` function (because that +-- | function ensures that the encoding is always supplied explicitly). +setDefaultEncoding :: forall r eff. Writable r eff -> Encoding -> Eff eff Unit setDefaultEncoding r enc = setDefaultEncodingImpl r (show enc) -- | End writing data to the stream. -foreign import end :: forall r eff a. Writable r eff a -> Eff eff Unit -> Eff eff Unit +foreign import end :: forall r eff. Writable r eff -> Eff eff Unit -> Eff eff Unit + diff --git a/test/Main.js b/test/Main.js index 6e98398..9a94009 100644 --- a/test/Main.js +++ b/test/Main.js @@ -2,8 +2,37 @@ // module Test.Main -exports.stdin = process.stdin; -exports.stdout = process.stdout; +exports.writableStreamBuffer = function() { + var W = require('stream-buffers').WritableStreamBuffer; + return new W; +}; -exports.gzip = require('zlib').createGzip; \ No newline at end of file +exports.getContentsAsString = function(w) { + return function() { + return w.getContentsAsString('utf8'); + }; +}; + +exports.readableStreamBuffer = function() { + var R = require('stream-buffers').ReadableStreamBuffer; + return new R; +}; + +exports.putImpl = function(str) { + return function(enc) { + return function(r) { + return function() { + r.put(str, enc); + }; + }; + }; +}; + +exports.createGzip = require('zlib').createGzip; +exports.createGunzip = require('zlib').createGunzip; + +exports.passThrough = function () { + var s = require('stream'); + return new s.PassThrough(); +}; diff --git a/test/Main.purs b/test/Main.purs index 1bd59ee..9b3c95e 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -2,20 +2,103 @@ module Test.Main where import Prelude +import Data.Either (Either(..)) +import Node.Buffer as Buffer +import Node.Encoding import Node.Stream +import Node.Stream.StdIO import Control.Monad.Eff import Control.Monad.Eff.Console -foreign import data GZIP :: ! +import Test.Assert + +assertEqual :: forall e a. (Show a, Eq a) => a -> a -> Eff (assert :: ASSERT | e) Unit +assertEqual x y = + assert' (show x <> " did not equal " <> show y) (x == y) + +foreign import data STREAM_BUFFER :: ! -foreign import stdin :: forall eff. Readable () (console :: CONSOLE | eff) String +foreign import writableStreamBuffer :: forall eff. Eff (sb :: STREAM_BUFFER | eff) (Writable () (sb :: STREAM_BUFFER | eff)) -foreign import stdout :: forall eff. Writable () (console :: CONSOLE | eff) String +foreign import getContentsAsString :: forall r eff. Writable r (sb :: STREAM_BUFFER | eff) -> Eff (sb :: STREAM_BUFFER | eff) String -foreign import gzip :: forall eff a. Eff (gzip :: GZIP | eff) (Duplex (gzip :: GZIP | eff) a) +foreign import readableStreamBuffer :: forall eff. Eff (sb :: STREAM_BUFFER | eff) (Readable () (sb :: STREAM_BUFFER | eff)) + +foreign import putImpl :: forall r eff. String -> String -> Readable r (sb :: STREAM_BUFFER | eff) -> Eff (sb :: STREAM_BUFFER | eff) Unit + +put :: forall r eff. String -> Encoding -> Readable r (sb :: STREAM_BUFFER | eff) -> Eff (sb :: STREAM_BUFFER | eff) Unit +put str enc = putImpl str (show enc) main = do - z <- gzip - stdin `pipe` z - z `pipe` stdout + log "setDefaultEncoding should not affect writing" + testSetDefaultEncoding + + log "setEncoding should not affect reading" + testSetEncoding + + log "test pipe" + testPipe + +testString :: String +testString = "üöß💡" + +testSetDefaultEncoding = do + w1 <- writableStreamBuffer + check w1 + + w2 <- writableStreamBuffer + setDefaultEncoding w2 UCS2 + check w2 + + where + check w = do + writeString w UTF8 testString do + c <- getContentsAsString w + assertEqual testString c + +testSetEncoding = do + check UTF8 + check UTF16LE + check UCS2 + where + check enc = do + r1 <- readableStreamBuffer + put testString enc r1 + + r2 <- readableStreamBuffer + put testString enc r2 + setEncoding r2 enc + + onData r1 \buf -> do + onDataEither r2 \(Left str) -> do + assertEqual <$> Buffer.toString enc buf <*> pure testString + assertEqual str testString + +testPipe = do + sIn <- passThrough + sOut <- passThrough + zip <- createGzip + unzip <- createGunzip + + log "pipe 1" + sIn `pipe` zip + log "pipe 2" + zip `pipe` unzip + log "pipe 3" + unzip `pipe` sOut + + writeString sIn UTF8 testString do + end sIn do + onDataString sOut UTF8 \str -> do + assertEqual str testString + +foreign import data GZIP :: ! + +foreign import createGzip :: forall eff. Eff (gzip :: GZIP | eff) (Duplex (gzip :: GZIP | eff)) +foreign import createGunzip :: forall eff. Eff (gzip :: GZIP | eff) (Duplex (gzip :: GZIP | eff)) + +foreign import data PASS_THROUGH :: ! + +-- | Create a PassThrough stream, which simply writes its input to its output. +foreign import passThrough :: forall eff. Eff (stream :: PASS_THROUGH | eff) (Duplex (stream :: PASS_THROUGH | eff))