light-service 0.8.4 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -2
  3. data/README.md +149 -52
  4. data/RELEASES.md +6 -0
  5. data/gemfiles/activesupport_3.gemfile +1 -1
  6. data/gemfiles/activesupport_4.gemfile +1 -1
  7. data/gemfiles/activesupport_5.gemfile +1 -1
  8. data/lib/light-service.rb +7 -0
  9. data/lib/light-service/action.rb +22 -2
  10. data/lib/light-service/orchestrator.rb +21 -3
  11. data/lib/light-service/organizer.rb +42 -20
  12. data/lib/light-service/organizer/execute.rb +14 -0
  13. data/lib/light-service/organizer/iterate.rb +22 -0
  14. data/lib/light-service/organizer/reduce_if.rb +17 -0
  15. data/lib/light-service/organizer/reduce_until.rb +20 -0
  16. data/lib/light-service/organizer/scoped_reducable.rb +13 -0
  17. data/lib/light-service/organizer/verify_call_method_exists.rb +28 -0
  18. data/lib/light-service/organizer/with_callback.rb +26 -0
  19. data/lib/light-service/organizer/with_reducer.rb +14 -4
  20. data/lib/light-service/organizer/with_reducer_factory.rb +2 -0
  21. data/lib/light-service/version.rb +1 -1
  22. data/light-service.gemspec +2 -2
  23. data/resources/orchestrators_deprecated.svg +10 -0
  24. data/spec/acceptance/add_numbers_spec.rb +3 -3
  25. data/spec/acceptance/after_actions_spec.rb +67 -0
  26. data/spec/acceptance/before_actions_spec.rb +109 -0
  27. data/spec/acceptance/orchestrator/context_failure_and_skipping_spec.rb +12 -10
  28. data/spec/acceptance/orchestrator/execute_spec.rb +8 -6
  29. data/spec/acceptance/orchestrator/iterate_spec.rb +9 -7
  30. data/spec/acceptance/orchestrator/organizer_action_combination_spec.rb +13 -11
  31. data/spec/acceptance/orchestrator/reduce_if_spec.rb +9 -7
  32. data/spec/acceptance/orchestrator/reduce_until_spec.rb +7 -5
  33. data/spec/acceptance/orchestrator/with_callback_spec.rb +8 -6
  34. data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
  35. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +65 -0
  36. data/spec/acceptance/organizer/execute_spec.rb +46 -0
  37. data/spec/acceptance/organizer/iterate_spec.rb +37 -0
  38. data/spec/acceptance/organizer/reduce_if_spec.rb +51 -0
  39. data/spec/acceptance/organizer/reduce_until_spec.rb +43 -0
  40. data/spec/acceptance/organizer/with_callback_spec.rb +110 -0
  41. data/spec/organizer/with_reducer_spec.rb +2 -7
  42. data/spec/sample/calculates_tax_spec.rb +1 -4
  43. data/spec/spec_helper.rb +12 -0
  44. data/spec/support.rb +9 -0
  45. data/spec/test_doubles.rb +84 -17
  46. metadata +34 -6
@@ -17,8 +17,19 @@ module LightService
17
17
  # In case this module is included
18
18
  module ClassMethods
19
19
  def with(data = {})
20
- VerifyCallMethodExists.call(self, caller(1..1).first)
20
+ VerifyCallMethodExists.run(self, caller(1..1).first)
21
21
  data[:_aliases] = @aliases if @aliases
22
+
23
+ if @before_actions
24
+ data[:_before_actions] = @before_actions.dup
25
+ @before_actions = nil
26
+ end
27
+
28
+ if @after_actions
29
+ data[:_after_actions] = @after_actions.dup
30
+ @after_actions = nil
31
+ end
32
+
22
33
  WithReducerFactory.make(self).with(data)
23
34
  end
24
35
 
@@ -26,29 +37,24 @@ module LightService
26
37
  with({}).reduce(actions)
27
38
  end
28
39
 
29
- # We need to make sure existing users will
30
- # use `call` method name going forward.
31
- # This should be removed eventually.
32
- class VerifyCallMethodExists
33
- def self.call(klass, first_caller = '')
34
- invoker_method = caller_method(first_caller)
35
- return if invoker_method == 'call'
40
+ def reduce_if(condition_block, steps)
41
+ ReduceIf.run(self, condition_block, steps)
42
+ end
36
43
 
37
- call_method_exists = klass.methods.include?(:call)
38
- return if call_method_exists
44
+ def reduce_until(condition_block, steps)
45
+ ReduceUntil.run(self, condition_block, steps)
46
+ end
39
47
 
40
- warning_msg = "The <#{klass.name}> class is an organizer, " \
41
- "its entry method (the one that calls with & reduce) " \
42
- "should be named `call`. " \
43
- "Please use #{klass}.call going forward."
44
- ActiveSupport::Deprecation.warn(warning_msg)
45
- end
48
+ def iterate(collection_key, steps)
49
+ Iterate.run(self, collection_key, steps)
50
+ end
46
51
 
47
- def self.caller_method(first_caller)
48
- return nil unless first_caller =~ /`(.*)'/
52
+ def execute(code_block)
53
+ Execute.run(code_block)
54
+ end
49
55
 
50
- Regexp.last_match[1]
51
- end
56
+ def with_callback(action, steps)
57
+ WithCallback.run(self, action, steps)
52
58
  end
53
59
  end
54
60
 
@@ -56,6 +62,22 @@ module LightService
56
62
  def aliases(key_hash)
57
63
  @aliases = key_hash
58
64
  end
65
+
66
+ def before_actions(*logic)
67
+ self.before_actions = logic
68
+ end
69
+
70
+ def before_actions=(logic)
71
+ @before_actions = [logic].flatten
72
+ end
73
+
74
+ def after_actions(*logic)
75
+ self.after_actions = logic
76
+ end
77
+
78
+ def after_actions=(logic)
79
+ @after_actions = [logic].flatten
80
+ end
59
81
  end
60
82
  end
61
83
  end
@@ -0,0 +1,14 @@
1
+ module LightService
2
+ module Organizer
3
+ class Execute
4
+ def self.run(code_block)
5
+ lambda do |ctx|
6
+ return ctx if ctx.stop_processing?
7
+
8
+ code_block.call(ctx)
9
+ ctx
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ module LightService
2
+ module Organizer
3
+ class Iterate
4
+ extend ScopedReducable
5
+
6
+ def self.run(organizer, collection_key, steps)
7
+ lambda do |ctx|
8
+ return ctx if ctx.stop_processing?
9
+
10
+ collection = ctx[collection_key]
11
+ item_key = collection_key.to_s.singularize.to_sym
12
+ collection.each do |item|
13
+ ctx[item_key] = item
14
+ ctx = scoped_reduce(organizer, ctx, steps)
15
+ end
16
+
17
+ ctx
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module LightService
2
+ module Organizer
3
+ class ReduceIf
4
+ extend ScopedReducable
5
+
6
+ def self.run(organizer, condition_block, steps)
7
+ lambda do |ctx|
8
+ return ctx if ctx.stop_processing?
9
+
10
+ ctx = scoped_reduce(organizer, ctx, steps) \
11
+ if condition_block.call(ctx)
12
+ ctx
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module LightService
2
+ module Organizer
3
+ class ReduceUntil
4
+ extend ScopedReducable
5
+
6
+ def self.run(organizer, condition_block, steps)
7
+ lambda do |ctx|
8
+ return ctx if ctx.stop_processing?
9
+
10
+ loop do
11
+ ctx = scoped_reduce(organizer, ctx, steps)
12
+ break if condition_block.call(ctx) || ctx.failure?
13
+ end
14
+
15
+ ctx
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module LightService
2
+ module Organizer
3
+ module ScopedReducable
4
+ def scoped_reduce(organizer, ctx, steps)
5
+ ctx.reset_skip_remaining! unless ctx.failure?
6
+ ctx = organizer.with(ctx).reduce([steps])
7
+ ctx.reset_skip_remaining! unless ctx.failure?
8
+
9
+ ctx
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module LightService
2
+ module Organizer
3
+ # We need to make sure existing users will
4
+ # use `call` method name going forward.
5
+ # This should be removed eventually.
6
+ class VerifyCallMethodExists
7
+ def self.run(klass, first_caller = '')
8
+ invoker_method = caller_method(first_caller)
9
+ return if invoker_method == 'call'
10
+
11
+ call_method_exists = klass.methods.include?(:call)
12
+ return if call_method_exists
13
+
14
+ warning_msg = "The <#{klass.name}> class is an organizer, " \
15
+ "its entry method (the one that calls with & reduce) " \
16
+ "should be named `call`. " \
17
+ "Please use #{klass}.call going forward."
18
+ ActiveSupport::Deprecation.warn(warning_msg)
19
+ end
20
+
21
+ def self.caller_method(first_caller)
22
+ return nil unless first_caller =~ /`(.*)'/
23
+
24
+ Regexp.last_match[1]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module LightService
2
+ module Organizer
3
+ class WithCallback
4
+ extend ScopedReducable
5
+
6
+ def self.run(organizer, action, steps)
7
+ lambda do |ctx|
8
+ return ctx if ctx.stop_processing?
9
+
10
+ # This will only allow 2 level deep nesting of callbacks
11
+ previous_callback = ctx[:callback]
12
+
13
+ ctx[:callback] = lambda do |context|
14
+ ctx = scoped_reduce(organizer, context, steps)
15
+ ctx
16
+ end
17
+
18
+ ctx = action.execute(ctx)
19
+ ctx[:callback] = previous_callback
20
+
21
+ ctx
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,7 +1,7 @@
1
1
  module LightService
2
2
  module Organizer
3
3
  class WithReducer
4
- attr_reader :context, :around_each_handler
4
+ attr_reader :context
5
5
 
6
6
  def with(data = {})
7
7
  @context = LightService::Context.make(data)
@@ -13,6 +13,14 @@ module LightService
13
13
  self
14
14
  end
15
15
 
16
+ def around_each_handler
17
+ @around_each_handler ||= Class.new do
18
+ def self.call(_context)
19
+ yield
20
+ end
21
+ end
22
+ end
23
+
16
24
  def reduce(*actions)
17
25
  raise "No action(s) were provided" if actions.empty?
18
26
  actions.flatten!
@@ -46,10 +54,12 @@ module LightService
46
54
  private
47
55
 
48
56
  def invoke_action(current_context, action)
49
- return action.execute(current_context) unless around_each_handler
50
-
51
57
  around_each_handler.call(current_context) do
52
- action.execute(current_context)
58
+ if action.respond_to?(:call)
59
+ action.call(current_context)
60
+ else
61
+ action.execute(current_context)
62
+ end
53
63
  end
54
64
  end
55
65
 
@@ -3,7 +3,9 @@ module LightService
3
3
  class WithReducerFactory
4
4
  def self.make(monitored_organizer)
5
5
  if LightService::Configuration.logger.nil?
6
+ # :nocov:
6
7
  WithReducer.new
8
+ # :nocov:
7
9
  else
8
10
  WithReducerLogDecorator.new(monitored_organizer, WithReducer.new)
9
11
  end
@@ -1,3 +1,3 @@
1
1
  module LightService
2
- VERSION = "0.8.4".freeze
2
+ VERSION = "0.10.0".freeze
3
3
  end
@@ -19,7 +19,7 @@ Gem::Specification.new do |gem|
19
19
  gem.add_dependency("activesupport", ">= 3.0")
20
20
 
21
21
  gem.add_development_dependency("rspec", "~> 3.0")
22
- gem.add_development_dependency("simplecov", "~> 0.14.1")
23
- gem.add_development_dependency("rubocop", "~> 0.51")
22
+ gem.add_development_dependency("simplecov", "~> 0.16.1")
23
+ gem.add_development_dependency("rubocop", "~> 0.53")
24
24
  gem.add_development_dependency("pry", "~> 0.10")
25
25
  end
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <svg version="1.1"
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ xmlns:xlink="http://www.w3.org/1999/xlink"
5
+ width="350" height="20"
6
+ >
7
+ <text font-size="20" x="0" y="20" font-family="Verdana">
8
+ <tspan fill="red">Orchestrators are DEPRECATED!</tspan>
9
+ </text>
10
+ </svg>
@@ -1,10 +1,10 @@
1
1
  require 'spec_helper'
2
2
  require 'test_doubles'
3
3
 
4
- describe TestDoubles::AdditionOrganizer do
5
- it "Adds 1, 2 and 3 to the initial value of 1" do
4
+ RSpec.describe TestDoubles::AdditionOrganizer do
5
+ it 'Adds 1, 2 and 3 to the initial value of 1' do
6
6
  result = TestDoubles::AdditionOrganizer.call(1)
7
- number = result.fetch(:product)
7
+ number = result.fetch(:number)
8
8
 
9
9
  expect(number).to eq(7)
10
10
  end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe 'Action after_actions' do
5
+ describe 'works with simple organizers - from outside' do
6
+ it 'can be used to inject code block before each action' do
7
+ TestDoubles::AdditionOrganizer.after_actions =
8
+ lambda do |ctx|
9
+ ctx.number -= 2 if ctx.current_action == TestDoubles::AddsThreeAction
10
+ end
11
+
12
+ result = TestDoubles::AdditionOrganizer.call(0)
13
+
14
+ expect(result.fetch(:number)).to eq(4)
15
+ end
16
+
17
+ it 'works with iterator' do
18
+ TestDoubles::TestIterate.after_actions = [
19
+ lambda do |ctx|
20
+ ctx.number -= 2 if ctx.current_action == TestDoubles::AddsOneAction
21
+ end
22
+ ]
23
+
24
+ result = TestDoubles::TestIterate.call(:number => 0,
25
+ :counters => [1, 2, 3, 4])
26
+
27
+ expect(result).to be_success
28
+ expect(result.number).to eq(-4)
29
+ end
30
+ end
31
+
32
+ describe 'can be added to organizers declaratively' do
33
+ module AfterActions
34
+ class AdditionOrganizer
35
+ extend LightService::Organizer
36
+ after_actions (lambda do |ctx|
37
+ if ctx.current_action == TestDoubles::AddsOneAction
38
+ ctx.number -= 2
39
+ end
40
+ end),
41
+ (lambda do |ctx|
42
+ if ctx.current_action == TestDoubles::AddsThreeAction
43
+ ctx.number -= 3
44
+ end
45
+ end)
46
+
47
+ def self.call(number)
48
+ with(:number => number).reduce(actions)
49
+ end
50
+
51
+ def self.actions
52
+ [
53
+ TestDoubles::AddsOneAction,
54
+ TestDoubles::AddsTwoAction,
55
+ TestDoubles::AddsThreeAction
56
+ ]
57
+ end
58
+ end
59
+ end
60
+
61
+ it 'accepts after_actions hook lambdas from organizer' do
62
+ result = AfterActions::AdditionOrganizer.call(0)
63
+
64
+ expect(result.fetch(:number)).to eq(1)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe 'Action before_actions' do
5
+ describe 'works with simple organizers - from outside' do
6
+ it 'can be used to inject code block before each action' do
7
+ TestDoubles::AdditionOrganizer.before_actions =
8
+ lambda do |ctx|
9
+ ctx.number -= 2 if ctx.current_action == TestDoubles::AddsThreeAction
10
+ end
11
+
12
+ result = TestDoubles::AdditionOrganizer.call(0)
13
+
14
+ expect(result.fetch(:number)).to eq(4)
15
+ end
16
+
17
+ it 'works with iterator' do
18
+ TestDoubles::TestIterate.before_actions = [
19
+ lambda do |ctx|
20
+ ctx.number -= 2 if ctx.current_action == TestDoubles::AddsOneAction
21
+ end
22
+ ]
23
+
24
+ result = TestDoubles::TestIterate.call(:number => 0,
25
+ :counters => [1, 2, 3, 4])
26
+
27
+ expect(result).to be_success
28
+ expect(result.number).to eq(-4)
29
+ end
30
+ end
31
+
32
+ describe 'can be added to organizers declaratively' do
33
+ module BeforeActions
34
+ class AdditionOrganizer
35
+ extend LightService::Organizer
36
+ before_actions (lambda do |ctx|
37
+ if ctx.current_action == TestDoubles::AddsOneAction
38
+ ctx.number -= 2
39
+ end
40
+ end),
41
+ (lambda do |ctx|
42
+ if ctx.current_action == TestDoubles::AddsThreeAction
43
+ ctx.number -= 3
44
+ end
45
+ end)
46
+
47
+ def self.call(number)
48
+ with(:number => number).reduce(actions)
49
+ end
50
+
51
+ def self.actions
52
+ [
53
+ TestDoubles::AddsOneAction,
54
+ TestDoubles::AddsTwoAction,
55
+ TestDoubles::AddsThreeAction
56
+ ]
57
+ end
58
+ end
59
+ end
60
+
61
+ it 'accepts before_actions hook lambdas from organizer' do
62
+ result = BeforeActions::AdditionOrganizer.call(0)
63
+
64
+ expect(result.fetch(:number)).to eq(1)
65
+ end
66
+ end
67
+
68
+ describe 'works with callbacks' do
69
+ it 'can interact with actions from the outside' do
70
+ TestDoubles::TestWithCallback.before_actions = [
71
+ lambda do |ctx|
72
+ if ctx.current_action == TestDoubles::AddToTotalAction
73
+ ctx.total -= 1000
74
+ end
75
+ end
76
+ ]
77
+ result = TestDoubles::TestWithCallback.call
78
+
79
+ expect(result.counter).to eq(3)
80
+ expect(result.total).to eq(-2994)
81
+ end
82
+ end
83
+
84
+ describe 'can halt all execution with a raised error' do
85
+ it 'does not call the rest of the callback steps' do
86
+ class SkipContextError < StandardError
87
+ attr_reader :ctx
88
+
89
+ def initialize(msg, ctx)
90
+ @ctx = ctx
91
+ super(msg)
92
+ end
93
+ end
94
+ TestDoubles::TestWithCallback.before_actions = [
95
+ lambda do |ctx|
96
+ if ctx.current_action == TestDoubles::IncrementCountAction
97
+ ctx.total -= 1000
98
+ raise SkipContextError.new("stop context now", ctx)
99
+ end
100
+ end
101
+ ]
102
+ begin
103
+ TestDoubles::TestWithCallback.call
104
+ rescue SkipContextError => e
105
+ expect(e.ctx).not_to be_empty
106
+ end
107
+ end
108
+ end
109
+ end