{"id":18545884,"url":"https://github.com/guillaumearm/handle-io","last_synced_at":"2025-04-09T19:32:18.825Z","repository":{"id":57260494,"uuid":"122104644","full_name":"guillaumearm/handle-io","owner":"guillaumearm","description":":sparkles: - Wrap side effects, combine them, and make this combination testable ","archived":false,"fork":false,"pushed_at":"2020-06-02T18:19:33.000Z","size":8845,"stargazers_count":4,"open_issues_count":37,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-24T11:11:32.454Z","etag":null,"topics":["generator","handle-io","io-monad","saga-pattern","testable","testing-tools"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guillaumearm.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-02-19T18:39:33.000Z","updated_at":"2018-08-27T08:09:10.000Z","dependencies_parsed_at":"2022-08-25T05:02:12.502Z","dependency_job_id":null,"html_url":"https://github.com/guillaumearm/handle-io","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guillaumearm%2Fhandle-io","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guillaumearm%2Fhandle-io/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guillaumearm%2Fhandle-io/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guillaumearm%2Fhandle-io/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guillaumearm","download_url":"https://codeload.github.com/guillaumearm/handle-io/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247994119,"owners_count":21030050,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["generator","handle-io","io-monad","saga-pattern","testable","testing-tools"],"created_at":"2024-11-06T20:22:43.291Z","updated_at":"2025-04-09T19:32:18.325Z","avatar_url":"https://github.com/guillaumearm.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# handle-io :sparkles:\n\n[![CircleCI branch](https://img.shields.io/circleci/project/github/guillaumearm/handle-io/master.svg)](https://circleci.com/gh/guillaumearm/handle-io)\n[![codecov](https://codecov.io/gh/guillaumearm/handle-io/branch/master/graph/badge.svg)](https://codecov.io/gh/guillaumearm/handle-io)\n[![npm](https://img.shields.io/npm/v/handle-io.svg)](https://www.npmjs.com/package/handle-io)\n[![Greenkeeper badge](https://badges.greenkeeper.io/guillaumearm/handle-io.svg)](https://greenkeeper.io/)\n[![NSP Status](https://nodesecurity.io/orgs/trapcodien/projects/b050060a-8207-40cc-a229-89efb0e8cee0/badge)](https://nodesecurity.io/orgs/trapcodien/projects/b050060a-8207-40cc-a229-89efb0e8cee0)\n[![dependencies Status](https://david-dm.org/guillaumearm/handle-io/status.svg)](https://david-dm.org/guillaumearm/handle-io)\n[![devDependencies Status](https://david-dm.org/guillaumearm/handle-io/dev-status.svg)](https://david-dm.org/guillaumearm/handle-io?type=dev)\n[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/guillaumearm/handle-io/blob/master/CONTRIBUTING.md)\n[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)\n[![Join the chat at https://gitter.im/handle-io/Lobby](https://badges.gitter.im/handle-io/Lobby.svg)](https://gitter.im/handle-io/Lobby?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)\n\nHighly inspired by [funkia/io](https://github.com/funkia/io) and [redux-saga](https://github.com/redux-saga/redux-saga), this library intends to wrap small pieces of impure code, orchestrates and tests them.\n\n## Purpose\n\n### Test side effects orchestration without pain\n\n```js\ntestHandler(logTwice('hello world'))\n  .matchIo(log('hello world'))\n  .matchIo(log('hello world'))\n  .run();\n```\n\nThis piece of code is an assertion, an error will be thrown if something goes wrong:\n\n- wrong io function\n- wrong io arguments\n- too much io ran\n- not enough io ran\n\n## Getting started\n\n### Install\n\n```js\nnpm install --save handle-io\n```\n\n------\n\n### IO\n\nio is just a wrapper for functions and arguments.\nIn some way, it transforms impure functions into pure functions.\n\nConceptually, an io function could just be defined in this way:\n\n```js\nconst log = (...args) =\u003e [console.log, args];\n```\n\nbut in `handle-io`, it isn't.\n\n##### Create IO functions\n\nYou can use `io` to create one:\n\n```js\nconst { io } = require('handle-io');\nconst log = io(console.log);\n```\n\n##### Run IO functions\n\nRunning log with arguments:\n\n```js\nlog('Hello', 'World').run(); // print Hello World\n```\n\nRunning log without arguments:\n\n```js\nlog().run();\n// or\nlog.run();\n```\n\n**Keep in mind**: pieces of code using `.run()` cannot be tested properly.\n\nThe idea of this library is to apply an **IO** function inside a structure called **handler**.\n\n------\n\n### Handlers\nA **handler** is a wrapped pure [generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) which just apply some **IO** function and/or **handler**.\n\n**e.g.**\n\n```js\nconst { io, handler } = require('handle-io');\n\nconst log = io(console.log);\n\nconst logTwice = handler(function*(...args) {\n  yield log(...args);\n  yield log(...args);\n});\n```\n\n#### Writing tests for handlers\n\nWriting tests for **handlers** is very simple (please see the first example above).\n\nWhat about testing a **handler** which applies an **IO** function and returns values ?\n\n**There is a very simple way**:\n\n- using the second argument of the .matchIo() method to mock returned values\n- using .shouldReturn() to assert on the final value\n\n**e.g.**\n\n```js\nconst { io, handler } = require('handle-io');\n\nconst getEnv = io((v) =\u003e process.env[v]);\n\nconst addValues = handler(function*() {\n  const value1 = yield getEnv('VALUE1');\n  const value2 = yield getEnv('VALUE2');\n  return value1 + value2;\n});\n\ntestHandler(addValues())\n  .matchIo(getEnv('VALUE1'), 32)\n  .matchIo(getEnv('VALUE2'), 10)\n  .shouldReturn(42)\n  .run();\n```\n\n#### Running handlers\nSame as for **IO** functions, there is a **.run()** method:\n\n```js\naddValues().run(); // =\u003e 42\n// or\naddValue.run();\n```\n\nLikewise, don't use handlers' **.run()** everywhere in your codebase.\n\n**handlers** are combinable together: **you can yield a handler**.\n\n------\n\n### Promise support\n\n`handle-io` supports promises and allows you to create asynchronous IO.\n\n**e.g.**\n\n```js\nconst { io, handler, testHandler } = require('handle-io');\n\n// async io\nconst sleep = io((ms) =\u003e new Promise(resolve =\u003e setTimeout(resolve, ms)));\n\n// create an async combination\nconst sleepSecond = handler(function*(s) {\n  yield sleep(s * 1000);\n  return s;\n});\n\n// test this combination synchronously\ntestHander(sleepSecond(42))\n  .matchIo(sleep(42000))\n  .shouldReturn(42)\n  .run();\n```\n\nPlease note that `sleep(n)` and `sleepSecond(n)` will expose .run() methods that return a promise.\n\n**e.g.**\n\n```js\nsleepSecond(1).run().then((n) =\u003e {\n  console.log(`${n} second(s) waited`);\n});\n```\n\n------\n\n### Dealing with errors\n\n#### using Try/Catch\n\nThe simplest way to handle errors with `handle-io` is to use try/catch blocks.\n\nAs you can see in the example below, you can try/catch any errors:\n\n- inside a handler:\n  - thrown error\n- inside an io function:\n  - thrown error\n  - unhandled promise rejection\n\n**e.g.**\n\n```js\nconst { io, handler } = require('handle-io');\n\nconst handler1 = handler(function*() {\n  throw new Error();\n});\n\n// Synchronous IO\nconst io1 = io(() =\u003e { throw new Error() });\n\n// Asynchronous IO\nconst io2 = io(() =\u003e Promise.reject(new Error()));\n\n// handler2 is safe, it can't throw because it handles errors\nconst handler2 = handler(function*() {\n  try {\n    yield io1();\n    yield io2();\n    yield handler1();\n  } catch (e) {\n    console.error(e);\n  }\n});\n\n```\n\n#### using `catchError` helper\n\nA functional helper exits to avoid try/catchs block, it allows to easily ignore errors and/or results.\n\nUnder the hood, `catchError` uses a try/catch block and works similarly.\n\n**e.g.**\n\n```js\nconst { io, handler, catchError } = require('handle-io');\n\nconst ioError = io((e) =\u003e { throw new Error(e) });\n\nconst myHandler = handler(function*() {\n  const [res, err] = yield catchError(ioError('error'));\n  if (err) {\n    yield log(err);\n  }\n  return res;\n});\n```\n\n### How to test errors\n\nBy default, no mocked **IO** throws any error.\n\nIt's possible to simulate throws with `testHandler` using the `simulateThrow` test utility.\n\nWriting tests for myHandler means two cases need to be handled:\n\n- when `ioError` throws:\n\n```js\ntestHandler(myHandler())\n  .matchIo(ioError('error'), simulateThrow('error'))\n  .matchIo(log('error'))\n  .shouldReturn(undefined)\n  .run();\n```\n\n- when `ioError` doesn't throw:\n\n```js\ntestHandler(myHandler())\n  .matchIo(ioError('error'), 42)\n  .shouldReturn(42)\n  .run();\n```\n\n------\n\n### Custom testHandler\n\nA custom `testHandler` can be created using `createTestHandler`.\n\n**e.g.**\n\n```js\nimport { io, createTestHandler } from 'handle-io';\n\n\nconst createCustomTestHandler = (h, mockedIOs = [], expectedRetValue, assertRet = false, constructor = createCustomTestHandler) =\u003e {\n  return {\n    ...createTestHandler(h, mockedIOs, expectedRetValue, assertRet, constructor),\n    matchLog: (arg, ret) =\u003e constructor(\n      h,\n      [...mockedIOs, [io(console.log)(arg), ret]],\n      expectedRetValue,\n      assertRet,\n      constructor,\n    ),\n  };\n};\n\nconst customTestHandler = h =\u003e createCustomTestHandler(h);\n\nconst log = io(console.log);\nconst myHandler = handler(function*(value) {\n  yield log(value);\n  yield log(value);\n  return 42;\n});\n\ncustomTestHandler(myHandler('hello world'))\n  .shouldReturn(42)\n  .matchLog('hello world')\n  .matchLog('hello world')\n  .run()\n```\n\n------\n\n## Use with [Redux](https://redux.js.org)\n\nThere is a way to use `handler` as [redux middleware](https://redux.js.org/advanced/middleware).\n\nPlease take a look to [redux-fun Handlers](https://github.com/guillaumearm/redux-fun#handlers).\n\n\n## License\n[MIT](https://github.com/guillaumearm/handle-io/blob/master/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguillaumearm%2Fhandle-io","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguillaumearm%2Fhandle-io","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguillaumearm%2Fhandle-io/lists"}