call_sheet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +60 -0
- data/LICENSE.md +9 -0
- data/README.md +150 -0
- data/Rakefile +12 -0
- data/lib/call_sheet/dsl.rb +37 -0
- data/lib/call_sheet/step.rb +30 -0
- data/lib/call_sheet/step_adapters/base.rb +19 -0
- data/lib/call_sheet/step_adapters/map.rb +11 -0
- data/lib/call_sheet/step_adapters/raw.rb +11 -0
- data/lib/call_sheet/step_adapters/tee.rb +12 -0
- data/lib/call_sheet/step_adapters/try.rb +11 -0
- data/lib/call_sheet/step_adapters.rb +19 -0
- data/lib/call_sheet/step_failure.rb +16 -0
- data/lib/call_sheet/transaction.rb +52 -0
- data/lib/call_sheet/version.rb +3 -0
- data/lib/call_sheet.rb +8 -0
- data/spec/examples.txt +17 -0
- data/spec/integration/call_sheet_spec.rb +86 -0
- data/spec/integration/inline_procs_spec.rb +40 -0
- data/spec/integration/passing_step_arguments_spec.rb +50 -0
- data/spec/spec_helper.rb +97 -0
- data/spec/support/test_module_constants.rb +11 -0
- metadata +151 -0
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
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,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,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
|
data/lib/call_sheet.rb
ADDED
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
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: []
|