light-service 0.15.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +28 -0
  3. data/.rubocop.yml +117 -3
  4. data/.travis.yml +3 -8
  5. data/README.md +89 -37
  6. data/RELEASES.md +18 -1
  7. data/lib/generators/light_service/generator_utils.rb +0 -2
  8. data/lib/light-service/action.rb +61 -4
  9. data/lib/light-service/context/key_verifier.rb +22 -3
  10. data/lib/light-service/context.rb +10 -12
  11. data/lib/light-service/errors.rb +5 -0
  12. data/lib/light-service/orchestrator.rb +1 -1
  13. data/lib/light-service/organizer/reduce_case.rb +48 -0
  14. data/lib/light-service/organizer/reduce_if_else.rb +21 -0
  15. data/lib/light-service/organizer/with_reducer.rb +11 -14
  16. data/lib/light-service/organizer/with_reducer_log_decorator.rb +2 -2
  17. data/lib/light-service/organizer.rb +20 -3
  18. data/lib/light-service/testing/context_factory.rb +2 -0
  19. data/lib/light-service/version.rb +1 -1
  20. data/lib/light-service.rb +2 -0
  21. data/light-service.gemspec +3 -2
  22. data/spec/acceptance/after_actions_spec.rb +17 -6
  23. data/spec/acceptance/around_each_spec.rb +15 -0
  24. data/spec/acceptance/before_actions_spec.rb +3 -9
  25. data/spec/acceptance/log_from_organizer_spec.rb +1 -1
  26. data/spec/acceptance/organizer/add_to_context_spec.rb +27 -0
  27. data/spec/acceptance/organizer/execute_with_add_to_context_spec.rb +28 -0
  28. data/spec/acceptance/organizer/reduce_case_spec.rb +53 -0
  29. data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
  30. data/spec/acceptance/organizer/reduce_if_spec.rb +2 -0
  31. data/spec/action_optional_expected_keys_spec.rb +82 -0
  32. data/spec/context/inspect_spec.rb +5 -21
  33. data/spec/context_spec.rb +1 -1
  34. data/spec/lib/generators/action_generator_advanced_spec.rb +1 -1
  35. data/spec/lib/generators/action_generator_simple_spec.rb +1 -1
  36. data/spec/lib/generators/organizer_generator_advanced_spec.rb +1 -1
  37. data/spec/lib/generators/organizer_generator_simple_spec.rb +1 -1
  38. data/spec/sample/calculates_tax_spec.rb +0 -1
  39. data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
  40. data/spec/test_doubles.rb +48 -5
  41. metadata +22 -13
  42. data/gemfiles/activesupport_4.gemfile +0 -7
  43. data/resources/orchestrators_deprecated.svg +0 -10
@@ -6,10 +6,12 @@ module LightService
6
6
  FAILURE = 1
7
7
  end
8
8
 
9
- # rubocop:disable ClassLength
9
+ # rubocop:disable Metrics/ClassLength
10
10
  class Context < Hash
11
- attr_accessor :message, :error_code, :current_action, :organized_by
11
+ attr_accessor :message, :error_code, :current_action, :around_actions,
12
+ :organized_by
12
13
 
14
+ # rubocop:disable Metrics/ParameterLists, Lint/MissingSuper
13
15
  def initialize(context = {},
14
16
  outcome = Outcomes::SUCCESS,
15
17
  message = '',
@@ -21,6 +23,7 @@ module LightService
21
23
 
22
24
  context.to_hash.each { |k, v| self[k] = v }
23
25
  end
26
+ # rubocop:enable Metrics/ParameterLists, Lint/MissingSuper
24
27
 
25
28
  def self.make(context = {})
26
29
  unless context.is_a?(Hash) || context.is_a?(LightService::Context)
@@ -115,9 +118,9 @@ module LightService
115
118
  end
116
119
 
117
120
  def define_accessor_methods_for_keys(keys)
118
- return if keys.nil?
121
+ return if keys.blank?
119
122
 
120
- keys.each do |key|
123
+ Array(keys).each do |key|
121
124
  next if respond_to?(key.to_sym)
122
125
 
123
126
  define_singleton_method(key.to_s) { fetch(key) }
@@ -151,13 +154,8 @@ module LightService
151
154
  end
152
155
 
153
156
  def inspect
154
- "#{self.class}(#{self}, " \
155
- + "success: #{success?}, " \
156
- + "message: #{check_nil(message)}, " \
157
- + "error_code: #{check_nil(error_code)}, " \
158
- + "skip_remaining: #{@skip_remaining}, " \
159
- + "aliases: #{@aliases}" \
160
- + ")"
157
+ "#{self.class}(#{self}, success: #{success?}, message: #{check_nil(message)}, error_code: " \
158
+ "#{check_nil(error_code)}, skip_remaining: #{@skip_remaining}, aliases: #{@aliases})"
161
159
  end
162
160
 
163
161
  private
@@ -168,5 +166,5 @@ module LightService
168
166
  "'#{value}'"
169
167
  end
170
168
  end
171
- # rubocop:enable ClassLength
169
+ # rubocop:enable Metrics/ClassLength
172
170
  end
@@ -1,6 +1,11 @@
1
1
  module LightService
2
2
  class FailWithRollbackError < StandardError; end
3
+
3
4
  class ExpectedKeysNotInContextError < StandardError; end
5
+
4
6
  class PromisedKeysNotInContextError < StandardError; end
7
+
5
8
  class ReservedKeysInContextError < StandardError; end
9
+
10
+ class UnusableExpectKeyDefaultError < StandardError; end
6
11
  end
@@ -115,7 +115,7 @@ module LightService
115
115
 
116
116
  def issue_deprecation_warning_for(method_name)
117
117
  msg = "`Orchestrator##{method_name}` is DEPRECATED and will be " \
118
- "removed, please switch to `Organizer##{method_name} instead. "
118
+ "removed, please switch to `Organizer##{method_name} instead. "
119
119
  ActiveSupport::Deprecation.warn(msg)
120
120
  end
121
121
  end
@@ -0,0 +1,48 @@
1
+ module LightService
2
+ module Organizer
3
+ class ReduceCase
4
+ extend ScopedReducable
5
+
6
+ class Arguments
7
+ attr_reader :value, :when, :else
8
+
9
+ def initialize(**args)
10
+ validate_arguments(**args)
11
+ @value = args[:value]
12
+ @when = args[:when]
13
+ @else = args[:else]
14
+ end
15
+
16
+ private
17
+
18
+ # rubocop:disable Style/MultilineIfModifier
19
+ def validate_arguments(**args)
20
+ raise(
21
+ ArgumentError,
22
+ "Expected keyword arguments: [:value, :when, :else]. Given: #{args.keys}"
23
+ ) unless args.keys.intersection(mandatory_arguments).count == mandatory_arguments.count
24
+ end
25
+ # rubocop:enable Style/MultilineIfModifier
26
+
27
+ def mandatory_arguments
28
+ %i[value when else]
29
+ end
30
+ end
31
+
32
+ def self.run(organizer, **args)
33
+ arguments = Arguments.new(**args)
34
+
35
+ lambda do |ctx|
36
+ return ctx if ctx.stop_processing?
37
+
38
+ matched_case = arguments.when.keys.find { |k| k.eql?(ctx[arguments.value]) }
39
+ steps = arguments.when[matched_case] || arguments.else
40
+
41
+ ctx = scoped_reduce(organizer, ctx, steps)
42
+
43
+ ctx
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ module LightService
2
+ module Organizer
3
+ class ReduceIfElse
4
+ extend ScopedReducable
5
+
6
+ def self.run(organizer, condition_block, if_steps, else_steps)
7
+ lambda do |ctx|
8
+ return ctx if ctx.stop_processing?
9
+
10
+ ctx = if condition_block.call(ctx)
11
+ scoped_reduce(organizer, ctx, if_steps)
12
+ else
13
+ scoped_reduce(organizer, ctx, else_steps)
14
+ end
15
+
16
+ ctx
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -30,17 +30,16 @@ module LightService
30
30
  def reduce(*actions)
31
31
  raise "No action(s) were provided" if actions.empty?
32
32
 
33
+ @context.around_actions ||= around_each_handler
33
34
  actions.flatten!
34
35
 
35
36
  actions.each_with_object(context) do |action, current_context|
36
- begin
37
- invoke_action(current_context, action)
38
- rescue FailWithRollbackError
39
- reduce_rollback(actions)
40
- ensure
41
- # For logging
42
- yield(current_context, action) if block_given?
43
- end
37
+ invoke_action(current_context, action)
38
+ rescue FailWithRollbackError
39
+ reduce_rollback(actions)
40
+ ensure
41
+ # For logging
42
+ yield(current_context, action) if block_given?
44
43
  end
45
44
  end
46
45
 
@@ -59,12 +58,10 @@ module LightService
59
58
  private
60
59
 
61
60
  def invoke_action(current_context, action)
62
- around_each_handler.call(current_context) do
63
- if action.respond_to?(:call)
64
- action.call(current_context)
65
- else
66
- action.execute(current_context)
67
- end
61
+ if action.respond_to?(:call)
62
+ action.call(current_context)
63
+ else
64
+ action.execute(current_context)
68
65
  end
69
66
  end
70
67
 
@@ -5,7 +5,7 @@ module LightService
5
5
 
6
6
  alias logged? logged
7
7
 
8
- def initialize(organizer, decorated: WithReducer.new, logger:)
8
+ def initialize(organizer, logger:, decorated: WithReducer.new)
9
9
  @decorated = decorated
10
10
  @organizer = organizer
11
11
 
@@ -22,7 +22,7 @@ module LightService
22
22
 
23
23
  logger.info do
24
24
  "[LightService] - keys in context: " \
25
- "#{extract_keys(decorated.context.keys)}"
25
+ "#{extract_keys(decorated.context.keys)}"
26
26
  end
27
27
  self
28
28
  end
@@ -41,10 +41,18 @@ module LightService
41
41
  ReduceIf.run(self, condition_block, steps)
42
42
  end
43
43
 
44
+ def reduce_if_else(condition_block, if_steps, else_steps)
45
+ ReduceIfElse.run(self, condition_block, if_steps, else_steps)
46
+ end
47
+
44
48
  def reduce_until(condition_block, steps)
45
49
  ReduceUntil.run(self, condition_block, steps)
46
50
  end
47
51
 
52
+ def reduce_case(**args)
53
+ ReduceCase.run(self, **args)
54
+ end
55
+
48
56
  def iterate(collection_key, steps)
49
57
  Iterate.run(self, collection_key, steps)
50
58
  end
@@ -65,9 +73,18 @@ module LightService
65
73
  @logger
66
74
  end
67
75
 
68
- def add_to_context(**args)
69
- args.map do |key, value|
70
- execute(->(ctx) { ctx[key.to_sym] = value })
76
+ # Set the value as a key on the context hash
77
+ # and also create convenience accessors for the keys
78
+ def add_to_context(args)
79
+ Context::ReservedKeysViaOrganizerVerifier.new(args).verify
80
+
81
+ Hash(args).map do |key, value|
82
+ context_key = lambda do |ctx|
83
+ ctx[key.to_sym] = value
84
+ ctx.define_accessor_methods_for_keys(key)
85
+ end
86
+
87
+ execute(context_key)
71
88
  end
72
89
  end
73
90
 
@@ -26,11 +26,13 @@ module LightService
26
26
 
27
27
  # More than one arguments can be passed to the
28
28
  # Organizer's #call method
29
+ # rubocop:disable Style/ArgumentsForwarding
29
30
  def with(*args, &block)
30
31
  catch(:return_ctx_from_execution) do
31
32
  @organizer.call(*args, &block)
32
33
  end
33
34
  end
35
+ # rubocop:enable Style/ArgumentsForwarding
34
36
 
35
37
  def initialize(organizer)
36
38
  @organizer = organizer
@@ -1,3 +1,3 @@
1
1
  module LightService
2
- VERSION = "0.15.0".freeze
2
+ VERSION = "0.18.0".freeze
3
3
  end
data/lib/light-service.rb CHANGED
@@ -13,7 +13,9 @@ require 'light-service/organizer/with_reducer'
13
13
  require 'light-service/organizer/with_reducer_log_decorator'
14
14
  require 'light-service/organizer/with_reducer_factory'
15
15
  require 'light-service/organizer/reduce_if'
16
+ require 'light-service/organizer/reduce_if_else'
16
17
  require 'light-service/organizer/reduce_until'
18
+ require 'light-service/organizer/reduce_case'
17
19
  require 'light-service/organizer/iterate'
18
20
  require 'light-service/organizer/execute'
19
21
  require 'light-service/organizer/with_callback'
@@ -15,8 +15,9 @@ Gem::Specification.new do |gem|
15
15
  gem.name = "light-service"
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = LightService::VERSION
18
+ gem.required_ruby_version = '>= 2.6.0'
18
19
 
19
- gem.add_runtime_dependency("activesupport", ">= 3.0.0")
20
+ gem.add_runtime_dependency("activesupport", ">= 4.0.0")
20
21
 
21
22
  gem.add_development_dependency("generator_spec", "~> 0.9.4")
22
23
  gem.add_development_dependency("test-unit", "~> 3.0") # Needed for generator specs.
@@ -24,7 +25,7 @@ Gem::Specification.new do |gem|
24
25
  gem.add_development_dependency("rspec", "~> 3.0")
25
26
  gem.add_development_dependency("simplecov", "~> 0.17")
26
27
  gem.add_development_dependency("codecov", "~> 0.1")
27
- gem.add_development_dependency("rubocop", "~> 0.68.0")
28
+ gem.add_development_dependency("rubocop", "~> 1.26.0")
28
29
  gem.add_development_dependency("rubocop-performance", "~> 1.2.0")
29
30
  gem.add_development_dependency("pry", "~> 0.12.2")
30
31
  end
@@ -34,14 +34,10 @@ RSpec.describe 'Action after_actions' do
34
34
  class AdditionOrganizer
35
35
  extend LightService::Organizer
36
36
  after_actions (lambda do |ctx|
37
- if ctx.current_action == TestDoubles::AddsOneAction
38
- ctx.number -= 2
39
- end
37
+ ctx.number -= 2 if ctx.current_action == TestDoubles::AddsOneAction
40
38
  end),
41
39
  (lambda do |ctx|
42
- if ctx.current_action == TestDoubles::AddsThreeAction
43
- ctx.number -= 3
44
- end
40
+ ctx.number -= 3 if ctx.current_action == TestDoubles::AddsThreeAction
45
41
  end)
46
42
 
47
43
  def self.call(number)
@@ -65,6 +61,21 @@ RSpec.describe 'Action after_actions' do
65
61
  end
66
62
  end
67
63
 
64
+ context 'with callbacks' do
65
+ it 'ensures the correct :current_action is set' do
66
+ TestDoubles::TestWithCallback.after_actions = [
67
+ lambda do |ctx|
68
+ ctx.total -= 1000 if ctx.current_action == TestDoubles::IterateCollectionAction
69
+ end
70
+ ]
71
+
72
+ result = TestDoubles::TestWithCallback.call
73
+
74
+ expect(result.counter).to eq(3)
75
+ expect(result.total).to eq(-994)
76
+ end
77
+ end
78
+
68
79
  describe 'after_actions can be appended' do
69
80
  it 'adds to the :_after_actions collection' do
70
81
  TestDoubles::AdditionOrganizer.append_after_actions(
@@ -16,4 +16,19 @@ describe 'Executing arbitrary code around each action' do
16
16
  }]
17
17
  )
18
18
  end
19
+
20
+ it 'logs data with nested actions' do
21
+ context = { :number => 1, :logger => TestDoubles::TestLogger.new }
22
+
23
+ result = TestDoubles::AroundEachWithReduceIfOrganizer.call(context)
24
+
25
+ expect(result.fetch(:number)).to eq(7)
26
+ expect(result[:logger].logs).to eq(
27
+ [
28
+ { :action => TestDoubles::AddsOneAction, :before => 1, :after => 2 },
29
+ { :action => TestDoubles::AddsTwoAction, :before => 2, :after => 4 },
30
+ { :action => TestDoubles::AddsThreeAction, :before => 4, :after => 7 }
31
+ ]
32
+ )
33
+ end
19
34
  end
@@ -34,14 +34,10 @@ RSpec.describe 'Action before_actions' do
34
34
  class AdditionOrganizer
35
35
  extend LightService::Organizer
36
36
  before_actions (lambda do |ctx|
37
- if ctx.current_action == TestDoubles::AddsOneAction
38
- ctx.number -= 2
39
- end
37
+ ctx.number -= 2 if ctx.current_action == TestDoubles::AddsOneAction
40
38
  end),
41
39
  (lambda do |ctx|
42
- if ctx.current_action == TestDoubles::AddsThreeAction
43
- ctx.number -= 3
44
- end
40
+ ctx.number -= 3 if ctx.current_action == TestDoubles::AddsThreeAction
45
41
  end)
46
42
 
47
43
  def self.call(number)
@@ -69,9 +65,7 @@ RSpec.describe 'Action before_actions' do
69
65
  it 'can interact with actions from the outside' do
70
66
  TestDoubles::TestWithCallback.before_actions = [
71
67
  lambda do |ctx|
72
- if ctx.current_action == TestDoubles::AddToTotalAction
73
- ctx.total -= 1000
74
- end
68
+ ctx.total -= 1000 if ctx.current_action == TestDoubles::AddToTotalAction
75
69
  end
76
70
  ]
77
71
  result = TestDoubles::TestWithCallback.call
@@ -59,7 +59,7 @@ describe "Logs from organizer" do
59
59
  expect(log_message).to include(organizer_log_message)
60
60
  end
61
61
 
62
- it "lists the keys in contect after the actions are executed" do
62
+ it "lists the keys in context after the actions are executed" do
63
63
  organizer_log_message = "[LightService] - keys in context: " \
64
64
  ":tea, :milk, :coffee, :milk_tea, :latte"
65
65
  expect(log_message).to include(organizer_log_message)
@@ -20,6 +20,20 @@ RSpec.describe LightService::Organizer do
20
20
  end
21
21
  end
22
22
 
23
+ class TestAddToContextReservedWords
24
+ extend LightService::Organizer
25
+
26
+ def self.call(context = LightService::Context.make)
27
+ with(context).reduce(steps)
28
+ end
29
+
30
+ def self.steps
31
+ [
32
+ add_to_context(:message => "yo", "error_code" => "00P5")
33
+ ]
34
+ end
35
+ end
36
+
23
37
  it 'adds items to the context on the fly' do
24
38
  result = TestAddToContext.call
25
39
 
@@ -27,4 +41,17 @@ RSpec.describe LightService::Organizer do
27
41
  expect(result.number).to eq(1)
28
42
  expect(result[:something]).to eq('hello')
29
43
  end
44
+
45
+ it 'adds items to the context as accessors' do
46
+ result = TestAddToContext.call
47
+
48
+ expect(result).to be_success
49
+ expect(result.something).to eq('hello')
50
+ end
51
+
52
+ it "will not add items as accessors when they are reserved" do
53
+ expect { TestAddToContextReservedWords.call }.to \
54
+ raise_error(LightService::ReservedKeysInContextError)
55
+ .with_message(/:message, :error_code/)
56
+ end
30
57
  end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ class TestExecuteWithAddToContext
6
+ extend LightService::Organizer
7
+
8
+ def self.call
9
+ with.reduce(steps)
10
+ end
11
+
12
+ def self.steps
13
+ [
14
+ add_to_context(:greeting => "hello"),
15
+ execute(->(ctx) { ctx.greeting.upcase! })
16
+ ]
17
+ end
18
+ end
19
+
20
+ context "when using context values created by add_to_context" do
21
+ it "is expected to reference them as accessors" do
22
+ result = TestExecuteWithAddToContext.call
23
+
24
+ expect(result).to be_a_success
25
+ expect(result.greeting).to eq "HELLO"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ class TestReduceCase
6
+ extend LightService::Organizer
7
+
8
+ def self.call(context)
9
+ with(context).reduce(actions)
10
+ end
11
+
12
+ def self.actions
13
+ [
14
+ reduce_case(
15
+ :value => :incr_num,
16
+ :when => {
17
+ :one => [TestDoubles::AddsOneAction],
18
+ :two => [TestDoubles::AddsTwoAction],
19
+ :three => [TestDoubles::AddsThreeAction]
20
+ },
21
+ :else => [TestDoubles::FailureAction]
22
+ )
23
+ ]
24
+ end
25
+ end
26
+
27
+ it 'adds one if the incr_num is one' do
28
+ result = TestReduceCase.call(:number => 0, :incr_num => :one)
29
+
30
+ expect(result).to be_success
31
+ expect(result[:number]).to eq(1)
32
+ end
33
+
34
+ it 'adds two if the incr_num is two' do
35
+ result = TestReduceCase.call(:number => 0, :incr_num => :two)
36
+
37
+ expect(result).to be_success
38
+ expect(result[:number]).to eq(2)
39
+ end
40
+
41
+ it 'adds three if the incr_num is three' do
42
+ result = TestReduceCase.call(:number => 0, :incr_num => :three)
43
+
44
+ expect(result).to be_success
45
+ expect(result[:number]).to eq(3)
46
+ end
47
+
48
+ it 'will fail if the incr_num is neither one, two, or three' do
49
+ result = TestReduceCase.call(:number => 0, :incr_num => :four)
50
+
51
+ expect(result).to be_failure
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ class TestReduceIfElse
6
+ extend LightService::Organizer
7
+
8
+ def self.call(context)
9
+ with(context).reduce(actions)
10
+ end
11
+
12
+ def self.actions
13
+ [
14
+ TestDoubles::AddsOneAction,
15
+ reduce_if_else(
16
+ ->(ctx) { ctx.number == 1 },
17
+ [TestDoubles::AddsOneAction],
18
+ [TestDoubles::AddsTwoAction]
19
+ )
20
+ ]
21
+ end
22
+ end
23
+
24
+ let(:empty_context) { LightService::Context.make }
25
+
26
+ it 'reduces the if_steps if the condition is true' do
27
+ result = TestReduceIfElse.call(:number => 0)
28
+
29
+ expect(result).to be_success
30
+ expect(result[:number]).to eq(2)
31
+ end
32
+
33
+ it 'reduces the else_steps if the condition is false' do
34
+ result = TestReduceIfElse.call(:number => 2)
35
+
36
+ expect(result).to be_success
37
+ expect(result[:number]).to eq(5)
38
+ end
39
+
40
+ it 'will not reduce over a failed context' do
41
+ empty_context.fail!('Something bad happened')
42
+
43
+ result = TestReduceIfElse.call(empty_context)
44
+
45
+ expect(result).to be_failure
46
+ end
47
+
48
+ it 'does not reduce over a skipped context' do
49
+ empty_context.skip_remaining!('No more needed')
50
+
51
+ result = TestReduceIfElse.call(empty_context)
52
+ expect(result).to be_success
53
+ end
54
+
55
+ it "knows that it's being conditionally reduced from within an organizer" do
56
+ result = TestReduceIfElse.call(:number => 2)
57
+
58
+ expect(result.organized_by).to eq TestReduceIfElse
59
+ end
60
+ end
@@ -63,6 +63,7 @@ RSpec.describe LightService::Organizer do
63
63
  reduce(actions)
64
64
  end
65
65
 
66
+ # rubocop:disable Metrics/AbcSize
66
67
  def self.actions
67
68
  [
68
69
  reduce_if(
@@ -76,6 +77,7 @@ RSpec.describe LightService::Organizer do
76
77
  execute(->(c) { c[:last_outside] = true })
77
78
  ]
78
79
  end
80
+ # rubocop:enable Metrics/AbcSize
79
81
  end
80
82
 
81
83
  result = org.call