Skip to content

Commit

Permalink
Implements skip stage
Browse files Browse the repository at this point in the history
The `:skip` stage makes the Pipeline halts when the given conditional
returns `true`.
  • Loading branch information
rafaels88 authored and zorbash committed Jul 17, 2018
1 parent 236e576 commit a895928
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
check: 2,
link: 1,
link: 2,
skip: 1,
instrument: 2,
instrument: 3,
send: 2
Expand All @@ -25,6 +26,7 @@
check: 2,
link: 1,
link: 2,
skip: 1,
instrument: 2,
instrument: 3,
send: 2,
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The package can be installed by adding `opus` to your list of dependencies in `m

```elixir
def deps do
[{:opus, "~> 0.5"}]
[{:opus, "~> 0.6"}]
end
```

Expand Down Expand Up @@ -42,6 +42,7 @@ end
defmodule ArithmeticPipeline do
use Opus.Pipeline

skip if: :greater_than_fifty?
step :add_one, with: &(&1 + 1)
check :even?, with: &(rem(&1, 2) == 0), error_message: :expected_an_even
tee :publish_number, if: &Publisher.publishable?/1, raise: [ExternalError]
Expand All @@ -52,6 +53,7 @@ defmodule ArithmeticPipeline do
def double(n), do: n * 2
def lucky_number?(n) when n in 42..1337, do: true
def lucky_number?(_), do: false
def greater_than_fifty?(n), do: n > 50
end

ArithmeticPipeline.call(41)
Expand Down Expand Up @@ -112,6 +114,13 @@ This stage is to link with another Opus.Pipeline module. It calls
`call/1` for the provided module. If the module is not an
`Opus.Pipeline` it is ignored.

### Skip

This stage expects a `:if` option only, on which is expected to return a
boolean value. If `true`, then the pipeline halts and Opus returns
`{:ok, :skipped}`. If `false` or any other value is returned (including non-boolean),
then the next stage is called with no side effect.

### Available options

The behaviour of each stage can be configured with any of the available
Expand Down
21 changes: 20 additions & 1 deletion lib/opus/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ defmodule Opus.Pipeline do
alias Opus.PipelineError
alias Opus.Instrumentation
alias Opus.Pipeline.StageFilter
alias Opus.Pipeline.Stage.{Step, Tee, Check, Link}
alias Opus.Pipeline.Stage.{Step, Tee, Check, Link, Skip}
alias __MODULE__, as: Pipeline

import Opus.Instrumentation, only: :macros
Expand Down Expand Up @@ -127,6 +127,7 @@ defmodule Opus.Pipeline do
defp run_stage({module, :tee, name, opts} = stage, input), do: Tee.run(stage, input)
defp run_stage({module, :check, name, opts} = stage, input), do: Check.run(stage, input)
defp run_stage({module, :link, name, opts} = stage, input), do: Link.run(stage, input)
defp run_stage({module, :skip, name, opts} = stage, input), do: Skip.run(stage, input)
end
end

Expand Down Expand Up @@ -230,4 +231,22 @@ defmodule Opus.Pipeline do
@opus_stages {:check, unquote(name), Map.new(options ++ [stage_id: unquote(stage_id)])}
end
end

defmacro skip(name, opts \\ []) do
stage_id = :erlang.unique_integer([:positive])
callbacks = Opus.Pipeline.Registration.maybe_define_callbacks(stage_id, name, opts)

quote do
unquote(callbacks)

options =
Opus.Pipeline.Registration.normalize_opts(
unquote(opts),
unquote(stage_id),
@opus_callbacks
)

@opus_stages {:skip, unquote(name), Map.new(options ++ [stage_id: unquote(stage_id)])}
end
end
end
16 changes: 16 additions & 0 deletions lib/opus/pipeline/stage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ defmodule Opus.Pipeline.Stage do
when is_atom(fun),
do: maybe_run({module, type, name, %{opts | if: {module, fun, [input]}}}, input)

def maybe_run({module, :skip, name, %{if: {_m, _f, _a} = condition}}, input) do
case Safe.apply(condition) do
true ->
module.instrument(:pipeline_skipped, %{stage: %{pipeline: module, name: name}}, %{
stage: name,
input: input
})

# Stop the pipeline execution
:pipeline_skipped

_ ->
nil
end
end

def maybe_run({module, _type, name, %{if: {_m, _f, _a} = condition} = opts} = stage, input) do
case Safe.apply(condition) do
true ->
Expand Down
33 changes: 33 additions & 0 deletions lib/opus/pipeline/stage/skip.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule Opus.Pipeline.Stage.Skip do
@moduledoc ~S"""
The skip stage is meant to halt the pipeline with no error if the given condition is true.
This stage must be called with an `if` option, in order to decide if the pipeline is going to be
halted or not.
When the given conditional is `true`, the pipeline will return {:ok, :skipped} and all the following
steps will be skipped.
```
defmodule CreateUserPipeline do
use Opus.Pipeline
skip if: :user_exists?
step :persist_user
end
```
In this example, if the `user_exists?` implementation returns `true`, then the next step `persist_user`
is not going to be called. If `false` or any other value, then Opus will keep following to the next stages.
"""

alias Opus.Pipeline.Stage

@behaviour Stage

def run({module, type, [if: func], opts}, input) do
case Stage.maybe_run({module, type, nil, opts |> put_in([:if], func)}, input) do
:pipeline_skipped -> {:halt, :skipped}
_ -> {:cont, input}
end
end
end
2 changes: 2 additions & 0 deletions lib/opus/safe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule Opus.Safe do

def apply(term), do: apply(term, %{})

def apply({_m, nil, _a}, _), do: nil

def apply({m, f, a}, opts) do
Kernel.apply(m, f, a)
rescue
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Opus.Mixfile do
def project do
[
app: :opus,
version: "0.5.0",
version: "0.6.0",
elixir: "~> 1.4",
elixirc_paths: elixirc_paths(Mix.env()),
build_embedded: Mix.env() == :prod,
Expand Down
142 changes: 142 additions & 0 deletions test/opus/pipeline/stage/skip_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
defmodule Opus.Pipeline.Stage.SkipTest do
use ExUnit.Case

describe "when the stage returns false and there's no next stage" do
defmodule SingleSkipFalsePipeline do
use Opus.Pipeline

skip if: :should_skip?

def should_skip?(_), do: false
end

setup do
{:ok, %{subject: SingleSkipFalsePipeline}}
end

test "returns a success tuple with the original input", %{subject: subject} do
assert {:ok, 1} = subject.call(1)
end
end

describe "when the stage returns false and there's another next stage" do
defmodule SkipFalsePipeline do
use Opus.Pipeline

skip if: :should_skip?
step :sum_10, with: &(&1 + 10)

def should_skip?(_), do: false
end

setup do
{:ok, %{subject: SkipFalsePipeline}}
end

test "returns a success tuple with the expected final pipeline data", %{subject: subject} do
assert {:ok, 11} = subject.call(1)
end
end

describe "when the stage returns true" do
defmodule SkipTruePipeline do
use Opus.Pipeline

skip if: :should_skip?
step :shouldnt_be_called, with: fn _ -> raise "Shoudn't raise" end

def should_skip?(_), do: true
end

setup do
{:ok, %{subject: SkipTruePipeline}}
end

test "returns a success tuple with :skipped as the second value", %{subject: subject} do
assert {:ok, :skipped} = subject.call(1)
end
end

describe "when more than one skip stage is added to the pipeline and the first skip returns true" do
defmodule MultiSkipFirstTruePipeline do
use Opus.Pipeline

skip if: :hope_it_skips
skip if: :not_gonna_skip
step :shouldnt_be_called, with: fn _ -> raise "Shoudn't raise" end

def hope_it_skips(_), do: true
def not_gonna_skip(_), do: false
end

setup do
{:ok, %{subject: MultiSkipFirstTruePipeline}}
end

test "returns a success tuple with :skipped as the second value", %{subject: subject} do
assert {:ok, :skipped} = subject.call(1)
end
end

describe "when more than one skip stage is added to the pipeline and the second skip returns true" do
defmodule MultiSkipSecondTruePipeline do
use Opus.Pipeline

skip if: :not_gonna_skip
skip if: :hope_it_skips
step :shouldnt_be_called, with: fn _ -> raise "Shoudn't raise" end

def hope_it_skips(_), do: true
def not_gonna_skip(_), do: false
end

setup do
{:ok, %{subject: MultiSkipSecondTruePipeline}}
end

test "returns a success tuple with :skipped as the second value", %{subject: subject} do
assert {:ok, :skipped} = subject.call(1)
end
end

describe "when more than one skip stage is added to the pipeline and all them return false" do
defmodule MultiSkipFalsePipeline do
use Opus.Pipeline

skip if: :not_gonna_skip
skip if: :not_gonna_skip_also
step :sum_10, with: &(&1 + 10)

def not_gonna_skip(_), do: false
def not_gonna_skip_also(_), do: false
end

setup do
{:ok, %{subject: MultiSkipFalsePipeline}}
end

test "returns a success tuple with the expected final pipeline data", %{subject: subject} do
assert {:ok, 11} = subject.call(1)
end
end

describe "when the stage returns anything other than 'true' (boolean) and there's another next stage" do
defmodule SkipFalseNonBooleanPipeline do
use Opus.Pipeline

skip if: :should_skip?
step :sum_10, with: &(&1 + 10)

def should_skip?(_), do: 'anything_else'
end

setup do
{:ok, %{subject: SkipFalseNonBooleanPipeline}}
end

test "returns a success tuple as if the skip stage has returned false and next stage is called",
%{subject: subject} do
assert {:ok, 11} = subject.call(1)
end
end
end

0 comments on commit a895928

Please sign in to comment.