call_sheet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b3c20bf00a8dd774a7a7a9110001576dfae39667
4
+ data.tar.gz: acca5a6e3f2575b54b13c7d3744f466e471ff753
5
+ SHA512:
6
+ metadata.gz: 5442457894d06089e52de190a7ccd62b504e71d7cd66ba4764ee01134c48c2e5cc923b534fbc3caff3d41f3f2368d5a89af373e44393a2f933a6504392ae271b
7
+ data.tar.gz: 05acf6f917f9fbc1102c12bfb54e1a60aa0089d8e810a2a70ddec22d44f31c43b7d4368997fb3180a47fa7312013395606191642ae06888fda808bbde3955583
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Gem dependencies are specified in call_sheet.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,60 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ call_sheet (0.1.0)
5
+ deterministic (>= 0.15.3)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.1.0)
11
+ astrolabe (1.3.1)
12
+ parser (~> 2.2)
13
+ deterministic (0.15.3)
14
+ diff-lcs (1.2.5)
15
+ docile (1.1.5)
16
+ json (1.8.3)
17
+ parser (2.2.3.0)
18
+ ast (>= 1.1, < 3.0)
19
+ powerpack (0.1.1)
20
+ rainbow (2.0.0)
21
+ rake (10.4.2)
22
+ rspec (3.3.0)
23
+ rspec-core (~> 3.3.0)
24
+ rspec-expectations (~> 3.3.0)
25
+ rspec-mocks (~> 3.3.0)
26
+ rspec-core (3.3.2)
27
+ rspec-support (~> 3.3.0)
28
+ rspec-expectations (3.3.1)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.3.0)
31
+ rspec-mocks (3.3.2)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.3.0)
34
+ rspec-support (3.3.0)
35
+ rubocop (0.34.2)
36
+ astrolabe (~> 1.3)
37
+ parser (>= 2.2.2.5, < 3.0)
38
+ powerpack (~> 0.1)
39
+ rainbow (>= 1.99.1, < 3.0)
40
+ ruby-progressbar (~> 1.4)
41
+ ruby-progressbar (1.7.5)
42
+ simplecov (0.10.0)
43
+ docile (~> 1.1.0)
44
+ json (~> 1.8)
45
+ simplecov-html (~> 0.10.0)
46
+ simplecov-html (0.10.0)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ bundler (~> 1.10)
53
+ call_sheet!
54
+ rake (~> 10.4.2)
55
+ rspec (~> 3.3.0)
56
+ rubocop (~> 0.34.2)
57
+ simplecov (~> 0.10.0)
58
+
59
+ BUNDLED WITH
60
+ 1.10.6
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2015 [Icelab](http://icelab.com.au/).
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Call Sheet
2
+
3
+ Call Sheet is a business transaction DSL. It provides a simple way to define a complex business transaction that includes processing by many different objects. It makes error handling a primary concern by using a “[Railway Oriented Programming](http://fsharpforfunandprofit.com/rop/)” approach for capturing and returning errors from any step in the transaction.
4
+
5
+ Call Sheet is based on the following ideas, drawn mostly from [Transflow](http://github.com/solnic/transflow):
6
+
7
+ * A business transaction is a series of operations where each can fail and stop processing.
8
+ * A business transaction resolves its dependencies using an external container object and it doesn’t know any details about the individual operation objects except their identifiers.
9
+ * A business transaction can describe its steps on an abstract level without being coupled to any details about how individual operations work.
10
+ * A business transaction doesn’t have any state.
11
+ * Each operation shouldn’t accumulate state, instead it should receive an input and return an output without causing any side-effects.
12
+ * The only interface of a an operation is `#call(input)`.
13
+ * Each operation provides a meaningful functionality and can be reused.
14
+ * Errors in any operation can be easily caught and handled as part of the normal application flow.
15
+
16
+ ## Why?
17
+
18
+ Requiring a business transaction's steps to exist as independent operations directly addressable voa a container means that they can be tested in isolation and easily reused throughout your application. Following from this, keeping the business transaction to a series of high-level, declarative steps ensures that it's easy to understand at a glance.
19
+
20
+ The output of each step is wrapped in a [Deterministic](https://github.com/pzol/deterministic) `Result` object (either `Success(s)` or `Failure(f)`). This allows the steps to be chained together and ensures that processing stops in the case of a failure. Returning a `Result` from the overall transaction also allows for error handling to remain a primary concern without it getting in the way of tidy, straightforward operation logic. Wrapping the step output also means that you can work with a wide variety of operations within your application – they don’t need to return a `Result` already.
21
+
22
+ ## Usage
23
+
24
+ All you need to use Call Sheet is a container of operations that respond to `#call(input)`. The operations will be resolved from the container via `#[]`. The examples below use a plain Hash for simplicity, but for a larger app you may like to consider something like [dry-container](https://github.com/dryrb/dry-container).
25
+
26
+ Each operation is integrated into your business transaction through one of the following step adapters:
27
+
28
+ * `map` – any output is considered successful and returned as `Success(output)`
29
+ * `try` – the operation may raise an exception in an error case. This is caught and returned as `Failure(exception)`. The output is otherwise returned as `Success(output)`.
30
+ * `tee` – the operation interacts with some external system and has no meaningful output. The original input is passed through and returned as `Success(input)`.
31
+ * `raw` – the operation already returns its own `Result` object, and needs no special handling.
32
+
33
+ ```ruby
34
+ DB = []
35
+
36
+ container = {
37
+ process: -> input { {name: input["name"], email: input["email"]} },
38
+ validate: -> input { input[:email].nil? ? raise("not valid") : input },
39
+ persist: -> input { DB << input and true }
40
+ }
41
+
42
+ save_user = CallSheet(container: container) do
43
+ map :process
44
+ try :validate
45
+ tee :persist
46
+ end
47
+
48
+ save_user.call("name" => "Jane", "email" => "jane@doe.com")
49
+ # => Success({:name=>"Jane", :email=>"jane@doe.com"})
50
+
51
+ DB
52
+ # => [{:name=>"Jane", :email=>"jane@doe.com"}]
53
+ ```
54
+
55
+ Each transaction returns a `Success(s)` or `Failure(f)` result. You can handle these different results with Deterministic’s [pattern matching](https://github.com/pzol/deterministic#pattern-matching):
56
+
57
+ ```ruby
58
+ save_user.call(name: "Jane", email: "jane@doe.com").match do
59
+ Success(s) do
60
+ puts "Succeeded!"
61
+ end
62
+ Failure(f, where { f == :validate }) do |errors|
63
+ # In a more realistic example, you’d loop through a list of messages in `errors`.
64
+ puts "Couldn’t save this user. Please provide an email address."
65
+ end
66
+ Failure(f) do
67
+ puts "Couldn’t save this user."
68
+ end
69
+ end
70
+ ```
71
+
72
+ You can use guard expressions like `where { f == :step_name }` in the failure matches to catch failures that arise from particular steps in your transaction.
73
+
74
+ ### Passing additional step arguments
75
+
76
+ Additional arguments for step operations can be passed at the time of calling your transaction. Provide these arguments as an array, and they’ll be [splatted](https://endofline.wordpress.com/2011/01/21/the-strange-ruby-splat/) into the front of the operation’s arguments. This effectively means that transactions can support operations with any sort of `#call(*args, input)` interface.
77
+
78
+ ```ruby
79
+ DB = []
80
+
81
+ container = {
82
+ process: -> input { {name: input["name"], email: input["email"]} },
83
+ validate: -> allowed, input { input[:email].include?(allowed) ? raise("not allowed") : input },
84
+ persist: -> input { DB << input and true }
85
+ }
86
+
87
+ save_user = CallSheet(container: container) do
88
+ map :process
89
+ try :validate
90
+ tee :persist
91
+ end
92
+
93
+ input = {name: "Jane", email: "jane@doe.com"}
94
+ save_user.call(input, validate: ["doe.com"])
95
+ # => Success({:name=>"Jane", :email=>"jane@doe.com"})
96
+
97
+ save_user.call(input, validate: ["smith.com"])
98
+ # => Failure("not allowed")
99
+ ```
100
+
101
+ ### Working with a larger container
102
+
103
+ In practice, your container won’t be a trivial collection of generically named operations. You can keep your transaction step names simple by using the `with:` option to provide the identifiers for the operations within your container:
104
+
105
+ ```ruby
106
+ save_user = CallSheet(container: large_whole_app_container) do
107
+ map :process, with: "attributes.user"
108
+ try :validate, with: "validations.user"
109
+ tee :persist, with: "persistance.commands.update_user"
110
+ end
111
+ ```
112
+
113
+ ### Using inline procs
114
+
115
+ You can inject small pieces of custom behavior into your transaction using inline procs and a `raw` step. This can be helpful if you want to provide a special failure case based on the output of a previous step.
116
+
117
+ ```ruby
118
+ update_user = CallSheet(container: container) do
119
+ map :find_user
120
+ raw :check_locked, with: -> input { input.locked? ? Failure("Cannot update locked user") : Success(input) }
121
+ try :validate
122
+ tee :persist
123
+ end
124
+ ```
125
+
126
+ A `raw` step can also be used if the operation in your container already returns a `Result` and therefore doesn’t need any special handling.
127
+
128
+ ## Installation
129
+
130
+ Add this line to your application’s `Gemfile`:
131
+
132
+ ```ruby
133
+ gem "call_sheet"
134
+ ```
135
+
136
+ Run `bundle` to install the gem.
137
+
138
+ ## Contributing
139
+
140
+ Bug reports and pull requests are welcome on [GitHub](http://github.com/icelab/call_sheet).
141
+
142
+ ## Credits
143
+
144
+ Call Sheet is developed and maintained by [Icelab](http://icelab.com.au/).
145
+
146
+ Call Sheet’s error handling is based on Scott Wlaschin’s [Railway Oriented Programming](http://fsharpforfunandprofit.com/rop/), found via Zohaib Rauf’s [Railway Oriented Programming in Elixir](http://zohaib.me/railway-programming-pattern-in-elixir/) blog post. Call Sheet’s behavior as a business transaction library draws heavy inspiration from Piotr Solnica’s [Transflow](http://github.com/solnic/transflow) and Gilbert B Garza’s [Solid Use Case](https://github.com/mindeavor/solid_use_case). Piotr Zolnierek’s [Deterministic](https://github.com/pzol/deterministic) gem makes working with functional programming patterns in Ruby fun and easy. Thank you all!
147
+
148
+ ## License
149
+
150
+ Copyright © 2015 [Icelab](http://icelab.com.au/). Call Sheet is free software, and may be redistributed under the terms specified in the [license](LICENSE.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new
5
+
6
+ require "rubocop/rake_task"
7
+ RuboCop::RakeTask.new
8
+
9
+ task default: :ci
10
+
11
+ desc "Run the test suite"
12
+ task ci: %w(rubocop spec)
@@ -0,0 +1,37 @@
1
+ require "call_sheet/step"
2
+ require "call_sheet/step_adapters"
3
+ require "call_sheet/step_adapters/base"
4
+ require "call_sheet/step_adapters/raw"
5
+ require "call_sheet/step_adapters/map"
6
+ require "call_sheet/step_adapters/tee"
7
+ require "call_sheet/step_adapters/try"
8
+ require "call_sheet/transaction"
9
+
10
+ module CallSheet
11
+ class DSL
12
+ include Deterministic::Prelude::Result
13
+
14
+ attr_reader :options # are we actually doing anything with this besides passing the container?
15
+ attr_reader :container
16
+ attr_reader :steps
17
+
18
+ def initialize(options, &block)
19
+ @options = options
20
+ @container = options.fetch(:container)
21
+ @steps = []
22
+
23
+ instance_exec(&block)
24
+ end
25
+
26
+ StepAdapters.each do |adapter_name, adapter_class|
27
+ define_method adapter_name do |step_name, options = {}|
28
+ operation = options[:with].is_a?(Proc) ? options[:with] : container[options.fetch(:with, step_name)]
29
+ steps << Step.new(step_name, adapter_class.new(operation, options))
30
+ end
31
+ end
32
+
33
+ def call
34
+ Transaction.new(steps)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ require "call_sheet/step_failure"
2
+
3
+ module CallSheet
4
+ class Step
5
+ include Deterministic::Prelude::Result
6
+
7
+ attr_reader :step_name
8
+ attr_reader :operation
9
+ attr_reader :call_args
10
+
11
+ def initialize(step_name, operation, call_args = [])
12
+ @step_name = step_name
13
+ @operation = operation
14
+ @call_args = call_args
15
+ end
16
+
17
+ def with_call_args(*call_args)
18
+ self.class.new(step_name, operation, call_args)
19
+ end
20
+
21
+ def call(input)
22
+ result = operation.call(*(call_args << input))
23
+ result.map_err { |v| Failure(StepFailure.new(step_name, v)) }
24
+ end
25
+
26
+ def arity
27
+ operation.arity
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ module CallSheet
2
+ module StepAdapters
3
+ class Base
4
+ include Deterministic::Prelude::Result
5
+
6
+ attr_reader :operation
7
+ attr_reader :options
8
+
9
+ def initialize(operation, options)
10
+ @operation = operation
11
+ @options = options
12
+ end
13
+
14
+ def arity
15
+ operation.is_a?(Proc) ? operation.arity : operation.method(:call).arity
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module CallSheet
2
+ module StepAdapters
3
+ class Map < Base
4
+ def call(*args, input)
5
+ Success(operation.call(input, *args))
6
+ end
7
+ end
8
+
9
+ register :map, Map
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module CallSheet
2
+ module StepAdapters
3
+ class Raw < Base
4
+ def call(*args, input)
5
+ operation.call(input, *args)
6
+ end
7
+ end
8
+
9
+ register :raw, Raw
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module CallSheet
2
+ module StepAdapters
3
+ class Tee < Base
4
+ def call(*args, input)
5
+ operation.call(*args, input)
6
+ Success(input)
7
+ end
8
+ end
9
+
10
+ register :tee, Tee
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module CallSheet
2
+ module StepAdapters
3
+ class Try < Base
4
+ def call(*args, input)
5
+ try! { operation.call(*args, input) }
6
+ end
7
+ end
8
+
9
+ register :try, Try
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ require "forwardable"
2
+
3
+ module CallSheet
4
+ module StepAdapters
5
+ @registry = {}
6
+
7
+ class << self
8
+ attr_reader :registry
9
+ private :registry
10
+
11
+ extend Forwardable
12
+ def_delegators :registry, :[], :each
13
+
14
+ def register(name, klass)
15
+ registry[name.to_sym] = klass
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module CallSheet
2
+ class StepFailure < BasicObject
3
+ def initialize(step_name, object)
4
+ @__step_name = step_name
5
+ @__object = object
6
+ end
7
+
8
+ def method_missing(name, *args, &block)
9
+ @__object.send(name, *args, &block)
10
+ end
11
+
12
+ def ==(other)
13
+ @__step_name == other || @__object == other
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,52 @@
1
+ module CallSheet
2
+ class Transaction
3
+ include Deterministic::Prelude::Result
4
+
5
+ attr_reader :steps
6
+ private :steps
7
+
8
+ def initialize(steps)
9
+ @steps = steps
10
+ end
11
+
12
+ def call(input, options = {})
13
+ assert_valid_options(options)
14
+ assert_options_satisfy_step_arity(options)
15
+
16
+ steps = steps_with_options_applied(options)
17
+ steps.inject(Success(input), :>>)
18
+ end
19
+ alias_method :[], :call
20
+
21
+ private
22
+
23
+ def assert_valid_options(options)
24
+ options.each_key do |step_name|
25
+ unless steps.map(&:step_name).include?(step_name)
26
+ raise ArgumentError, "+#{step_name}+ is not a valid step name"
27
+ end
28
+ end
29
+ end
30
+
31
+ def assert_options_satisfy_step_arity(options)
32
+ steps.each do |step|
33
+ args_required = step.arity >= 0 ? step.arity : ~step.arity
34
+ args_supplied = options.fetch(step.step_name, []).length + 1 # add 1 for main `input`
35
+
36
+ if args_required > args_supplied
37
+ raise ArgumentError, "not enough options for step +#{step.step_name}+"
38
+ end
39
+ end
40
+ end
41
+
42
+ def steps_with_options_applied(options)
43
+ steps.map { |step|
44
+ if (args = options[step.step_name])
45
+ step.with_call_args(*args)
46
+ else
47
+ step
48
+ end
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module CallSheet
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/call_sheet.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "deterministic"
2
+ require "call_sheet/version"
3
+ require "call_sheet/dsl"
4
+
5
+ # rubocop:disable Style/MethodName
6
+ def CallSheet(options = {}, &block)
7
+ CallSheet::DSL.new(options, &block).call
8
+ end
data/spec/examples.txt ADDED
@@ -0,0 +1,17 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------------------------- | ------ | --------------- |
3
+ ./spec/integration/call_sheet_spec.rb[1:1:1] | passed | 0.00285 seconds |
4
+ ./spec/integration/call_sheet_spec.rb[1:1:2] | passed | 0.0025 seconds |
5
+ ./spec/integration/call_sheet_spec.rb[1:1:3] | passed | 0.00373 seconds |
6
+ ./spec/integration/call_sheet_spec.rb[1:1:4] | passed | 0.00084 seconds |
7
+ ./spec/integration/call_sheet_spec.rb[1:2:1] | passed | 0.00061 seconds |
8
+ ./spec/integration/call_sheet_spec.rb[1:2:2] | passed | 0.00084 seconds |
9
+ ./spec/integration/call_sheet_spec.rb[1:2:3] | passed | 0.00176 seconds |
10
+ ./spec/integration/call_sheet_spec.rb[1:2:4] | passed | 0.00068 seconds |
11
+ ./spec/integration/call_sheet_spec.rb[1:2:5] | passed | 0.00063 seconds |
12
+ ./spec/integration/inline_procs_spec.rb[1:1:1] | passed | 0.0004 seconds |
13
+ ./spec/integration/inline_procs_spec.rb[1:2:1] | passed | 0.00042 seconds |
14
+ ./spec/integration/inline_procs_spec.rb[1:2:2] | passed | 0.00045 seconds |
15
+ ./spec/integration/passing_step_arguments_spec.rb[1:1:1] | passed | 0.00065 seconds |
16
+ ./spec/integration/passing_step_arguments_spec.rb[1:2:1] | passed | 0.00216 seconds |
17
+ ./spec/integration/passing_step_arguments_spec.rb[1:3:1] | passed | 0.00023 seconds |
@@ -0,0 +1,86 @@
1
+ RSpec.describe CallSheet do
2
+ let(:call_sheet) {
3
+ CallSheet(container: container) do
4
+ map :process
5
+ try :validate
6
+ tee :persist
7
+ end
8
+ }
9
+
10
+ let(:container) {
11
+ {
12
+ process: -> input { {name: input["name"], email: input["email"]} },
13
+ validate: -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input },
14
+ persist: -> input { Test::DB << input and true }
15
+ }
16
+ }
17
+
18
+ before do
19
+ Test::NotValidError = Class.new(StandardError)
20
+ Test::DB = []
21
+ end
22
+
23
+ context "successful" do
24
+ let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
25
+ let(:run_call_sheet) { call_sheet.call(input) }
26
+
27
+ it "calls the operations" do
28
+ run_call_sheet
29
+ expect(Test::DB).to include(name: "Jane", email: "jane@doe.com")
30
+ end
31
+
32
+ it "returns a success" do
33
+ expect(run_call_sheet).to be_success
34
+ end
35
+
36
+ it "wraps the result of the final operation" do
37
+ expect(run_call_sheet.value).to eq(name: "Jane", email: "jane@doe.com")
38
+ end
39
+
40
+ it "supports pattern matching on success" do
41
+ match = run_call_sheet.match do
42
+ Success(s) { "Matched on success" }
43
+ Failure(_) {}
44
+ end
45
+
46
+ expect(match).to eq "Matched on success"
47
+ end
48
+ end
49
+
50
+ context "failed in a try step" do
51
+ let(:input) { {"name" => "Jane"} }
52
+ let(:run_call_sheet) { call_sheet.call(input) }
53
+
54
+ it "does not run subsequent operations" do
55
+ run_call_sheet
56
+ expect(Test::DB).to be_empty
57
+ end
58
+
59
+ it "returns a failure" do
60
+ expect(run_call_sheet).to be_failure
61
+ end
62
+
63
+ it "wraps the result of the failing operation" do
64
+ expect(run_call_sheet.value).to be_a Test::NotValidError
65
+ end
66
+
67
+ it "supports pattern matching on failure" do
68
+ match = run_call_sheet.match do
69
+ Success(_) {}
70
+ Failure(f) { "Matched on failure" }
71
+ end
72
+
73
+ expect(match).to eq "Matched on failure"
74
+ end
75
+
76
+ it "supports pattern matching on specific step failures" do
77
+ match = run_call_sheet.match do
78
+ Success(_) {}
79
+ Failure(f, where { f == :validate }) { "Matched validate failure" }
80
+ Failure(_) {}
81
+ end
82
+
83
+ expect(match).to eq "Matched validate failure"
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,40 @@
1
+ RSpec.describe "using inline procs with raw steps" do
2
+ let(:call_sheet) {
3
+ CallSheet(container: container) do
4
+ map :process
5
+ raw :validate, with: -> input { input[:email].nil? ? Failure("email required") : Success(input) }
6
+ end
7
+ }
8
+
9
+ let(:container) { {process: -> input { {name: input["name"], email: input["email"]} }} }
10
+
11
+ before do
12
+ Test::NotValidError = Class.new(StandardError)
13
+ Test::DB = []
14
+ end
15
+
16
+ context "inline step returns Success" do
17
+ it "calls all the operations" do
18
+ input = {"name" => "Jane", "email" => "jane@doe.com"}
19
+ expect(call_sheet.call(input)).to be_success
20
+ end
21
+ end
22
+
23
+ context "inline step returns Failure" do
24
+ let(:input) { {"name" => "Jane"} }
25
+
26
+ it "stops running the step operations and returns the failure" do
27
+ expect(call_sheet.call(input)).to be_failure
28
+ end
29
+
30
+ it "supports pattern matching on the failed step name" do
31
+ match = call_sheet.call(input).match do
32
+ Success(_) {}
33
+ Failure(f, where { f == :validate }) { "Matched validate failure" }
34
+ Failure(_) {}
35
+ end
36
+
37
+ expect(match).to eq "Matched validate failure"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,50 @@
1
+ RSpec.describe "Passing additional arguments to step operations" do
2
+ let(:run_call_sheet) { call_sheet.call(input, step_options) }
3
+
4
+ let(:call_sheet) {
5
+ CallSheet(container: container) do
6
+ map :process
7
+ try :validate
8
+ tee :persist
9
+ end
10
+ }
11
+
12
+ let(:container) {
13
+ {
14
+ process: -> input { {name: input["name"], email: input["email"]} },
15
+ validate: -> allowed, input { !input[:email].include?(allowed) ? raise(Test::NotValidError, "email not allowed") : input },
16
+ persist: -> input { Test::DB << input and true }
17
+ }
18
+ }
19
+
20
+ let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
21
+
22
+ before do
23
+ Test::NotValidError = Class.new(StandardError)
24
+ Test::DB = []
25
+ end
26
+
27
+ context "required arguments provided" do
28
+ let(:step_options) { {validate: ["doe.com"]} }
29
+
30
+ it "passes the arguments and calls the operations successfully" do
31
+ expect(run_call_sheet).to be_success
32
+ end
33
+ end
34
+
35
+ context "required arguments not provided" do
36
+ let(:step_options) { {} }
37
+
38
+ it "raises an ArgumentError" do
39
+ expect { run_call_sheet }.to raise_error(ArgumentError)
40
+ end
41
+ end
42
+
43
+ context "spurious arguments provided" do
44
+ let(:step_options) { {validate: ["doe.com"], bogus: ["not matching any step"]} }
45
+
46
+ it "raises an ArgumentError" do
47
+ expect { run_call_sheet }.to raise_error(ArgumentError)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,97 @@
1
+ require "simplecov"
2
+ SimpleCov.start
3
+ SimpleCov.minimum_coverage 100
4
+
5
+ require "call_sheet"
6
+
7
+ # Requires supporting ruby files with custom matchers and macros, etc, in
8
+ # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
9
+ # run as spec files by default. This means that files in spec/support that end
10
+ # in _spec.rb will both be required and run as specs, causing the specs to be
11
+ # run twice. It is recommended that you do not name files matching this glob to
12
+ # end with _spec.rb. You can configure this pattern with the --pattern
13
+ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
14
+ #
15
+ # The following line is provided for convenience purposes. It has the downside
16
+ # of increasing the boot-up time by auto-requiring all files in the support
17
+ # directory. Alternatively, in the individual `*_spec.rb` files, manually
18
+ # require only the support files necessary.
19
+ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each do |f| require f end
20
+
21
+ # This file was generated by the `rspec --init` command. Conventionally, all
22
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
23
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
24
+ # this file to always be loaded, without a need to explicitly require it in any
25
+ # files.
26
+ #
27
+ # Given that it is always loaded, you are encouraged to keep this file as
28
+ # light-weight as possible. Requiring heavyweight dependencies from this file
29
+ # will add to the boot time of your test suite on EVERY test run, even for an
30
+ # individual file that may not need all of that loaded. Instead, consider making
31
+ # a separate helper file that requires the additional dependencies and performs
32
+ # the additional setup, and require it from the spec files that actually need
33
+ # it.
34
+ #
35
+ # The `.rspec` file also contains a few flags that are not defaults but that
36
+ # users commonly want.
37
+ #
38
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
39
+ RSpec.configure do |config|
40
+ # rspec-expectations config goes here. You can use an alternate
41
+ # assertion/expectation library such as wrong or the stdlib/minitest
42
+ # assertions if you prefer.
43
+ config.expect_with :rspec do |expectations|
44
+ # This option will default to `true` in RSpec 4. It makes the `description`
45
+ # and `failure_message` of custom matchers include text for helper methods
46
+ # defined using `chain`, e.g.:
47
+ # be_bigger_than(2).and_smaller_than(4).description
48
+ # # => "be bigger than 2 and smaller than 4"
49
+ # ...rather than:
50
+ # # => "be bigger than 2"
51
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
52
+ end
53
+
54
+ # rspec-mocks config goes here. You can use an alternate test double
55
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
56
+ config.mock_with :rspec do |mocks|
57
+ # Prevents you from mocking or stubbing a method that does not exist on
58
+ # a real object. This is generally recommended, and will default to
59
+ # `true` in RSpec 4.
60
+ mocks.verify_partial_doubles = true
61
+ end
62
+
63
+ # Allows RSpec to persist some state between runs in order to support
64
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
65
+ # you configure your source control system to ignore this file.
66
+ config.example_status_persistence_file_path = "spec/examples.txt"
67
+
68
+ # Limits the available syntax to the non-monkey patched syntax that is
69
+ # recommended.
70
+ config.disable_monkey_patching!
71
+
72
+ # This setting enables warnings. It's recommended, but in some cases may
73
+ # be too noisy due to issues in dependencies.
74
+ config.warnings = true
75
+
76
+ # Many RSpec users commonly either run the entire suite or an individual
77
+ # file, and it's useful to allow more verbose output when running an
78
+ # individual spec file.
79
+ if config.files_to_run.one?
80
+ # Use the documentation formatter for detailed output,
81
+ # unless a formatter has already been configured
82
+ # (e.g. via a command-line flag).
83
+ config.default_formatter = "doc"
84
+ end
85
+
86
+ # Run specs in random order to surface order dependencies. If you find an
87
+ # order dependency and want to debug it, you can fix the order by providing
88
+ # the seed, which is printed after each run.
89
+ # --seed 1234
90
+ config.order = :random
91
+
92
+ # Seed global randomization in this process using the `--seed` CLI option.
93
+ # Setting this allows you to use `--seed` to deterministically reproduce
94
+ # test failures related to randomization by passing the same `--seed` value
95
+ # as the one that triggered the failure.
96
+ Kernel.srand config.seed
97
+ end
@@ -0,0 +1,11 @@
1
+ module Test
2
+ def self.remove_constants
3
+ constants.each(&method(:remove_const))
4
+ end
5
+ end
6
+
7
+ RSpec.configure do |config|
8
+ config.after do
9
+ Test.remove_constants
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: call_sheet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Riley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: deterministic
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.15.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.15.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 10.4.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 10.4.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.3.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.3.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.34.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.34.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.10.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.10.0
97
+ description:
98
+ email:
99
+ - tim@icelab.com.au
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - Gemfile
105
+ - Gemfile.lock
106
+ - LICENSE.md
107
+ - README.md
108
+ - Rakefile
109
+ - lib/call_sheet.rb
110
+ - lib/call_sheet/dsl.rb
111
+ - lib/call_sheet/step.rb
112
+ - lib/call_sheet/step_adapters.rb
113
+ - lib/call_sheet/step_adapters/base.rb
114
+ - lib/call_sheet/step_adapters/map.rb
115
+ - lib/call_sheet/step_adapters/raw.rb
116
+ - lib/call_sheet/step_adapters/tee.rb
117
+ - lib/call_sheet/step_adapters/try.rb
118
+ - lib/call_sheet/step_failure.rb
119
+ - lib/call_sheet/transaction.rb
120
+ - lib/call_sheet/version.rb
121
+ - spec/examples.txt
122
+ - spec/integration/call_sheet_spec.rb
123
+ - spec/integration/inline_procs_spec.rb
124
+ - spec/integration/passing_step_arguments_spec.rb
125
+ - spec/spec_helper.rb
126
+ - spec/support/test_module_constants.rb
127
+ homepage: https://github.com/icelab/call_sheet
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.4.5.1
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: Business Transaction Flow DSL
151
+ test_files: []