diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..db67b0e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# Docker build instructions. +Dockerfile +.dockerignore + +# Revision control. +.git/ +**/.gitignore + +# Vim files. +tags +**/*.sw[po] + +# Build tools in base image. +./rebar3 + +# Local build artifacts. +./_build +.virtualenv +log +logs +test/.rebar3 diff --git a/.gitignore b/.gitignore index 3826c85..c532002 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,26 @@ -.eunit -deps -*.o *.beam +*.o *.plt +.erlang.cookie +.idea +_build +ebin erl_crash.dump -ebin/*.beam -rel/example_project -.concrete/DEV_MODE -.rebar +log +logs +rebar3.crashdump +.rebar3 + +# CI secrets. +codeship.aes +deploy/dockercfg.json +deploy/env.env +config/test.config + +# Optional python dependencies. +.virtualenv + +# Vim files. +*.swo +*.swp +tags diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..87179e8 --- /dev/null +++ b/.yamllint @@ -0,0 +1,4 @@ +ignore: | + /_build + /.virtualenv + /docker/.volumes diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0dd9ad2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM erlang:21.2 + +# Install build and test dependencies. +# python3-pip: for installing python build dependencies. +RUN set -xe && apt-get update && apt-get install -y python3-pip + +ENV REBAR="rebar3" +RUN mkdir -p /usr/src/app +WORKDIR /usr/src/app + +COPY ./requirements.txt ./ +RUN pip3 install -r requirements.txt + +COPY . ./ +RUN make compile + +ENTRYPOINT ["make"] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..80ebe8c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2018, John Krukoff . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..21ac943 --- /dev/null +++ b/Makefile @@ -0,0 +1,91 @@ +MAKEFLAGS += --warn-undefined-variables +.DEFAULT_GOAL := all + +PYTHON := python3 +REBAR ?= ./rebar3 + +.PHONY: all +## Build everything. +all: test-unit doc + ${MAKE} compile + +.PHONY: clean +## Delete intermediate files. +clean: + ${REBAR} clean -a + +.PHONY: compile +## Compile all profiles. +compile: + ${REBAR} compile + ${REBAR} as test compile + +.PHONY: doc +## Build documentation. +doc: require-pic2plot $(addsuffix .png,$(basename $(wildcard doc/*.pic))) + ${REBAR} edoc + ${REBAR} as markdown edoc + +doc/%.png: doc/%.pic + pic2plot --font-size=0.010 --bitmap-size=4096x4096 --line-width=0.00097656 -Tpng $< > $@ + +.SILENT: help +.PHONY: help +## This help screen. +help: + # Extracts help from the Makefile itself, printing help for any rule + # which matches the defined regular expression and that has a double + # hash (##) comment on the line above. + printf "Available Targets:\n\n" + awk '/^[a-zA-Z\-\_0-9]+:/ { \ + helpMessage = match(lastLine, /^## (.*)/); \ + if (helpMessage) { \ + helpCommand = substr($$1, 0, index($$1, ":")); \ + helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ + printf "%-18s %s\n", helpCommand, helpMessage; \ + } \ + } \ + { lastLine = $$0 }' ${MAKEFILE_LIST} + +rebar.lock: rebar.config + ${REBAR} update + ${REBAR} unlock + ${REBAR} upgrade + +REQUIREMENTS = $(addprefix require-,pic2plot yamllint virtualenv) + +.PHONY: ${REQUIREMENTS} +${REQUIREMENTS}: what=$(patsubst require-%,%,$@) +${REQUIREMENTS}: require-%: + @which $(what) > /dev/null || \ + (printf "%s%s%s\n" "$$(tput setaf 3)" '"$(what)" is required, please install.' "$$(tput sgr0)"; exit 1) + +rebar3: + curl -o rebar3 https://s3.amazonaws.com/rebar3/rebar3 + chmod 755 rebar3 + +.PHONY: test +## Run all test suites. +test: test-unit test-integration + +.PHONY: test-integration +## Run the integration test suite. +test-integration: + envsubst < config/test.config.template > config/test.config + if ! ${REBAR} as test do ct --config=config/test.config; then \ + grep -lr 'CT Error Notification' _build/test/logs/ | xargs -n 1 html2text; \ + fi + +.PHONY: test-unit +## Run the unit test suite. +test-unit: require-yamllint + ${REBAR} as prod dialyzer + ${REBAR} as test do dialyzer, eunit, proper, geas #, lint + yamllint -s . + +.virtualenv: requirements.txt + ${MAKE} require-virtualenv + virtualenv --clear --download --always-copy -p "$$(which ${PYTHON})" .virtualenv + .virtualenv/bin/pip install --force-reinstall --upgrade pip setuptools wheel + .virtualenv/bin/pip install -r requirements.txt + @printf "\n\n%s%s%s\n" "$$(tput setaf 3)" "To activate virtualenv run: source .virtualenv/bin/activate" "$$(tput sgr0)" diff --git a/doc/overview.edoc b/doc/overview.edoc new file mode 100644 index 0000000..092772e --- /dev/null +++ b/doc/overview.edoc @@ -0,0 +1,6 @@ +@author John Krukoff +@copyright 2019 John Krukoff +@version 1.0.0 +@title pipe_line +@doc

Overview

+@end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fa1e456 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +--- +version: "3" + +services: + + test: + build: + context: . + container_name: partial + command: test-unit diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..d46050e --- /dev/null +++ b/rebar.config @@ -0,0 +1,41 @@ +{minimum_otp_vsn, "18.0"}. + +{erl_opts, [debug_info, + {warn_format, 1}, + warn_export_all, + warn_export_vars, + warn_obsolete_guard, + warn_unused_import] +}. + +{plugins, [rebar3_hex]}. + +{dialyzer, [{plt_prefix, "pipe_line"}, + {warnings, [unmatched_returns, + error_handling, + race_conditions, + underspecs]}] +}. + +{eunit_tests, [{inparallel, [{application, pipe}, + {module, test_pipe}]}] +}. + +{xref_warnings, true}. + +{profiles, [{native, [{erl_opts, [{native, o3}, + {d, 'NATIVE'}]}] + }, + {test, [{erl_opts, [{d, 'TEST'}]}, + {deps, [proper]}, + {plugins, [geas_rebar3, + rebar3_lint, + rebar3_proper]}, + {dialyzer, [{plt_prefix, "pipe_line_test"}]}] + }, + {markdown, [{deps, [edown]}, + {edoc_opts, [{doclet, edown_doclet}, + {top_level_readme, {"./README.md", + "http://github.com/jkrukoff/pipe_line"}}]}] + }] +}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..57afcca --- /dev/null +++ b/rebar.lock @@ -0,0 +1 @@ +[]. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..644d1d9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +docker-compose +html2text +yamllint diff --git a/src/pipe.app.src b/src/pipe.app.src new file mode 100644 index 0000000..5f2d370 --- /dev/null +++ b/src/pipe.app.src @@ -0,0 +1,11 @@ +{application, pipe, + [{description, "Utility library for chaining function application."}, + {vsn, "1.0.0"}, + {applications, [kernel, + stdlib]}, + {modules, [pipe]}, + {registered, []}, + {env, []}, + {licenses, ["Apache 2.0"]}, + {links, [{"GitHub", "https://github.com/jkrukoff/pipe_line"}]}, + {extra, [{maintainers, ["John Krukoff"]}]}]}. diff --git a/src/pipe.erl b/src/pipe.erl new file mode 100644 index 0000000..38caa22 --- /dev/null +++ b/src/pipe.erl @@ -0,0 +1,153 @@ +%%%------------------------------------------------------------------- +%%% @doc +%%% +%%% @end +%%%------------------------------------------------------------------- +-module(pipe). +-compile(inline). + +-define(MATCH_ERROR(Error), (element(1, Error) == error)). +-define(SPEC_MONAD(For), -spec For(Fun :: monoid(), Value :: term()) -> term()). + +-type monoid() :: fun((term()) -> term()). +-type monad() :: fun((term(), monoid()) -> term()). + +%% API +-export([always/2, + ignore/2, + apply/2, + if_ok/2, + if_not_error/2, + if_not_throw/2, + if_not_exception/2, + compose/2, + via/2, + pipe/3, + line/2, + ok/2, + not_error/2, + not_throw/2, + not_exception/2]). + +-export_type([monad/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== + +?SPEC_MONAD(always). +always(Fun, Value) -> + Fun(Value). + +?SPEC_MONAD(ignore). +ignore(Fun, Value) -> + Fun(Value), + Value. + +?SPEC_MONAD(apply). +apply(Fun, Value) when is_list(Value) -> + erlang:apply(Fun, Value); +apply(Fun, Value) when is_tuple(Value) -> + erlang:apply(Fun, tuple_to_list(Value)). + +?SPEC_MONAD(if_ok). +if_ok(Fun, Value) -> + case Value of + {ok, Ok} -> + Fun(Ok); + _ -> + Value + end. + +?SPEC_MONAD(if_not_error). +if_not_error(Fun, Value) -> + case Value of + {ok, Ok} -> + Fun(Ok); + Error when ?MATCH_ERROR(Error) -> + Error; + _ -> + Fun(Value) + end. + +?SPEC_MONAD(if_not_throw). +if_not_throw(Fun, Value) -> + Try = fun (Unwrapped) -> + try Fun(Unwrapped) + catch + throw:Reason -> + exception_as_error(throw, Reason) + end + end, + case Value of + {ok, Ok} -> + Try(Ok); + Error when ?MATCH_ERROR(Error) -> + Error; + _ -> + Try(Value) + end. + +?SPEC_MONAD(if_not_exception). +if_not_exception(Fun, Value) -> + case Value of + {ok, Ok} -> + try Fun(Ok) + catch + Class:Reason -> + exception_as_error(Class, Reason) + end; + Error when ?MATCH_ERROR(Error) -> + Error; + _ -> + try Fun(Value) + catch + Class:Reason -> + exception_as_error(Class, Reason) + end + end. + +-spec compose(monad(), monad()) -> monad(). +compose(Monad1, Monad2) -> + fun (Fun, Value) -> + Monad1(fun (Inner) -> Monad2(Fun, Inner) end, Value) + end. + +via([_ | _] = Apply, Start) -> + [Head | Tail] = lists:reverse(Apply), + Composed = lists:foldl(fun compose/2, Head, Tail), + fun (Value) -> Composed(Start, Value) end. + +-spec pipe([monad()] | monad(), term(), [[fun((...) -> term())]] | [monoid()]) -> term(). +pipe([_ | _] = Apply, Start, Funs) -> + [Head | Tail] = lists:reverse(Apply), + pipe(lists:foldl(fun compose/2, Head, Tail), Start, Funs); +pipe(Apply, Start, Funs) -> + do_pipe(Apply, Start, Funs). + +line(Start, Funs) -> + do_pipe(fun always/2, Start, Funs). + +ok(Start, Funs) -> + do_pipe(fun if_ok/2, Start, Funs). + +not_error(Start, Funs) -> + do_pipe(fun if_not_error/2, Start, Funs). + +not_throw(Start, Funs) -> + do_pipe(fun if_not_throw/2, Start, Funs). + +not_exception(Start, Funs) -> + do_pipe(fun if_not_exception/2, Start, Funs). + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== + +exception_as_error(Class, Reason) -> + {error, {Class, Reason}}. + +do_pipe(_Apply, Value, []) -> + Value; +do_pipe(Apply, Value, [Fun | Funs]) -> + do_pipe(Apply, Apply(Fun, Value), Funs). diff --git a/test/test_pipe.erl b/test/test_pipe.erl new file mode 100644 index 0000000..e285da9 --- /dev/null +++ b/test/test_pipe.erl @@ -0,0 +1,171 @@ +%%%------------------------------------------------------------------- +%%% @doc +%%% +%%% @end +%%%------------------------------------------------------------------- +-module(test_pipe). + +-include_lib("eunit/include/eunit.hrl"). + +%%%=================================================================== +%%% Tests +%%%=================================================================== + +always_test() -> + ?assertEqual(value, pipe:always(identity(), value)). + +ignore_test() -> + ?assertEqual(value, pipe:ignore(constant(computed), value)). + +apply_test() -> + ?assertEqual(1, pipe:apply(fun erlang:'-'/2, {3, 2})). + +if_ok_test_() -> + error_monad_tests(fun pipe:if_ok/2, + computed, + value, + {error, value}) ++ + [?_assertThrow(exception, + pipe:if_ok(throws(exception), {ok, value})), + ?_assertError(exception, + pipe:if_ok(errors(exception), {ok, value}))]. + +if_not_error_test_() -> + error_monad_tests(fun pipe:if_not_error/2, + computed, + computed, + {error, value}) ++ + [?_assertThrow(exception, + pipe:if_not_error(throws(exception), {ok, value})), + ?_assertError(exception, + pipe:if_not_error(errors(exception), {ok, value}))]. + +if_not_throw_test_() -> + error_monad_tests(fun pipe:if_not_throw/2, + computed, + computed, + {error, value}) ++ + [?_assertEqual({error, {throw, exception}}, + pipe:if_not_throw(throws(exception), {ok, value})), + ?_assertError(exception, + pipe:if_not_throw(errors(exception), {ok, value}))]. + +if_not_exception_test_() -> + error_monad_tests(fun pipe:if_not_exception/2, + computed, + computed, + {error, value}) ++ + [?_assertEqual({error, {throw, exception}}, + pipe:if_not_exception(throws(exception), {ok, value})), + ?_assertEqual({error, {error, exception}}, + pipe:if_not_exception(errors(exception), {ok, value}))]. + +compose_test() -> + Composed = pipe:compose(fun pipe:if_ok/2, fun pipe:ignore/2), + ?assertEqual(value, Composed(constant(computed), {ok, value})). + +via_test() -> + Composed = pipe:via([fun pipe:if_ok/2, fun pipe:ignore/2], + constant(computed)), + ?assertEqual(value, Composed({ok, value})). + +pipe_test_() -> + [{"single", + ?_assertEqual(3, pipe:pipe(fun pipe:always/2, + 2, + [not_commutative()]))}, + {"multiple", + ?_assertEqual(5, pipe:pipe(fun pipe:always/2, + value, + [constant(2), + not_commutative(), + not_commutative()]))}, + {"multiple applies", + ?_assertEqual(value, pipe:pipe([fun pipe:if_ok/2, fun pipe:ignore/2], + {ok, value}, + [constant({ok, computed}), + constant(computed)]))}, + {"via", + ?_assertEqual(value, pipe:pipe(fun pipe:if_ok/2, + {ok, value}, + [pipe:via([fun pipe:ignore/2], + constant({ok, computed})), + constant(computed)]))}, + {"composable", + ?_assertEqual({ok, value}, pipe:pipe(fun pipe:if_ok/2, + pipe:pipe(fun pipe:if_ok/2, + {ok, value}, + [ok()]), + [ok()]))}]. + +pipe_line_test() -> + ?assertEqual(computed, + pipe:line(value, + [identity(), constant(computed), identity()])). + +pipe_ok_test_() -> + error_pipe_tests(fun pipe:ok/2, + {ok, computed}, + computed, + {error, computed}). + +pipe_not_error_test_() -> + error_pipe_tests(fun pipe:not_error/2, + {ok, computed}, + {ok, computed}, + {error, computed}). + +pipe_not_throw_test_() -> + error_pipe_tests(fun pipe:not_throw/2, + {ok, computed}, + {ok, computed}, + {error, computed}). + +pipe_not_exception_test_() -> + error_pipe_tests(fun pipe:not_exception/2, + {ok, computed}, + {ok, computed}, + {error, computed}). + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== + +identity() -> + fun (X) -> X end. + +constant(Value) -> + fun (_) -> Value end. + +not_commutative() -> + fun (X) -> X * 2 - 1 end. + +throws(Value) -> + fun (_) -> throw(Value) end. + +errors(Value) -> + fun (_) -> erlang:error(Value) end. + +ok() -> + fun (Value) -> {ok, Value} end. + +error_monad_tests(Monad, OkExpected, BareExpected, ErrorExpected) -> + For = io_lib:format("~w ", [Monad]), + [{lists:flatten([For, "ok"]), + ?_assertEqual(OkExpected, Monad(constant(computed), {ok, value}))}, + {lists:flatten([For, "bare"]), + ?_assertEqual(BareExpected, Monad(constant(computed), value))}, + {lists:flatten([For, "error"]), + ?_assertEqual(ErrorExpected, Monad(constant(computed), {error, value}))}]. + +error_pipe_tests(Pipe, OkExpected, BareExpected, ErrorExpected) -> + For = io_lib:format("~w ", [Pipe]), + [{lists:flatten([For, "ok"]), + ?_assertEqual(OkExpected, + Pipe({ok, value}, [constant({ok, computed}), ok(), ok()]))}, + {lists:flatten([For, "bare"]), + ?_assertEqual(BareExpected, + Pipe({ok, value}, [constant(computed), ok(), ok()]))}, + {lists:flatten([For, "error"]), + ?_assertEqual(ErrorExpected, + Pipe({ok, value}, [constant({error, computed}), ok(), ok()]))}].