├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── console.js ├── even └── fs.js ├── lib ├── channel.js ├── fake-perform.js ├── index.js ├── io.js ├── types.js ├── unsafe-perform.js └── wrap-function.js ├── node ├── child_process.js ├── console.js ├── fs.js ├── module.js └── process.js ├── package.json └── test ├── channel.js ├── fake-perform.js ├── fixtures └── test-module.js ├── helpers └── extract.js ├── node-child_process.js ├── node-fs.js ├── node-module.js ├── node-process.js ├── unsafe-perform.js └── wrap-function.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | max_line_length = 120 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | .nyc_output 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | yarn.lock 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6.0 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christoph Hermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # future-io 2 | [![NPM version](http://img.shields.io/npm/v/future-io.svg?style=flat-square)](https://www.npmjs.com/package/future-io) 3 | [![Build status](http://img.shields.io/travis/futurize/future-io/master.svg?style=flat-square)](https://travis-ci.org/futurize/future-io) 4 | [![Dependencies](https://img.shields.io/david/futurize/future-io.svg?style=flat-square)](https://david-dm.org/futurize/future-io) 5 | 6 | A [fantasy-land](https://github.com/fantasyland/fantasy-land) compliant monadic IO library for Node.js. 7 | 8 | Building a simple cli that tells you if a number is even can look something like this: 9 | 10 | ```js 11 | #!/usr/bin/node 12 | 13 | const io = require('future-io') 14 | const ioProcess = require('future-io/node/process') 15 | 16 | const even = ioProcess.argv 17 | .map(argv => (parseInt(argv[2]) % 2) === 0) 18 | .chain(even => ioProcess.stdout.write('Is even: ' + even)) 19 | 20 | io.unsafePerform(even) 21 | ``` 22 | 23 | # API 24 | 25 | ## IO-returning functions 26 | To get you started fast this library mimics the interface of the native node modules. 27 | It just returns io's instead of taking callbacks! 28 | 29 | Getting complete coverage of all io related functionality in node is still a work in progress. 30 | If you're missing something, please feel free to open an issue or pull request. 31 | 32 | At the moment the following modules are exported. 33 | For some recipes demonstrating their use check out the examples directory. 34 | 35 | ### `future-io/node/console` 36 | 37 | ### `future-io/node/fs` 38 | 39 | ### `future-io/node/module` 40 | 41 | ### `future-io/node/process` 42 | 43 | ### `future-io/node/child_process` 44 | 45 | ## performing IO actions 46 | 47 | ### `unsafePerform :: IO e a -> ()` 48 | This will execute the io. 49 | If the io represents an error value, this function will throw. 50 | If this is not what you want, use `IO.prototype.catch`. 51 | 52 | ## IO methods 53 | IO implements the [fantasy-land](https://github.com/fantasyland/fantasy-land) Functor, Apply and Monad specifications. 54 | 55 | ### `IO.of :: a -> IO e a` 56 | 57 | ### `IO.error :: e -> IO e a` 58 | 59 | ### `IO.prototype.map :: IO e a ~> (a -> b) -> IO e b` 60 | 61 | ### `IO.prototype.ap :: IO e (a -> b) ~> IO e a -> IO e b` 62 | 63 | ### `IO.prototype.chain :: IO e a ~> (a -> IO e b) -> IO e b` 64 | 65 | ### `IO.prototype.catch :: IO e a ~> (e -> IO f a) -> IO f a` 66 | 67 | ## Wrapping custom IO functions 68 | Often you'll find the need to define your own io returning functions, or wrap functions provided by a library. 69 | Luckily, this is very simple: 70 | 71 | ```js 72 | const io = require('future-io') 73 | 74 | // Wrapping a function performing some side effects in an IO. 75 | // The `customOperation` function should return a promise, task or plan value. 76 | const customIO = io.wrapMethod( 77 | 'customOperation', 78 | customOperation 79 | ) 80 | ``` 81 | 82 | # Testing 83 | Testing code performing a lot of IO is usually pretty painful. 84 | Not so when using future-io! 85 | 86 | Simply use `fakePerform` instead of `unsafePerform` to execute your IO actions in tests. 87 | Now you can step through your io functions step by step, 88 | checking the arguments being passed in and choosing values to return. 89 | 90 | `fakePerform()` return an object with three methods: 91 | - `take(actionName)`: Proceed until the next action call. 92 | Assert it has type `actionName`. 93 | Return a promise containg the arguments the action is being called with as an array 94 | - `put(returnValue)`: Call after a `take` resolves to send a return value. 95 | Also call this when not returning a value, to ensure execution of the IO continues. 96 | - `error(ioError)`: Like `put`, but returns an error value. 97 | 98 | When the io execution finishes, fakePerform triggers a last `end` action. 99 | This action is passed an `ioError`, if one exists, in the first-argument position. 100 | 101 | 102 | ## Example using [ava](https://github.com/sindresorhus/ava) and async/await 103 | ```js 104 | import test from 'ava' 105 | import {fakePerform} from 'future-io' 106 | import ioProcess from 'future-io/node/process' 107 | 108 | test('logging the current working directory', async t => { 109 | const io = ioProcess.cwd().chain(ioProcess.stdout.write) 110 | const { put, take } = fakePerform(io) 111 | const cwd = '/home/foo' 112 | 113 | await take('process.cwd') 114 | put(cwd) 115 | 116 | const [ loggedCwd ] = await take('process.stdout.write') 117 | t.is(loggedCwd, cwd) 118 | put() 119 | 120 | const [ ioError ] = await take('end') 121 | t.falsy(ioError) 122 | }) 123 | ``` 124 | 125 | ## Example using [mocha](https://github.com/sindresorhus/ava) and [co](https://github.com/tj/co) 126 | ```js 127 | import co from 'co' 128 | import {fakePerform} from 'future-io' 129 | import ioProcess from 'future-io/node/process' 130 | import assert from 'assert' 131 | 132 | it('logs the current working directory', co.wrap(function* () { 133 | const io = ioProcess.cwd().chain(ioProcess.stdout.write) 134 | const { put, take } = fakePerform(io) 135 | const cwd = '/home/foo' 136 | 137 | yield take('process.cwd') 138 | yield put(cwd) 139 | 140 | const [ loggedCwd ] = yield take('process.stdout.write') 141 | assert.equal(loggedCwd, cwd) 142 | put() 143 | 144 | const [ ioError ] = yield take('end') 145 | assert.ifError(ioError) 146 | })) 147 | ``` 148 | -------------------------------------------------------------------------------- /examples/console.js: -------------------------------------------------------------------------------- 1 | const io = require('../') 2 | const ioConsole = require('../node/console') 3 | 4 | const logPenguins = ioConsole.log('Emperor, Gentoo, Royal') 5 | 6 | io.unsafePerform(logPenguins) 7 | -------------------------------------------------------------------------------- /examples/even: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const ioProcess = require('../node/process') 4 | const io = require('../') 5 | 6 | const even = ioProcess.argv 7 | .map(argv => (parseInt(argv[2]) % 2) === 0) 8 | .chain(even => ioProcess.stdout.write('Is even: ' + even)) 9 | 10 | io.unsafePerform(even) 11 | -------------------------------------------------------------------------------- /examples/fs.js: -------------------------------------------------------------------------------- 1 | const io = require('../') 2 | const fs = require('../node/fs') 3 | 4 | const penguinFile = '/tmp/penguins' 5 | const newPenguins = 'Emperor, Gentoo, Royal' 6 | 7 | const ensurePenquins = fs.exists(penguinFile).chain( 8 | fileExists => fileExists 9 | ? fs.appendFile(penguinFile, ', ' + newPenguins) 10 | : fs.writeFile(penguinFile, newPenguins) 11 | ) 12 | 13 | io.unsafePerform(ensurePenquins) 14 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple csp style channel. 3 | * `put` passes a value into the channel and `take` retrieves one. 4 | * Both return promises that resolve as soon as both methods have been called. 5 | * 6 | * Both methods expect execution to be 'blocked' until the returned promise resolves. 7 | * Calling either method again before that will throw an error. 8 | */ 9 | function Channel () { 10 | var putResolver = null 11 | var takeResolver = null 12 | var value = null 13 | 14 | function checkReady () { 15 | if (putResolver && takeResolver) { 16 | putResolver() 17 | takeResolver(value) 18 | putResolver = null 19 | takeResolver = null 20 | value = null 21 | } 22 | } 23 | 24 | function take () { 25 | if (takeResolver) { 26 | throw new Error('Take was already called.') 27 | } 28 | return new Promise((resolve) => { 29 | takeResolver = resolve 30 | checkReady() 31 | }) 32 | } 33 | 34 | function put (_value) { 35 | if (putResolver) { 36 | throw new Error('Put was already called.') 37 | } 38 | value = _value 39 | return new Promise((resolve) => { 40 | putResolver = resolve 41 | checkReady() 42 | }) 43 | } 44 | 45 | return { put, take } 46 | } 47 | 48 | module.exports = Channel 49 | -------------------------------------------------------------------------------- /lib/fake-perform.js: -------------------------------------------------------------------------------- 1 | const Channel = require('./channel') 2 | const Task = require('data.task') 3 | const $ = require('./types') 4 | 5 | function fakePerform (io) { 6 | const callChannel = new Channel() 7 | const resultChannel = new Channel() 8 | 9 | const interpreter = (name, run, args) => { 10 | return new Task((reject, resolve) => { 11 | callChannel.put({ name, args }) 12 | .then(() => resultChannel.take()) 13 | .then((result) => 14 | result.error ? reject(result.error) : resolve(result.value) 15 | ) 16 | }) 17 | } 18 | 19 | io.interpret(interpreter).fork( 20 | (err) => callChannel.put({ name: 'end', args: [err] }), 21 | () => callChannel.put({ name: 'end', args: [] }) 22 | ) 23 | 24 | function take (type) { 25 | const stack = (new Error()).stack 26 | return callChannel.take().then(call => { 27 | if (call.name === type) { 28 | return call.args 29 | } else { 30 | return Promise.reject(errorWithStack( 31 | `Expected io "${type}" call but got "${call.name}" call instead.`, 32 | stack 33 | )) 34 | } 35 | }) 36 | } 37 | 38 | function put (value) { 39 | resultChannel.put({ value }) 40 | } 41 | 42 | function error (error) { 43 | resultChannel.put({ error }) 44 | } 45 | 46 | return { take, put, error } 47 | } 48 | 49 | function errorWithStack (msg, stack) { 50 | const error = new Error(msg) 51 | error.stack = stack 52 | return error 53 | } 54 | 55 | module.exports = $.def( 56 | 'fakePerform', 57 | {}, 58 | [$.$IO($.a), $.Object], 59 | fakePerform 60 | ) 61 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.IO = require('./io') 2 | exports.unsafePerform = require('./unsafe-perform') 3 | exports.fakePerform = require('./fake-perform') 4 | exports.wrapFunction = require('./wrap-function') 5 | -------------------------------------------------------------------------------- /lib/io.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | const Task = require('data.task') 3 | const $ = require('./types') 4 | 5 | const _IO = daggy.tagged('interpret') 6 | 7 | const IO = $.def( 8 | 'IO', 9 | {}, 10 | [$.Function, $.$IO($.a)], 11 | function (interpret) { 12 | const _interpret = $.def( 13 | 'interpret', 14 | {}, 15 | [$.Function, $.$Task($.a)], 16 | interpret 17 | ) 18 | return new _IO(_interpret) 19 | } 20 | ) 21 | 22 | IO.of = function of (x) { 23 | return new _IO( 24 | (interpreter) => Task.of(x) 25 | ) 26 | } 27 | 28 | IO.error = function error (x) { 29 | return new _IO( 30 | (interpreter) => Task.rejected(x) 31 | ) 32 | } 33 | 34 | _IO.prototype['@@type'] = 'future-io/IO' 35 | 36 | _IO.prototype.toString = () => 'IO' 37 | _IO.prototype.inspect = () => 'IO' 38 | 39 | _IO.prototype.chain = method($.def( 40 | 'IO#chain', 41 | {}, 42 | [$.$IO($.a), $.Function, $.$IO($.b)], 43 | function chain (io, g) { 44 | const _g = $.def( 45 | 'f in IO#chain(f)', 46 | {}, 47 | [$.a, $.$IO($.b)], 48 | g 49 | ) 50 | return new _IO( 51 | (interpreter) => io.interpret(interpreter).chain( 52 | (x) => _g(x).interpret(interpreter) 53 | ) 54 | ) 55 | } 56 | )) 57 | 58 | _IO.prototype.ap = method($.def( 59 | 'IO#ap', 60 | {}, 61 | [$.$IO($.Function), $.$IO($.a), $.$IO($.b)], 62 | function ap (io, m) { 63 | return io.chain((f) => m.map(f)) 64 | } 65 | )) 66 | 67 | _IO.prototype.map = method($.def( 68 | 'IO#map', 69 | {}, 70 | [$.$IO($.a), $.Function, $.$IO($.b)], 71 | function map (io, f) { 72 | return io.chain((x) => IO.of(f(x))) 73 | } 74 | )) 75 | 76 | _IO.prototype.catch = method($.def( 77 | 'IO#catch', 78 | {}, 79 | [$.$IO($.a), $.Function, $.$IO($.b)], 80 | function _catch (io, g) { 81 | const _g = $.def( 82 | 'f in IO#catch(f)', 83 | {}, 84 | [$.a, $.$IO($.b)], 85 | g 86 | ) 87 | return new _IO( 88 | (interpreter) => io.interpret(interpreter).orElse( 89 | (x) => _g(x).interpret(interpreter) 90 | ) 91 | ) 92 | } 93 | )) 94 | 95 | _IO.prototype.run = method($.def( 96 | 'IO#run', 97 | {}, 98 | [$.$IO($.a), $.Function, $.Undefined], 99 | function run (io, interpreter) { 100 | const _interpreter = $.def( 101 | 'interpreter', 102 | {}, 103 | [$.String, $.Function, $.Array($.Any), $.$Task($.a)], 104 | interpreter 105 | ) 106 | const noop = () => {} 107 | // When encountering an error, throw it outside of all contexts. 108 | const onIoError = (err) => setImmediate(() => { throw err }) 109 | io.interpret(_interpreter).fork(onIoError, noop) 110 | } 111 | )) 112 | 113 | // Create a `method` by taking an ordinary function and pre-applying `this` as its first agument. 114 | function method (f) { 115 | return function () { 116 | const args = [this].concat(Array.prototype.slice.call(arguments)) 117 | return f.apply(null, args) 118 | } 119 | } 120 | 121 | module.exports = IO 122 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | const $ = require('sanctuary-def') 2 | const a = $.TypeVariable('a') 3 | const b = $.TypeVariable('b') 4 | const $IO = $.UnaryType( 5 | 'future-io/IO', 6 | (x) => x && x['@@type'] === 'future-io/IO', 7 | () => [] 8 | ) 9 | const $Task = $.UnaryType( 10 | 'Task', 11 | (x) => x && typeof x.fork === 'function', 12 | () => [] 13 | ) 14 | const env = $.env.concat([ 15 | $IO, 16 | $Task 17 | ]) 18 | const def = $.create({ checkTypes: true, env }) 19 | 20 | module.exports = Object.assign({ def, a, b, $IO, $Task }, $) 21 | -------------------------------------------------------------------------------- /lib/unsafe-perform.js: -------------------------------------------------------------------------------- 1 | const $ = require('./types') 2 | 3 | function unsafePerform (io) { 4 | const interpreter = (name, run, args) => { 5 | return run.apply(null, args) 6 | } 7 | io.run(interpreter) 8 | } 9 | 10 | module.exports = $.def( 11 | 'unsafePerform', 12 | {}, 13 | [$.$IO($.a), $.Undefined], 14 | unsafePerform 15 | ) 16 | -------------------------------------------------------------------------------- /lib/wrap-function.js: -------------------------------------------------------------------------------- 1 | const IO = require('./io') 2 | const Task = require('data.task') 3 | const isPromise = require('is-promise') 4 | const $ = require('./types') 5 | 6 | function wrapFunction (name, f) { 7 | return function wrapper (args) { 8 | return new IO( 9 | (interpreter) => interpreter( 10 | name, 11 | function wrapper (/* arguments */) { 12 | try { 13 | const result = f.apply(null, arguments) 14 | if (isPromise(result)) { 15 | return new Task((reject, resolve) => result.then(resolve, reject)) 16 | } else if (isTask(result)) { 17 | return result 18 | } else { 19 | return Task.of(result) 20 | } 21 | } catch (error) { 22 | return Task.rejected(error) 23 | } 24 | }, 25 | Array.prototype.slice.call(arguments) 26 | ) 27 | ) 28 | } 29 | } 30 | 31 | // isTask :: Task -> Boolean 32 | function isTask (maybeTask) { 33 | maybeTask = maybeTask || {} 34 | const hasFork = maybeTask.fork instanceof Function 35 | const hasMap = maybeTask.map instanceof Function 36 | return (hasFork && hasMap) 37 | } 38 | 39 | module.exports = $.def( 40 | 'wrapFunction', 41 | {}, 42 | [$.String, $.Function, $.Function], 43 | wrapFunction 44 | ) 45 | -------------------------------------------------------------------------------- /node/child_process.js: -------------------------------------------------------------------------------- 1 | const wrapFunction = require('../lib/wrap-function') 2 | const childProcess = require('mz/child_process') 3 | 4 | const api = Object.keys(childProcess) 5 | 6 | api.forEach((name) => { 7 | if (typeof childProcess[name] === 'function') { 8 | exports[name] = wrapFunction('childProcess.' + name, childProcess[name]) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /node/console.js: -------------------------------------------------------------------------------- 1 | const wrapFunction = require('../lib/wrap-function') 2 | 3 | const consoleMethods = ['assert', 'dir', 'error', 'info', 'log', 'time', 'timeEnd', 'trace', 'warn'] 4 | consoleMethods.forEach( 5 | name => { exports[name] = wrapFunction('console.' + name, console[name]) } 6 | ) 7 | -------------------------------------------------------------------------------- /node/fs.js: -------------------------------------------------------------------------------- 1 | const wrapFunction = require('../lib/wrap-function') 2 | const fs = require('mz/fs') 3 | 4 | const api = Object.keys(fs) 5 | 6 | api.forEach((name) => { 7 | if (typeof fs[name] === 'function') { 8 | exports[name] = wrapFunction('fs.' + name, fs[name]) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /node/module.js: -------------------------------------------------------------------------------- 1 | const IO = require('../lib/io') 2 | const Task = require('data.task') 3 | const stack = require('callsite') 4 | const resolveRelative = require('resolve') 5 | const path = require('path') 6 | 7 | exports.require = function (modulePath) { 8 | // To resolve the require path later we need to store the calling file here. 9 | const callingFile = stack()[1].getFileName() 10 | return IO( 11 | (interpreter) => interpreter( 12 | 'module.require', 13 | () => resolveToTask(modulePath, callingFile).map(require), 14 | [modulePath] 15 | ) 16 | ) 17 | } 18 | 19 | exports.require.resolve = function (modulePath) { 20 | // To resolve the require path later we need to store the calling file here. 21 | const callingFile = stack()[1].getFileName() 22 | return IO( 23 | (interpreter) => interpreter( 24 | 'module.require.resolve', 25 | () => resolveToTask(modulePath, callingFile), 26 | [modulePath] 27 | ) 28 | ) 29 | } 30 | 31 | function resolveToTask (modulePath, callingFile) { 32 | const basedir = path.dirname(callingFile) 33 | return new Task( 34 | (reject, resolve) => resolveRelative( 35 | modulePath, 36 | { basedir }, 37 | (err, res) => err ? reject(err) : resolve(res) 38 | ) 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /node/process.js: -------------------------------------------------------------------------------- 1 | const wrapFunction = require('../lib/wrap-function') 2 | 3 | exports.stdout = {} 4 | exports.stdout.write = wrapFunction( 5 | 'process.stdout.write', 6 | process.stdout.write.bind(process.stdout) 7 | ) 8 | 9 | exports.stderr = {} 10 | exports.stderr.write = wrapFunction( 11 | 'process.stderr.write', 12 | process.stderr.write.bind(process.stderr) 13 | ) 14 | 15 | exports.argv = wrapFunction( 16 | 'process.argv', 17 | () => process.argv 18 | )() 19 | 20 | exports.exit = wrapFunction( 21 | 'process.exit', 22 | process.exit 23 | ) 24 | 25 | exports.cwd = wrapFunction( 26 | 'process.cwd', 27 | process.cwd 28 | ) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "future-io", 3 | "version": "1.1.0", 4 | "description": "IO Monad for JS", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "npm-run-all test:*", 8 | "test:lint": "standard", 9 | "test:unit": "nyc ava" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/futurize/future-io.git" 14 | }, 15 | "contributors": [ 16 | "schtoeffel", 17 | "Jasper Woudenberg" 18 | ], 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/futurize/future-io/issues" 22 | }, 23 | "homepage": "https://github.com/futurize/future-io#readme", 24 | "standard": { 25 | "parser": "babel-eslint" 26 | }, 27 | "dependencies": { 28 | "callsite": "^1.0.0", 29 | "daggy": "0.0.1", 30 | "data.task": "^3.1.0", 31 | "is-promise": "^2.1.0", 32 | "mz": "^2.4.0", 33 | "resolve": "^1.1.7", 34 | "sanctuary-def": "^0.6.0" 35 | }, 36 | "devDependencies": { 37 | "ava": "^0.18.0", 38 | "babel-eslint": "^7.1.0", 39 | "co": "^4.6.0", 40 | "npm-run-all": "^4.0.1", 41 | "nyc": "^10.0.0", 42 | "standard": "^10.0.0", 43 | "xyz": "^2.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/channel.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import Channel from '../lib/channel' 4 | 5 | test('put then take', async (t) => { 6 | const channel = new Channel() 7 | const value = 'foo' 8 | channel.put(value) 9 | const result = await channel.take() 10 | t.is(result, value) 11 | }) 12 | 13 | test('take then put', async (t) => { 14 | const channel = new Channel() 15 | const value = 'foo' 16 | const takePromise = channel.take() 17 | channel.put(value) 18 | const result = await takePromise 19 | t.is(result, value) 20 | }) 21 | 22 | test('calling put twice consecutively throws', async (t) => { 23 | const channel = new Channel() 24 | channel.put() 25 | t.throws( 26 | () => channel.put() 27 | ) 28 | }) 29 | 30 | test('calling take twice consecutively throws', async (t) => { 31 | const channel = new Channel() 32 | channel.take() 33 | t.throws( 34 | () => channel.take() 35 | ) 36 | }) 37 | -------------------------------------------------------------------------------- /test/fake-perform.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import IO from '../lib/io' 3 | import ioProcess from '../node/process' 4 | import fakePerform from '../lib/fake-perform' 5 | 6 | test('happy flow', async (t) => { 7 | const io = ioProcess.cwd().chain(ioProcess.stdout.write) 8 | const { put, take } = fakePerform(io) 9 | const cwd = '/home/foo' 10 | 11 | await take('process.cwd') 12 | put(cwd) 13 | 14 | const [ loggedCwd ] = await take('process.stdout.write') 15 | t.is(loggedCwd, cwd) 16 | put() 17 | 18 | const [ ioError ] = await take('end') 19 | t.falsy(ioError) 20 | }) 21 | 22 | test('using `IO.of`', async (t) => { 23 | const io = IO.of('foo').chain(ioProcess.stdout.write) 24 | const { put, take } = fakePerform(io) 25 | 26 | const [ loggedString ] = await take('process.stdout.write') 27 | t.is(loggedString, 'foo') 28 | put() 29 | 30 | const [ ioError ] = await take('end') 31 | t.falsy(ioError) 32 | }) 33 | 34 | test('using `IO.error`', async (t) => { 35 | const error = new Error('ioError') 36 | const io = IO.error(error) 37 | const { take } = fakePerform(io) 38 | 39 | const [ ioError ] = await take('end') 40 | t.is(ioError, error) 41 | }) 42 | 43 | test('throwing an io error', async (t) => { 44 | const io = ioProcess.cwd().chain(ioProcess.stdout.write) 45 | const { error, take } = fakePerform(io) 46 | 47 | await take('process.cwd') 48 | const cwdError = new Error('cwd failed') 49 | error(cwdError) 50 | 51 | const [ ioError ] = await take('end') 52 | t.is(ioError, cwdError) 53 | }) 54 | 55 | test('expecting the wrong call', async (t) => { 56 | const io = ioProcess.cwd().chain(ioProcess.stdout.write) 57 | const { take } = fakePerform(io) 58 | 59 | const failingTake = take('wrongCall') 60 | t.throws( 61 | failingTake, 62 | 'Expected io "wrongCall" call but got "process.cwd" call instead.' 63 | ) 64 | }) 65 | 66 | test('waiting for a call that doesn\'t come', async (t) => { 67 | const io = IO.of('foo') 68 | const { take } = fakePerform(io) 69 | 70 | const failingTake = take('wrongCall') 71 | t.throws( 72 | failingTake, 73 | 'Expected io "wrongCall" call but got "end" call instead.' 74 | ) 75 | }) 76 | 77 | test('catching an io error', async (t) => { 78 | const io = ioProcess.cwd() 79 | .catch((error) => IO.of(error.message)) 80 | .chain(ioProcess.stdout.write) 81 | const { put, error, take } = fakePerform(io) 82 | 83 | await take('process.cwd') 84 | const cwdError = new Error('cwd failed') 85 | error(cwdError) 86 | 87 | const [ loggedString ] = await take('process.stdout.write') 88 | t.is(loggedString, cwdError.message) 89 | put() 90 | 91 | const [ ioError ] = await take('end') 92 | t.falsy(ioError) 93 | }) 94 | -------------------------------------------------------------------------------- /test/fixtures/test-module.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /test/helpers/extract.js: -------------------------------------------------------------------------------- 1 | // extract :: IO e a -> Promise { name :: String, args :: Array *, error :: e, value :: a } 2 | function extract (io) { 3 | const interpreter = (name, f, args) => f.apply(null, args) 4 | return new Promise((resolve, reject) => io.interpret(interpreter).fork(reject, resolve)) 5 | } 6 | 7 | module.exports = extract 8 | -------------------------------------------------------------------------------- /test/node-child_process.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import childProcess from '../node/child_process' 3 | import extract from './helpers/extract' 4 | 5 | // librarization of `childProcess` is generic, so we're just testing a single module here. 6 | test('childProcess.spawn', async (t) => { 7 | const io = childProcess 8 | .exec('ls ./test/fixtures') 9 | const [value] = await extract(io) 10 | t.deepEqual(value, 'test-module.js\n') 11 | }) 12 | -------------------------------------------------------------------------------- /test/node-fs.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import fs from '../node/fs' 3 | import fakePerform from '../lib/fake-perform' 4 | import extract from './helpers/extract' 5 | 6 | // librarization of `fs` is generic, so we're just testing a single module here. 7 | test('fs.exists', async (t) => { 8 | const io = fs.exists(__filename) 9 | 10 | // Fake perform. 11 | const { take } = fakePerform(io) 12 | const [ filename ] = await take('fs.exists') 13 | t.is(filename, __filename) 14 | 15 | // Real perform. 16 | const value = await extract(io) 17 | t.is(value, true) 18 | }) 19 | -------------------------------------------------------------------------------- /test/node-module.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import ioModule from '../node/module' 3 | import fakePerform from '../lib/fake-perform' 4 | import extract from './helpers/extract' 5 | 6 | test('module.require.resolve', async (t) => { 7 | const modulePath = './fixtures/test-module' 8 | const io = ioModule.require.resolve(modulePath) 9 | 10 | // Fake perform. 11 | const { take } = fakePerform(io) 12 | const [ actualModulePath ] = await take('module.require.resolve') 13 | t.is(actualModulePath, modulePath) 14 | 15 | // Real perform. 16 | const value = await extract(io) 17 | t.is(value, require.resolve(modulePath)) 18 | }) 19 | 20 | test('module.require', async (t) => { 21 | const modulePath = './fixtures/test-module' 22 | const io = ioModule.require(modulePath) 23 | 24 | // Fake perform. 25 | const { take } = fakePerform(io) 26 | const [ actualModulePath ] = await take('module.require') 27 | t.is(actualModulePath, modulePath) 28 | 29 | // Real perform. 30 | const value = await extract(io) 31 | t.is(value, require(modulePath)) 32 | }) 33 | -------------------------------------------------------------------------------- /test/node-process.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import ioProcess from '../node/process' 3 | import fakePerform from '../lib/fake-perform' 4 | import extract from './helpers/extract' 5 | 6 | test('fs.argv', async (t) => { 7 | const io = ioProcess.argv 8 | 9 | // Fake perform. 10 | const { take } = fakePerform(io) 11 | const args = await take('process.argv') 12 | t.deepEqual(args, []) 13 | 14 | // Real perform. 15 | const value = await extract(io) 16 | t.deepEqual(value, process.argv) 17 | }) 18 | -------------------------------------------------------------------------------- /test/unsafe-perform.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import Task from 'data.task' 3 | import IO from '../lib/io' 4 | import unsafePerform from '../lib/unsafe-perform' 5 | 6 | test.cb('runs io functions', (t) => { 7 | t.plan(2) 8 | 9 | const ioFunction = () => IO( 10 | (interpreter) => interpreter( 11 | 'foo', 12 | () => { 13 | t.pass() 14 | return Task.of() 15 | }, 16 | [] 17 | ) 18 | ) 19 | 20 | const ioFunction2 = () => IO( 21 | (interpreter) => interpreter( 22 | 'bar', 23 | () => { 24 | t.pass() 25 | t.end() 26 | return Task.of() 27 | }, 28 | [] 29 | ) 30 | ) 31 | 32 | const io = ioFunction().chain(ioFunction2) 33 | 34 | unsafePerform(io) 35 | }) 36 | -------------------------------------------------------------------------------- /test/wrap-function.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import wrapFunction from '../lib/wrap-function' 3 | import Task from 'data.task' 4 | import extract from './helpers/extract' 5 | 6 | test('wrapped function returns resolving future', async (t) => { 7 | const f = wrapFunction('foo', (x) => Task.of(x * 2)) 8 | const io = f(4) 9 | const value = await extract(io) 10 | t.is(value, 8) 11 | }) 12 | 13 | test('wrapped function returns rejecting future', async (t) => { 14 | const f = wrapFunction('foo', (x) => Task.rejected(new Error(x * 2))) 15 | const io = f(4) 16 | t.throws( 17 | extract(io), 18 | '8' 19 | ) 20 | }) 21 | 22 | test('wrapped function returns plain values', async (t) => { 23 | const f = wrapFunction('foo', (x) => x * 2) 24 | const io = f(4) 25 | const value = await extract(io) 26 | t.is(value, 8) 27 | }) 28 | 29 | test('wrapped function throws', async (t) => { 30 | const f = wrapFunction('foo', (x) => { throw new Error(x * 2) }) 31 | const io = f(4) 32 | t.throws( 33 | extract(io), 34 | '8' 35 | ) 36 | }) 37 | 38 | test('wrapped function returns resolved promise', async (t) => { 39 | const f = wrapFunction('foo', (x) => Promise.resolve(x * 2)) 40 | const io = f(4) 41 | const value = await extract(io) 42 | t.is(value, 8) 43 | }) 44 | 45 | test('wrapped function returns rejected promise', async (t) => { 46 | const f = wrapFunction('foo', (x) => Promise.reject(new Error(x * 2))) 47 | const io = f(4) 48 | t.throws( 49 | extract(io), 50 | '8' 51 | ) 52 | }) 53 | --------------------------------------------------------------------------------