From 501cb15d571868c1d351f728c0c3a2441377f255 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 11:29:51 -0600 Subject: [PATCH 01/11] Fix `normalize` to skip exceptions --- lib/exceptional/normalize.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/exceptional/normalize.ex b/lib/exceptional/normalize.ex index 82ed3e9..d363eea 100644 --- a/lib/exceptional/normalize.ex +++ b/lib/exceptional/normalize.ex @@ -82,6 +82,7 @@ defmodule Exceptional.Normalize do if Exception.exception?(err), do: err, else: plain {:ok, value} -> value + exc = %{__exception__: _} -> exc value -> conversion_fun.(value) end end From baa671c7581890fb949744bc8c4a68ca56fcf803 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 12:36:05 -0600 Subject: [PATCH 02/11] Added the `block` command --- lib/exceptional.ex | 1 + lib/exceptional/block.ex | 164 ++++++++++++++++++++++++++++++++++++++ test/exceptional_test.exs | 1 + 3 files changed, 166 insertions(+) create mode 100644 lib/exceptional/block.ex diff --git a/lib/exceptional.ex b/lib/exceptional.ex index 733a470..3a4a372 100644 --- a/lib/exceptional.ex +++ b/lib/exceptional.ex @@ -23,6 +23,7 @@ defmodule Exceptional do defmacro __using__(opts \\ []) do quote bind_quoted: [opts: opts] do + use Exceptional.Block, opts use Exceptional.Control, opts use Exceptional.Normalize, opts use Exceptional.Pipe, opts diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex new file mode 100644 index 0000000..eeeed87 --- /dev/null +++ b/lib/exceptional/block.ex @@ -0,0 +1,164 @@ +defmodule Exceptional.Block do + @moduledoc ~S""" + Convenience functions to wrap a block of calls similar to `with`. + + ## Convenience `use`s + + Everything: + + use Exceptional.Block + + """ + + defmacro __using__(_) do + quote do + import unquote(__MODULE__) + end + end + + + @doc ~S""" + This specifies a block that is tested as normal similar to Elixir's `with`. + + This will auto-normalize every return value, so expect a raw `value` return, + not something like `{:ok, value}` when using `<-`. `=` is unwrapped and + unhandled. + + ## Examples + + iex> use Exceptional.Block + ...> block do + ...> _ <- {:ok, 2} + ...> end + 2 + + iex> use Exceptional.Block + ...> block do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> c <- {:ok, b * 2} + ...> c * 2 + ...> end + 16 + + iex> use Exceptional.Block + ...> block do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> _ = 42 + ...> c <- {:error, "Failed: #{b}"} + ...> c * 2 + ...> end + %ErlangError{original: "Failed: 4"} + + iex> use Exceptional.Block + ...> block do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> _ = 42 + ...> c <- {:error, "Failed: #{b}"} + ...> c * 2 + ...> else + ...> _ -> {:error, "unknown error"} + ...> end + %ErlangError{original: "unknown error"} + + iex> use Exceptional.Block + ...> conversion_fun = fn {:blah, reason} -> %ErlangError{original: "Blah: #{reason}"}; e -> e end + ...> block conversion_fun: conversion_fun do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> _ = 42 + ...> c <- {:blah, "Failed: #{b}"} + ...> c * 2 + ...> else + ...> _ -> {:error, "unknown error"} + ...> end + %ErlangError{original: "unknown error"} + + """ + defmacro block(opts, bodies \\ []) do + opts = bodies ++ opts + gen_block(opts) + end + + defp gen_block(opts) do + {:__block__, _meta, do_body} = wrap_block(opts[:do] || throw "Must specify a `do` body clause with at least one expression!") + conversion_fun_ast = + case opts[:conversion_fun] do + nil -> quote do fn x -> x end end + call -> call + end + conversion_fun = gen_unique_var("$conversion_fun") + else_fun_ast = + case opts[:else] do + nil -> quote do fn x -> x end end + clauses -> + # credo:disable-for-lines:7 + quote do + fn x -> + case x do + unquote(clauses) + end + |> Exceptional.Normalize.normalize(unquote(conversion_fun)) + end + end + end + else_fun = gen_unique_var("$else_fun") + body = gen_block_body(do_body, conversion_fun, else_fun) + quote generated: true do + unquote(conversion_fun) = unquote(conversion_fun_ast) + unquote(else_fun) = unquote(else_fun_ast) + unquote(body) + end + end + + defp wrap_block({:__block__, _, _} = ast), do: ast + defp wrap_block(ast), do: {:__block__, [], [ast]} + + defp gen_block_body(exprs, conversion_fun, else_fun) + defp gen_block_body([{:<-, meta, [binding, bound]} | exprs], conversion_fun, else_fun) do + value = Macro.var(:"$val", __MODULE__) + next = + case exprs do + [] -> value + _ -> gen_block_body(exprs, conversion_fun, else_fun) + end + {call, gen_meta, args} = + quote generated: true do + case Exceptional.Normalize.normalize(unquote(bound), unquote(conversion_fun)) do + %{__exception__: _} = unquote(value) -> unquote(else_fun).(unquote(value)) + unquote(binding) = unquote(value) -> unquote(next) + unquote(value) -> unquote(else_fun).(unquote(value)) + end + end + {call, meta ++ gen_meta, args} + end + defp gen_block_body([expr | exprs], conversion_fun, else_fun) do + value = gen_unique_var(:"$val") + next = + case exprs do + [] -> value + _ -> gen_block_body(exprs, conversion_fun, else_fun) + end + quote generated: true do + unquote(value) = unquote(expr) + unquote(next) + end + end + defp gen_block_body(exprs, _conversion_fun, _else_fun) do + throw {:UNHANDLED_EXPRS, exprs} + end + + defp gen_unique_var(name) do + id = Process.get(__MODULE__, 0) + Process.put(__MODULE__, id + 1) + if id === 0 do + String.to_atom(name) + else + String.to_atom("#{name}_#{id}") + end + |> Macro.var(__MODULE__) + end + +end diff --git a/test/exceptional_test.exs b/test/exceptional_test.exs index 283504a..c7033a3 100644 --- a/test/exceptional_test.exs +++ b/test/exceptional_test.exs @@ -1,6 +1,7 @@ defmodule Exceptional.PipeTest do use ExUnit.Case + doctest Exceptional.Block, [import: true] doctest Exceptional.Control, [import: true] doctest Exceptional.Normalize, [import: true] doctest Exceptional.Pipe, [import: true] From e895f170608f140eeb449c9a90bce2a7819c2948 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 12:55:49 -0600 Subject: [PATCH 03/11] Fix all credo issues and constrain to the more recent credo version --- lib/exceptional/block.ex | 25 +++++++++++++++---------- lib/exceptional/raise.ex | 4 ++-- lib/exceptional/safe.ex | 5 +---- lib/exceptional/value.ex | 4 ++-- mix.exs | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex index eeeed87..b18cf24 100644 --- a/lib/exceptional/block.ex +++ b/lib/exceptional/block.ex @@ -16,7 +16,6 @@ defmodule Exceptional.Block do end end - @doc ~S""" This specifies a block that is tested as normal similar to Elixir's `with`. @@ -64,7 +63,10 @@ defmodule Exceptional.Block do %ErlangError{original: "unknown error"} iex> use Exceptional.Block - ...> conversion_fun = fn {:blah, reason} -> %ErlangError{original: "Blah: #{reason}"}; e -> e end + ...> conversion_fun = fn + ...> {:blah, reason} -> %ErlangError{original: "Blah: #{reason}"} + ...> e -> e + ...> end ...> block conversion_fun: conversion_fun do ...> a <- {:ok, 2} ...> b = a * 2 @@ -94,10 +96,11 @@ defmodule Exceptional.Block do case opts[:else] do nil -> quote do fn x -> x end end clauses -> - # credo:disable-for-lines:7 + # credo:disable-for-lines:7 /Alias|Nesting/ quote do fn x -> - case x do + x + |> case do unquote(clauses) end |> Exceptional.Normalize.normalize(unquote(conversion_fun)) @@ -126,6 +129,7 @@ defmodule Exceptional.Block do end {call, gen_meta, args} = quote generated: true do + # credo:disable-for-lines:1 Credo.Check.Design.AliasUsage case Exceptional.Normalize.normalize(unquote(bound), unquote(conversion_fun)) do %{__exception__: _} = unquote(value) -> unquote(else_fun).(unquote(value)) unquote(binding) = unquote(value) -> unquote(next) @@ -153,12 +157,13 @@ defmodule Exceptional.Block do defp gen_unique_var(name) do id = Process.get(__MODULE__, 0) Process.put(__MODULE__, id + 1) - if id === 0 do - String.to_atom(name) - else - String.to_atom("#{name}_#{id}") - end - |> Macro.var(__MODULE__) + name = + if id === 0 do + String.to_atom(name) + else + String.to_atom("#{name}_#{id}") + end + Macro.var(name, __MODULE__) end end diff --git a/lib/exceptional/raise.ex b/lib/exceptional/raise.ex index 771424f..6046e49 100644 --- a/lib/exceptional/raise.ex +++ b/lib/exceptional/raise.ex @@ -51,7 +51,7 @@ defmodule Exceptional.Raise do ** (ArgumentError) raise me """ - @lint {Credo.Check.Design.AliasUsage, false} + # credo:disable-for-lines:8 Credo.Check.Design.AliasUsage defmacro raise_or_continue!(maybe_exception, continue) do quote do require Exceptional.Control @@ -75,7 +75,7 @@ defmodule Exceptional.Raise do ** (ArgumentError) raise me """ - @lint {Credo.Check.Design.AliasUsage, false} + # credo:disable-for-lines:8 Credo.Check.Design.AliasUsage defmacro maybe_exception >>> continue do quote do require Exceptional.Control diff --git a/lib/exceptional/safe.ex b/lib/exceptional/safe.ex index 3b8bb94..18fee9b 100644 --- a/lib/exceptional/safe.ex +++ b/lib/exceptional/safe.ex @@ -78,11 +78,8 @@ defmodule Exceptional.Safe do :error """ + # credo:disable-for-lines:37 /ABCSize|CyclomaticComplexity/ @spec safe(fun) :: fun - @lint [ - {Credo.Check.Refactor.ABCSize, false}, - {Credo.Check.Refactor.CyclomaticComplexity, false} - ] def safe(dangerous) do safe = safe(dangerous, :dynamic) {:arity, arity} = :erlang.fun_info(dangerous, :arity) diff --git a/lib/exceptional/value.ex b/lib/exceptional/value.ex index 924f946..79b9b80 100644 --- a/lib/exceptional/value.ex +++ b/lib/exceptional/value.ex @@ -66,7 +66,7 @@ defmodule Exceptional.Value do ** (Enum.OutOfBoundsError) out of bounds error """ - @lint {Credo.Check.Design.AliasUsage, false} + # credo:disable-for-lines:8 Credo.Check.Design.AliasUsage @spec exception_or_continue(Exception.t | any, fun) :: Exception.t | any defmacro exception_or_continue(maybe_exception, continue) do quote do @@ -101,7 +101,7 @@ defmodule Exceptional.Value do ** (Enum.OutOfBoundsError) out of bounds error """ - @lint {Credo.Check.Design.AliasUsage, false} + # credo:disable-for-lines:8 Credo.Check.Design.AliasUsage defmacro maybe_exception ~> continue do quote do require Exceptional.Control diff --git a/mix.exs b/mix.exs index df36675..a7c9ead 100644 --- a/mix.exs +++ b/mix.exs @@ -26,7 +26,7 @@ defmodule Exceptional.Mixfile do start_permanent: Mix.env == :prod, deps: [ - {:credo, "~> 0.5", only: [:dev, :test]}, + {:credo, "~> 0.8", only: [:dev, :test]}, {:dialyxir, "~> 0.3", only: :dev}, {:earmark, "~> 1.0", only: :dev}, From 5dfd0c3781df7c39668ca1f17ffbfa9590fb4b66 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:06:02 -0600 Subject: [PATCH 04/11] Added throwing version of `block` as `block!` --- lib/exceptional/block.ex | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex index b18cf24..f9e14b5 100644 --- a/lib/exceptional/block.ex +++ b/lib/exceptional/block.ex @@ -84,6 +84,30 @@ defmodule Exceptional.Block do gen_block(opts) end + @doc ~S""" + The auto-throwing version of `block`, will raise it's final error. + + ## Examples + + iex> use Exceptional.Block + ...> block! do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> _ = 42 + ...> c <- {:error, "Failed: #{b}"} + ...> c * 2 + ...> end + ** (ErlangError) Erlang error: "Failed: 4" + + """ + defmacro block!(opts, bodies \\ []) do + opts = bodies ++ opts + body = gen_block(opts) + quote do + Exceptional.Raise.ensure!(unquote(body)) + end + end + defp gen_block(opts) do {:__block__, _meta, do_body} = wrap_block(opts[:do] || throw "Must specify a `do` body clause with at least one expression!") conversion_fun_ast = From 791e028809376ff400f102599bdb399af41ecd14 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:06:57 -0600 Subject: [PATCH 05/11] Make Credo happy again --- lib/exceptional/block.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex index f9e14b5..38d1d48 100644 --- a/lib/exceptional/block.ex +++ b/lib/exceptional/block.ex @@ -104,6 +104,7 @@ defmodule Exceptional.Block do opts = bodies ++ opts body = gen_block(opts) quote do + # credo:disable-for-lines:1 Credo.Check.Design.AliasUsage Exceptional.Raise.ensure!(unquote(body)) end end From dedf98fa2bfee8a5da78af47f8de22443ec22676 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:08:53 -0600 Subject: [PATCH 06/11] Add some docs for `block`/`block!` --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index acc8c93..f7828b9 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,61 @@ end #=> "error message" ``` +### [Block](https://hexdocs.pm/exceptional/Exceptional.Block.html) + +Kind of a combination of Elixir's normal +[`with`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1) +special form in addition to a monad'y `do` pipeline. + +This automatically-wraps every return value with +[`normalize`](https://hexdocs.pm/exceptional/Exceptional.Normalize.html). + +```elixir +block do + a <- {:ok, 2} + b = a * 2 + c <- {:ok, b * 2} + c * 2 +end +#=> 16 + + +block do + a <- {:ok, 2} + b = a * 2 + _ = 42 + c <- {:error, "Failed: #{b}"} + c * 2 +end +#=> %ErlangError{original: "Failed: 4"} + + +conversion_fun = fn + {:blah, reason} -> %ErlangError{original: "Blah: #{reason}"} + e -> e +end +block conversion_fun: conversion_fun do + a <- {:ok, 2} + b = a * 2 + _ = 42 + c <- {:blah, "Failed: #{b}"} + c * 2 +else + _ -> {:error, "unknown error"} +end +#=> %ErlangError{original: "unknown error"} + + +block! do + a <- {:ok, 2} + b = a * 2 + _ = 42 + c <- {:error, "Failed: #{b}"} + c * 2 +end +#=> ** (ErlangError) Erlang error: "Failed: 4" +``` + ## Related Packages - [Phoenix/Exceptional](https://hex.pm/packages/phoenix_exceptional) From 7fe96b59ada7f2332390db4635cba24a4c0ca1e1 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:13:32 -0600 Subject: [PATCH 07/11] Fix double-spacing --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index f7828b9..ff36e4d 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,6 @@ block do end #=> 16 - block do a <- {:ok, 2} b = a * 2 @@ -305,7 +304,6 @@ block do end #=> %ErlangError{original: "Failed: 4"} - conversion_fun = fn {:blah, reason} -> %ErlangError{original: "Blah: #{reason}"} e -> e @@ -321,7 +319,6 @@ else end #=> %ErlangError{original: "unknown error"} - block! do a <- {:ok, 2} b = a * 2 From 7918a4887f646deb0a4bbf2f70a8022979966202 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:14:59 -0600 Subject: [PATCH 08/11] Better grammar --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff36e4d..4434aff 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ end Kind of a combination of Elixir's normal [`with`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1) -special form in addition to a monad'y `do` pipeline. +special form in addition to a monad-style `do` pipeline. This automatically-wraps every return value with [`normalize`](https://hexdocs.pm/exceptional/Exceptional.Normalize.html). From b7e8bdccccaca1396390596e53cfec96156b65e4 Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:23:39 -0600 Subject: [PATCH 09/11] More explicit docs on how matching occurs --- README.md | 10 ++++++++++ lib/exceptional/block.ex | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/README.md b/README.md index 4434aff..37f6ca8 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,16 @@ block! do c * 2 end #=> ** (ErlangError) Erlang error: "Failed: 4" + +# Early return: + +block do + a <- {:ok, 2} + b = a * 2 + :wrong <- b * 2 # Returning 8 here due to wrong match + b * 4 +end +8 ``` ## Related Packages diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex index 38d1d48..3944e73 100644 --- a/lib/exceptional/block.ex +++ b/lib/exceptional/block.ex @@ -40,6 +40,33 @@ defmodule Exceptional.Block do ...> end 16 + iex> use Exceptional.Block + ...> block do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> 8 <- b * 2 # Match's supported on the values + ...> b * 4 + ...> end + 16 + + iex> use Exceptional.Block + ...> block do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> :wrong <- b * 2 # Returning 8 here due to wrong match + ...> b * 4 + ...> end + 8 + + iex> use Exceptional.Block + ...> block do + ...> a <- {:ok, 2} + ...> b = a * 2 + ...> :wrong = b * 2 # Match Exception is raised here + ...> b * 4 + ...> end + ** (MatchError) no match of right hand side value: 8 + iex> use Exceptional.Block ...> block do ...> a <- {:ok, 2} From 3b0caa543f57c905eba98de37a66837cc95ca09f Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:28:23 -0600 Subject: [PATCH 10/11] More doctests to show how things work. --- README.md | 3 ++- lib/exceptional/block.ex | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 37f6ca8..7f38a03 100644 --- a/README.md +++ b/README.md @@ -315,9 +315,10 @@ block conversion_fun: conversion_fun do c <- {:blah, "Failed: #{b}"} c * 2 else + %ErlangError{original: "Blah: "<>_} = exc -> exc _ -> {:error, "unknown error"} end -#=> %ErlangError{original: "unknown error"} +#=> %ErlangError{original: "Blah: Failed: 4"} block! do a <- {:ok, 2} diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex index 3944e73..9aab847 100644 --- a/lib/exceptional/block.ex +++ b/lib/exceptional/block.ex @@ -101,9 +101,10 @@ defmodule Exceptional.Block do ...> c <- {:blah, "Failed: #{b}"} ...> c * 2 ...> else + ...> %ErlangError{original: "Blah: "<>_} = exc -> exc ...> _ -> {:error, "unknown error"} ...> end - %ErlangError{original: "unknown error"} + %ErlangError{original: "Blah: Failed: 4"} """ defmacro block(opts, bodies \\ []) do From fedd89d6263c5b59013983bce06bdb27c355a99d Mon Sep 17 00:00:00 2001 From: OvermindDL1 Date: Fri, 25 Aug 2017 13:31:32 -0600 Subject: [PATCH 11/11] More descriptive docs on `block` --- lib/exceptional/block.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/exceptional/block.ex b/lib/exceptional/block.ex index 9aab847..9c93ce8 100644 --- a/lib/exceptional/block.ex +++ b/lib/exceptional/block.ex @@ -23,6 +23,18 @@ defmodule Exceptional.Block do not something like `{:ok, value}` when using `<-`. `=` is unwrapped and unhandled. + This requires a `do` body, last expression is final returned value. + - Inside the `do` body it accepts `matcher <- expression` and will early + return if it is a bad match or the expression returns an error. + - Values will be unwrapped as in `normalize` + + This accepts an `else` body, which takes cases to handle error conditions + and transform them as necessary. + + This takes a `conversion_fun: some_fun` optional argument to pass in to the + `normalize` call to transform normalization errors into custom values and/or + errors. + ## Examples iex> use Exceptional.Block