light-service 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +104 -14
- data/lib/light-service.rb +1 -0
- data/lib/light-service/configuration.rb +1 -0
- data/lib/light-service/context.rb +5 -0
- data/lib/light-service/orchestrator.rb +79 -0
- data/lib/light-service/organizer/with_reducer.rb +1 -1
- data/lib/light-service/version.rb +1 -1
- data/light-service.gemspec +1 -1
- data/spec/acceptance/around_each_spec.rb +10 -18
- data/spec/acceptance/orchestrator/context_failure_and_skipping_spec.rb +56 -0
- data/spec/acceptance/orchestrator/execute_spec.rb +29 -0
- data/spec/acceptance/orchestrator/iterate_spec.rb +36 -0
- data/spec/acceptance/orchestrator/organizer_action_combination_spec.rb +39 -0
- data/spec/acceptance/orchestrator/reduce_if_spec.rb +34 -0
- data/spec/acceptance/orchestrator/reduce_until_spec.rb +26 -0
- data/spec/organizer/with_reducer_spec.rb +1 -1
- data/spec/test_doubles.rb +62 -3
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f66d801de921ff601005501d2aec87d3943138fc
|
4
|
+
data.tar.gz: 6c9ea24dac7c7500e05d5b99ca5898bcfe97fa7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5607646dacdc11ac1201ed7a6b5be6f963a43b3838263edc1909db4a032e92a051f3555ec71ea3006084d211c9b2dd8f12fc755ff9fc0889689ef9a9a7c68152
|
7
|
+
data.tar.gz: 0f42791e151e88a49a0018705c78ea1e1471df2f57f13c2fad886a25a28cc4bfa9126618a1aeee5df2760d0574a29655dd926d84d29edff4a5bab742b4993970
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
![LightService](
|
1
|
+
![LightService](resources/light-service.png)
|
2
2
|
|
3
3
|
[![Gem Version](https://img.shields.io/gem/v/light-service.svg)](https://rubygems.org/gems/light-service)
|
4
4
|
[![Build Status](https://secure.travis-ci.org/adomokos/light-service.png)](http://travis-ci.org/adomokos/light-service)
|
@@ -36,7 +36,7 @@ end
|
|
36
36
|
```
|
37
37
|
|
38
38
|
This controller violates [SRP](http://en.wikipedia.org/wiki/Single_responsibility_principle) all over.
|
39
|
-
Also, imagine what would
|
39
|
+
Also, imagine what it would take to test this beast.
|
40
40
|
You could move the tax_percentage finders and calculations into the tax model,
|
41
41
|
but then you'll make your model logic heavy.
|
42
42
|
|
@@ -50,19 +50,19 @@ Wouldn't it be nice to see this instead?
|
|
50
50
|
|
51
51
|
```ruby
|
52
52
|
(
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
LooksUpTaxPercentage,
|
54
|
+
CalculatesOrderTax,
|
55
|
+
ChecksFreeShipping
|
56
56
|
)
|
57
57
|
```
|
58
58
|
|
59
59
|
This block of code should tell you the "story" of what's going on in this workflow.
|
60
60
|
With the help of LightService you can write code this way. First you need an organizer object that sets up the actions in order
|
61
|
-
and executes them one-by-one. Then you need to create the actions
|
61
|
+
and executes them one-by-one. Then you need to create the actions with one method (that will do only one thing).
|
62
62
|
|
63
|
-
This is how the organizer and actions interact with
|
63
|
+
This is how the organizer and actions interact with each other:
|
64
64
|
|
65
|
-
![LightService](
|
65
|
+
![LightService](resources/organizer_and_actions.png)
|
66
66
|
|
67
67
|
```ruby
|
68
68
|
class CalculatesTax
|
@@ -157,6 +157,7 @@ simple and elegant Rails code where I told the story of how LightService was ext
|
|
157
157
|
* [Error Codes](#error-codes)
|
158
158
|
* [Action Rollback](#action-rollback)
|
159
159
|
* [Localizing Messages](#localizing-messages)
|
160
|
+
* [Orchestrators](#orchestrators)
|
160
161
|
|
161
162
|
## Stopping the Series of Actions
|
162
163
|
When nothing unexpected happens during the organizer's call, the returned `context` will be successful. Here is how you can check for this:
|
@@ -192,7 +193,7 @@ class SubmitsOrderAction
|
|
192
193
|
expects :order, :mailer
|
193
194
|
|
194
195
|
executed do |context|
|
195
|
-
unless context.order.
|
196
|
+
unless context.order.submit_order_successful?
|
196
197
|
context.fail!("Failed to submit the order")
|
197
198
|
next context
|
198
199
|
end
|
@@ -201,7 +202,7 @@ class SubmitsOrderAction
|
|
201
202
|
end
|
202
203
|
end
|
203
204
|
```
|
204
|
-
![LightService](
|
205
|
+
![LightService](resources/fail_actions.png)
|
205
206
|
|
206
207
|
In the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd had a failure, that pushed the context into a failure state and the 4th action was skipped.
|
207
208
|
|
@@ -221,7 +222,7 @@ class ChecksOrderStatusAction
|
|
221
222
|
end
|
222
223
|
end
|
223
224
|
```
|
224
|
-
![LightService](
|
225
|
+
![LightService](resources/skip_actions.png)
|
225
226
|
|
226
227
|
In the example above the organizer called 4 actions. The first 2 actions got executed successfully. The 3rd decided to skip the rest, the 4th action was not invoked. The context was successful.
|
227
228
|
|
@@ -316,7 +317,7 @@ class FooAction
|
|
316
317
|
end
|
317
318
|
```
|
318
319
|
|
319
|
-
Take a look at [this spec](
|
320
|
+
Take a look at [this spec](spec/action_expects_and_promises_spec.rb) to see the refactoring in action.
|
320
321
|
|
321
322
|
## Key Aliases
|
322
323
|
The `aliases` macro sets up pairs of keys and aliases in an organizer. Actions can access the context using the aliases.
|
@@ -329,7 +330,7 @@ Say for example you have actions `AnAction` and `AnotherAction` that you've used
|
|
329
330
|
class AnOrganizer
|
330
331
|
extend LightService::Organizer
|
331
332
|
|
332
|
-
aliases my_key
|
333
|
+
aliases :my_key => :key_alias
|
333
334
|
|
334
335
|
def self.call(order)
|
335
336
|
with(:order => order).reduce(
|
@@ -492,7 +493,7 @@ Using the `rolled_back` macro is optional for the actions in the chain. You shou
|
|
492
493
|
|
493
494
|
The actions are rolled back in reversed order from the point of failure starting with the action that triggered it.
|
494
495
|
|
495
|
-
See [this](
|
496
|
+
See [this](spec/acceptance/rollback_spec.rb) acceptance test to learn more about this functionality.
|
496
497
|
|
497
498
|
## Localizing Messages
|
498
499
|
By default LightService provides a mechanism for easily translating your error or success messages via I18n. You can also provide your own custom localization adapter if your application's logic is more complex than what is shown here.
|
@@ -571,7 +572,96 @@ end
|
|
571
572
|
|
572
573
|
To get the value of a `fail!` or `succeed!` message, simply call `#message` on the returned context.
|
573
574
|
|
575
|
+
## Orchestrators
|
576
|
+
|
577
|
+
The Organizer - Action combination works really well for simple use cases. However, as business logic gets more complex, or when LightService is used in an ETL workflow, the code that routes the different organizers becomes very complex and imperative. Let's look at a piece of code that does basic data transformations:
|
578
|
+
|
579
|
+
```ruby
|
580
|
+
class ExtractsTransformsLoadsData
|
581
|
+
def self.run(connection)
|
582
|
+
context = RetrievesConnectionInfo.call(connection)
|
583
|
+
context = PullsDataFromRemoteApi.call(context)
|
584
|
+
|
585
|
+
retrieved_items = context.retrieved_items
|
586
|
+
if retrieved_items.empty?
|
587
|
+
NotifiesEngineeringTeamAction.execute(context)
|
588
|
+
end
|
589
|
+
|
590
|
+
retrieved_items.each do |item|
|
591
|
+
context[:item] = item
|
592
|
+
TransformsData.call(context)
|
593
|
+
end
|
594
|
+
|
595
|
+
context = LoadsData.call(context)
|
596
|
+
|
597
|
+
SendsNotifications.call(context)
|
598
|
+
end
|
599
|
+
end
|
600
|
+
```
|
601
|
+
|
602
|
+
The `LightService::Context` is initialized with the first action, that context is passed around among organizers and actions. This code is still simpler than many out there, but it feels very imperative: it has conditionals, iterators in it. Let's see how we could make it a bit more simpler with a declarative style:
|
603
|
+
|
604
|
+
```ruby
|
605
|
+
class ExtractsTransformsLoadsData
|
606
|
+
extend LightService::Orchestrator
|
607
|
+
|
608
|
+
def self.run(connection)
|
609
|
+
with(:connection => connection).reduce(steps)
|
610
|
+
end
|
611
|
+
|
612
|
+
def self.steps
|
613
|
+
[
|
614
|
+
RetrievesConnectionInfo,
|
615
|
+
PullsDataFromRemoteApi,
|
616
|
+
reduce_if(->(ctx) { ctx.retrieved_items.empty? }, [
|
617
|
+
NotifiesEngineeringTeamAction
|
618
|
+
]),
|
619
|
+
iterate(:retrieved_item, [
|
620
|
+
TransformsData
|
621
|
+
]),
|
622
|
+
LoadsData,
|
623
|
+
SendsNotifications
|
624
|
+
]
|
625
|
+
end
|
626
|
+
end
|
627
|
+
```
|
628
|
+
|
629
|
+
This code is much easier to reason about, it's less noisy and it captures the goal of LightService well: simple, declarative code that's easy to understand.
|
630
|
+
|
631
|
+
Our convention for naming the public methods on the different items at different levels is this:
|
632
|
+
```
|
633
|
+
Orchestrators
|
634
|
+
|-> run - steps
|
635
|
+
Organizers
|
636
|
+
|-> call - actions
|
637
|
+
Actions
|
638
|
+
|-> execute
|
639
|
+
```
|
640
|
+
|
641
|
+
You can mix organizers with actions in the orchestrator steps, but mixing other organizers with actions in an organizer is discouraged for the sake of simplicity.
|
642
|
+
|
643
|
+
The 5 different constructs an orchestrator can have:
|
644
|
+
|
645
|
+
1. `reduce`
|
646
|
+
2. `reduce_until`
|
647
|
+
3. `reduce_if`
|
648
|
+
4. `iterate`
|
649
|
+
5. `execute`
|
650
|
+
|
651
|
+
The `reduce` method needs no interaction, it behaves similarly to organizers' `reduce` method.
|
652
|
+
|
653
|
+
`reduce_until` behaves like a while loop in imperative languages, it iterates until the provided predicate in the lambda evaluates to true. Take a look at [this acceptance test](spec/acceptance/orchestrator/reduce_until_spec.rb) to see how it's used.
|
654
|
+
|
655
|
+
`reduce_if` will reduce the included organizers and/or actions if the predicate in the labmda evaulates to true. [This acceptance test](spec/acceptance/orchestrator/reduce_if_spec.rb) describes this functionality.
|
656
|
+
|
657
|
+
`iterate` gives your iteration logic, the symbol you define there has to be in the context as a key. For example. to iterate over items you will use `iterate(:items)` in your steps, the context needs to have `items` as a key, otherwise it will fail. The orchestrator will singularize the collection name and will put the actual item into the context under that name. Remaining with the example above, each element will be accessible by the name `item` for the actions in the `iterate` steps. [This acceptance test](spec/acceptance/orchestrator/iterate_spec.rb) should provide you with an example.
|
658
|
+
|
659
|
+
To take advantage of another organizer or action, you might need to tweak the context a bit. Let's say you have a hash, and you need to iterate over its values in a series of action. To alter the context and have the values assigned into a variable, you need to create a new action with 1 line of code in it. That seems a lot of seremony for a simple change. You can do that in a `execute` method like this `execute(->(ctx) { ctx[:some_values] = ctx.some_hash.values })`. [This test](spec/acceptance/orchestrator/execute_spec.rb) describes how you can use it.
|
660
|
+
|
661
|
+
** Thanks to [@bwvoss](https://github.com/bwvoss) for writing most of the Orchestrators code, I only ported his changes to LS and submitted the PR.
|
662
|
+
|
574
663
|
## Requirements
|
664
|
+
|
575
665
|
This gem requires ruby 2.x
|
576
666
|
|
577
667
|
## Installation
|
data/lib/light-service.rb
CHANGED
@@ -0,0 +1,79 @@
|
|
1
|
+
module LightService
|
2
|
+
module Orchestrator
|
3
|
+
def self.extended(base_class)
|
4
|
+
base_class.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def with(data = {})
|
9
|
+
@context = LightService::Context.make(data)
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def reduce(steps, context = @context)
|
14
|
+
steps.each_with_object(context) do |step, ctx|
|
15
|
+
if step.respond_to?(:execute)
|
16
|
+
step.execute(ctx)
|
17
|
+
elsif step.respond_to?(:call)
|
18
|
+
step.call(ctx)
|
19
|
+
else
|
20
|
+
raise 'Pass either an action or organizer'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def reduce_until(condition_block, steps)
|
26
|
+
lambda do |ctx|
|
27
|
+
loop do
|
28
|
+
ctx = scoped_reduction(ctx, steps)
|
29
|
+
break if condition_block.call(ctx) || ctx.failure?
|
30
|
+
end
|
31
|
+
|
32
|
+
ctx
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def reduce_if(condition_block, steps)
|
37
|
+
lambda do |ctx|
|
38
|
+
ctx = scoped_reduction(ctx, steps) if condition_block.call(ctx)
|
39
|
+
ctx
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def execute(code_block)
|
44
|
+
lambda do |ctx|
|
45
|
+
ctx = code_block.call(ctx)
|
46
|
+
ctx
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def iterate(collection_key, steps)
|
51
|
+
lambda do |ctx|
|
52
|
+
collection = ctx[collection_key]
|
53
|
+
item_key = collection_key.to_s.singularize.to_sym
|
54
|
+
collection.each do |item|
|
55
|
+
ctx[item_key] = item
|
56
|
+
ctx = scoped_reduction(ctx, steps)
|
57
|
+
end
|
58
|
+
|
59
|
+
ctx
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def scoped_reduction(ctx, steps)
|
66
|
+
ctx.reset_skip_all! unless ctx.failure?
|
67
|
+
ctx =
|
68
|
+
if steps.is_a?(Array)
|
69
|
+
reduce(steps, ctx)
|
70
|
+
else
|
71
|
+
reduce([steps], ctx)
|
72
|
+
end
|
73
|
+
ctx.reset_skip_all! unless ctx.failure?
|
74
|
+
|
75
|
+
ctx
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -48,7 +48,7 @@ module LightService
|
|
48
48
|
def invoke_action(current_context, action)
|
49
49
|
return action.execute(current_context) unless around_each_handler
|
50
50
|
|
51
|
-
around_each_handler.call(
|
51
|
+
around_each_handler.call(current_context) do
|
52
52
|
action.execute(current_context)
|
53
53
|
end
|
54
54
|
end
|
data/light-service.gemspec
CHANGED
@@ -20,6 +20,6 @@ Gem::Specification.new do |gem|
|
|
20
20
|
|
21
21
|
gem.add_development_dependency("rspec", "~> 3.0")
|
22
22
|
gem.add_development_dependency("simplecov", "~> 0.11")
|
23
|
-
gem.add_development_dependency("rubocop", "~> 0.
|
23
|
+
gem.add_development_dependency("rubocop", "~> 0.42")
|
24
24
|
gem.add_development_dependency("pry", "~> 0.10")
|
25
25
|
end
|
@@ -1,27 +1,19 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'test_doubles'
|
3
3
|
|
4
|
-
describe
|
5
|
-
|
6
|
-
|
7
|
-
.with(TestDoubles::AddsTwoActionWithFetch, :number => 0)
|
8
|
-
end
|
9
|
-
|
10
|
-
def assert_after_action_execute_log
|
11
|
-
expect(MyLogger).to receive(:info)
|
12
|
-
.with(TestDoubles::AddsTwoActionWithFetch, :number => 2)
|
13
|
-
end
|
14
|
-
|
15
|
-
it "can be used to log data" do
|
16
|
-
MyLogger = double
|
17
|
-
context = { :number => 0 }
|
18
|
-
|
19
|
-
assert_before_action_execute_log
|
20
|
-
assert_after_action_execute_log
|
4
|
+
describe 'Executing arbitrary code around each action' do
|
5
|
+
it 'can be used to log data' do
|
6
|
+
context = { :number => 0, :logger => TestDoubles::TestLogger.new }
|
21
7
|
|
22
8
|
result = TestDoubles::AroundEachOrganizer.call(context)
|
23
9
|
|
24
10
|
expect(result.fetch(:number)).to eq(2)
|
11
|
+
expect(result[:logger].logs).to eq(
|
12
|
+
[{
|
13
|
+
:action => TestDoubles::AddsTwoActionWithFetch,
|
14
|
+
:before => 0,
|
15
|
+
:after => 2
|
16
|
+
}]
|
17
|
+
)
|
25
18
|
end
|
26
19
|
end
|
27
|
-
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'test_doubles'
|
3
|
+
|
4
|
+
describe LightService::Orchestrator do
|
5
|
+
class TestSkipState
|
6
|
+
extend LightService::Orchestrator
|
7
|
+
def self.run_skip_before
|
8
|
+
with(:number => 1).reduce([
|
9
|
+
TestDoubles::SkipAllAction,
|
10
|
+
reduce_until(->(ctx) { ctx.number == 3 },
|
11
|
+
TestDoubles::AddOneAction)
|
12
|
+
])
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.run_skip_after
|
16
|
+
with(:number => 1).reduce([
|
17
|
+
TestDoubles::SkipAllAction,
|
18
|
+
reduce_until(->(ctx) { ctx.number == 3 }, [
|
19
|
+
TestDoubles::AddOneAction,
|
20
|
+
TestDoubles::SkipAllAction
|
21
|
+
]),
|
22
|
+
TestDoubles::AddOneAction
|
23
|
+
])
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.run_failure
|
27
|
+
with(:number => 1).reduce([
|
28
|
+
TestDoubles::FailureAction,
|
29
|
+
reduce_until(->(ctx) { ctx[:number] == 3 },
|
30
|
+
TestDoubles::AddOneAction),
|
31
|
+
TestDoubles::AddOneAction
|
32
|
+
])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'does not skip nested contexts' do
|
37
|
+
result = TestSkipState.run_skip_before
|
38
|
+
|
39
|
+
expect(result).to be_success
|
40
|
+
expect(result.number).to eq(3)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'does not skip after a nested context' do
|
44
|
+
result = TestSkipState.run_skip_after
|
45
|
+
|
46
|
+
expect(result).to be_success
|
47
|
+
expect(result.number).to eq(4)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'respects failure across all nestings' do
|
51
|
+
result = TestSkipState.run_failure
|
52
|
+
|
53
|
+
expect(result).to be_failure
|
54
|
+
expect(result[:number]).to eq(1)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'test_doubles'
|
3
|
+
|
4
|
+
describe LightService::Orchestrator do
|
5
|
+
class TestExecute
|
6
|
+
extend LightService::Orchestrator
|
7
|
+
|
8
|
+
def self.run(context)
|
9
|
+
with(context).reduce(steps)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.steps
|
13
|
+
[
|
14
|
+
TestDoubles::AddOneAction,
|
15
|
+
execute(->(ctx) { ctx.number += 1 }),
|
16
|
+
execute(->(ctx) { ctx[:something] = 'hello' }),
|
17
|
+
TestDoubles::AddOneAction
|
18
|
+
]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'calls the lambda in the execute block using the context' do
|
23
|
+
result = TestExecute.run(:number => 0)
|
24
|
+
|
25
|
+
expect(result).to be_success
|
26
|
+
expect(result.number).to eq(3)
|
27
|
+
expect(result[:something]).to eq('hello')
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'test_doubles'
|
3
|
+
|
4
|
+
describe LightService::Orchestrator do
|
5
|
+
class TestIterate
|
6
|
+
extend LightService::Orchestrator
|
7
|
+
|
8
|
+
def self.run(context)
|
9
|
+
with(context).reduce([
|
10
|
+
iterate(:numbers, [
|
11
|
+
TestDoubles::AddOneAction
|
12
|
+
])
|
13
|
+
])
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.run_single(context)
|
17
|
+
with(context).reduce([
|
18
|
+
iterate(:numbers, TestDoubles::AddOneAction)
|
19
|
+
])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'reduces each item of a collection and singularizes the collection key' do
|
24
|
+
result = TestIterate.run(:numbers => [1, 2, 3, 4])
|
25
|
+
|
26
|
+
expect(result).to be_success
|
27
|
+
expect(result.number).to eq(5)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'accepts a single action or organizer' do
|
31
|
+
result = TestIterate.run_single(:numbers => [1, 2, 3, 4])
|
32
|
+
|
33
|
+
expect(result).to be_success
|
34
|
+
expect(result.number).to eq(5)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'test_doubles'
|
3
|
+
|
4
|
+
describe LightService::Orchestrator do
|
5
|
+
class TestReduce
|
6
|
+
extend LightService::Orchestrator
|
7
|
+
|
8
|
+
def self.run(context, steps)
|
9
|
+
with(context).reduce(steps)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'responds to both actions and organizers' do
|
14
|
+
result = TestReduce.run({ :number => 0 }, [
|
15
|
+
TestDoubles::AddTwoOrganizer,
|
16
|
+
TestDoubles::AddOneAction
|
17
|
+
])
|
18
|
+
|
19
|
+
expect(result).to be_success
|
20
|
+
expect(result.number).to eq(3)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'fails fast by skipping proceeding actions/organizers after failure' do
|
24
|
+
result = TestReduce.run({ :number => 0 }, [
|
25
|
+
TestDoubles::AddTwoOrganizer,
|
26
|
+
TestDoubles::FailureAction,
|
27
|
+
TestDoubles::AddOneAction
|
28
|
+
])
|
29
|
+
|
30
|
+
expect(result).not_to be_success
|
31
|
+
expect(result.number).to eq(2)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'does not allow anything but actions and organizers' do
|
35
|
+
expect do
|
36
|
+
TestReduce.run({}, [double])
|
37
|
+
end.to raise_error(RuntimeError)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'test_doubles'
|
3
|
+
|
4
|
+
describe LightService::Orchestrator do
|
5
|
+
class TestReduceIf
|
6
|
+
extend LightService::Orchestrator
|
7
|
+
|
8
|
+
def self.run(context)
|
9
|
+
with(context).reduce(steps)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.steps
|
13
|
+
[
|
14
|
+
TestDoubles::AddOneAction,
|
15
|
+
reduce_if(->(ctx) { ctx.number == 1 },
|
16
|
+
TestDoubles::AddOneAction)
|
17
|
+
]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'reduces if the block evaluates to true' do
|
22
|
+
result = TestReduceIf.run(:number => 0)
|
23
|
+
|
24
|
+
expect(result).to be_success
|
25
|
+
expect(result.number).to eq(2)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'does not reduce if the block evaluates to false' do
|
29
|
+
result = TestReduceIf.run(:number => 2)
|
30
|
+
|
31
|
+
expect(result).to be_success
|
32
|
+
expect(result.number).to eq(3)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'test_doubles'
|
3
|
+
|
4
|
+
RSpec.describe LightService::Orchestrator do
|
5
|
+
class TestReduceUntil
|
6
|
+
extend LightService::Orchestrator
|
7
|
+
|
8
|
+
def self.run
|
9
|
+
with(:number => 1).reduce(steps)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.steps
|
13
|
+
[
|
14
|
+
reduce_until(->(ctx) { ctx.number == 3 },
|
15
|
+
TestDoubles::AddOneAction)
|
16
|
+
]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'reduces until the block evaluates to true' do
|
21
|
+
result = TestReduceUntil.run
|
22
|
+
|
23
|
+
expect(result).to be_success
|
24
|
+
expect(result.number).to eq(3)
|
25
|
+
end
|
26
|
+
end
|
@@ -22,7 +22,7 @@ describe LightService::Organizer::WithReducer do
|
|
22
22
|
it "executes a handler around each action and continues reducing" do
|
23
23
|
expect(action1).to receive(:execute).with(context).and_return(context)
|
24
24
|
expect(TestDoubles::AroundEachNullHandler).to receive(:call)
|
25
|
-
.with(
|
25
|
+
.with(context).and_yield
|
26
26
|
|
27
27
|
result = described_class.new.with(context)
|
28
28
|
.around_each(TestDoubles::AroundEachNullHandler)
|
data/spec/test_doubles.rb
CHANGED
@@ -1,17 +1,76 @@
|
|
1
1
|
# A collection of Action and Organizer dummies used in specs
|
2
2
|
|
3
3
|
module TestDoubles
|
4
|
+
class RollbackAction
|
5
|
+
extend LightService::Action
|
6
|
+
executed(&:fail_with_rollback!)
|
7
|
+
end
|
8
|
+
|
9
|
+
class RaiseErrorAction
|
10
|
+
extend LightService::Action
|
11
|
+
executed do |_ctx|
|
12
|
+
raise 'A problem has occured.'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class RaiseAnotherErrorAction
|
17
|
+
extend LightService::Action
|
18
|
+
executed do |_ctx|
|
19
|
+
raise 'More problems'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class SkipAllAction
|
24
|
+
extend LightService::Action
|
25
|
+
executed(&:skip_all!)
|
26
|
+
end
|
27
|
+
|
28
|
+
class FailureAction
|
29
|
+
extend LightService::Action
|
30
|
+
executed(&:fail!)
|
31
|
+
end
|
32
|
+
|
33
|
+
class AddOneAction
|
34
|
+
extend LightService::Action
|
35
|
+
expects :number
|
36
|
+
promises :number
|
37
|
+
|
38
|
+
executed do |ctx|
|
39
|
+
ctx.number += 1
|
40
|
+
ctx.message = 'Added 1'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class AddTwoOrganizer
|
45
|
+
extend LightService::Organizer
|
46
|
+
def self.call(context)
|
47
|
+
with(context).reduce([AddOneAction, AddOneAction])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
4
51
|
class AroundEachNullHandler
|
5
52
|
def self.call(_action, _context)
|
6
53
|
yield
|
7
54
|
end
|
8
55
|
end
|
9
56
|
|
57
|
+
class TestLogger
|
58
|
+
attr_accessor :logs
|
59
|
+
def initialize
|
60
|
+
@logs = []
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
10
64
|
class AroundEachLoggerHandler
|
11
|
-
def self.call(
|
12
|
-
|
65
|
+
def self.call(context)
|
66
|
+
before_number = context[:number]
|
13
67
|
result = yield
|
14
|
-
|
68
|
+
|
69
|
+
context[:logger].logs << {
|
70
|
+
:action => context.current_action,
|
71
|
+
:before => before_number,
|
72
|
+
:after => result[:number]
|
73
|
+
}
|
15
74
|
|
16
75
|
result
|
17
76
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: light-service
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Attila Domokos
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0.
|
61
|
+
version: '0.42'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0.
|
68
|
+
version: '0.42'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pry
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,6 +111,7 @@ files:
|
|
111
111
|
- lib/light-service/context/key_verifier.rb
|
112
112
|
- lib/light-service/errors.rb
|
113
113
|
- lib/light-service/localization_adapter.rb
|
114
|
+
- lib/light-service/orchestrator.rb
|
114
115
|
- lib/light-service/organizer.rb
|
115
116
|
- lib/light-service/organizer/with_reducer.rb
|
116
117
|
- lib/light-service/organizer/with_reducer_factory.rb
|
@@ -127,6 +128,12 @@ files:
|
|
127
128
|
- spec/acceptance/log_from_organizer_spec.rb
|
128
129
|
- spec/acceptance/message_localization_spec.rb
|
129
130
|
- spec/acceptance/not_having_call_method_warning_spec.rb
|
131
|
+
- spec/acceptance/orchestrator/context_failure_and_skipping_spec.rb
|
132
|
+
- spec/acceptance/orchestrator/execute_spec.rb
|
133
|
+
- spec/acceptance/orchestrator/iterate_spec.rb
|
134
|
+
- spec/acceptance/orchestrator/organizer_action_combination_spec.rb
|
135
|
+
- spec/acceptance/orchestrator/reduce_if_spec.rb
|
136
|
+
- spec/acceptance/orchestrator/reduce_until_spec.rb
|
130
137
|
- spec/acceptance/rollback_spec.rb
|
131
138
|
- spec/action_expected_keys_spec.rb
|
132
139
|
- spec/action_expects_and_promises_spec.rb
|
@@ -178,6 +185,12 @@ test_files:
|
|
178
185
|
- spec/acceptance/log_from_organizer_spec.rb
|
179
186
|
- spec/acceptance/message_localization_spec.rb
|
180
187
|
- spec/acceptance/not_having_call_method_warning_spec.rb
|
188
|
+
- spec/acceptance/orchestrator/context_failure_and_skipping_spec.rb
|
189
|
+
- spec/acceptance/orchestrator/execute_spec.rb
|
190
|
+
- spec/acceptance/orchestrator/iterate_spec.rb
|
191
|
+
- spec/acceptance/orchestrator/organizer_action_combination_spec.rb
|
192
|
+
- spec/acceptance/orchestrator/reduce_if_spec.rb
|
193
|
+
- spec/acceptance/orchestrator/reduce_until_spec.rb
|
181
194
|
- spec/acceptance/rollback_spec.rb
|
182
195
|
- spec/action_expected_keys_spec.rb
|
183
196
|
- spec/action_expects_and_promises_spec.rb
|