light-service 0.13.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +28 -0
  3. data/.travis.yml +3 -9
  4. data/Appraisals +0 -4
  5. data/Gemfile +0 -2
  6. data/README.md +287 -51
  7. data/RELEASES.md +21 -2
  8. data/gemfiles/activesupport_5.gemfile +0 -1
  9. data/gemfiles/activesupport_6.gemfile +0 -1
  10. data/lib/generators/light_service/action_generator.rb +90 -0
  11. data/lib/generators/light_service/generator_utils.rb +45 -0
  12. data/lib/generators/light_service/organizer_generator.rb +66 -0
  13. data/lib/generators/light_service/templates/action_spec_template.erb +31 -0
  14. data/lib/generators/light_service/templates/action_template.erb +30 -0
  15. data/lib/generators/light_service/templates/organizer_spec_template.erb +20 -0
  16. data/lib/generators/light_service/templates/organizer_template.erb +22 -0
  17. data/lib/light-service/action.rb +61 -4
  18. data/lib/light-service/context/key_verifier.rb +18 -1
  19. data/lib/light-service/context.rb +5 -3
  20. data/lib/light-service/errors.rb +1 -0
  21. data/lib/light-service/organizer/reduce_if_else.rb +21 -0
  22. data/lib/light-service/organizer/with_reducer.rb +12 -7
  23. data/lib/light-service/organizer/with_reducer_factory.rb +1 -1
  24. data/lib/light-service/organizer/with_reducer_log_decorator.rb +3 -0
  25. data/lib/light-service/organizer.rb +16 -3
  26. data/lib/light-service/version.rb +1 -1
  27. data/lib/light-service.rb +1 -0
  28. data/light-service.gemspec +6 -1
  29. data/spec/acceptance/after_actions_spec.rb +17 -0
  30. data/spec/acceptance/around_each_spec.rb +15 -0
  31. data/spec/acceptance/log_from_organizer_spec.rb +1 -1
  32. data/spec/acceptance/organizer/add_to_context_spec.rb +27 -0
  33. data/spec/acceptance/organizer/execute_with_add_to_context_spec.rb +28 -0
  34. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  35. data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
  36. data/spec/acceptance/organizer/reduce_if_spec.rb +6 -0
  37. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  38. data/spec/action_optional_expected_keys_spec.rb +82 -0
  39. data/spec/action_spec.rb +8 -0
  40. data/spec/lib/generators/action_generator_advanced_spec.rb +43 -0
  41. data/spec/lib/generators/action_generator_simple_spec.rb +37 -0
  42. data/spec/lib/generators/full_generator_test_blobs.rb +193 -0
  43. data/spec/lib/generators/organizer_generator_advanced_spec.rb +37 -0
  44. data/spec/lib/generators/organizer_generator_simple_spec.rb +37 -0
  45. data/spec/organizer_spec.rb +5 -0
  46. data/spec/spec_helper.rb +5 -1
  47. data/spec/test_doubles.rb +37 -0
  48. metadata +87 -9
  49. data/gemfiles/activesupport_3.gemfile +0 -8
  50. data/gemfiles/activesupport_4.gemfile +0 -8
  51. data/resources/orchestrators_deprecated.svg +0 -10
@@ -111,7 +111,24 @@ module LightService
111
111
  end
112
112
 
113
113
  def reserved_keys
114
- %i[message error_code current_action].freeze
114
+ %i[message error_code current_action organized_by].freeze
115
+ end
116
+ end
117
+
118
+ class ReservedKeysViaOrganizerVerifier < ReservedKeysVerifier
119
+ def initialize(context_data)
120
+ @context = LightService::Context.make(context_data)
121
+ end
122
+
123
+ def violated_keys
124
+ context.keys.map(&:to_sym) & reserved_keys
125
+ end
126
+
127
+ def error_message
128
+ <<~ERR
129
+ reserved keys cannot be added to the context
130
+ reserved key: [#{format_keys(violated_keys)}]
131
+ ERR
115
132
  end
116
133
  end
117
134
  end
@@ -8,7 +8,8 @@ module LightService
8
8
 
9
9
  # rubocop:disable ClassLength
10
10
  class Context < Hash
11
- attr_accessor :message, :error_code, :current_action
11
+ attr_accessor :message, :error_code, :current_action, :around_actions,
12
+ :organized_by
12
13
 
13
14
  def initialize(context = {},
14
15
  outcome = Outcomes::SUCCESS,
@@ -18,6 +19,7 @@ module LightService
18
19
  @message = message
19
20
  @error_code = error_code
20
21
  @skip_remaining = false
22
+
21
23
  context.to_hash.each { |k, v| self[k] = v }
22
24
  end
23
25
 
@@ -114,9 +116,9 @@ module LightService
114
116
  end
115
117
 
116
118
  def define_accessor_methods_for_keys(keys)
117
- return if keys.nil?
119
+ return if keys.blank?
118
120
 
119
- keys.each do |key|
121
+ Array(keys).each do |key|
120
122
  next if respond_to?(key.to_sym)
121
123
 
122
124
  define_singleton_method(key.to_s) { fetch(key) }
@@ -3,4 +3,5 @@ module LightService
3
3
  class ExpectedKeysNotInContextError < StandardError; end
4
4
  class PromisedKeysNotInContextError < StandardError; end
5
5
  class ReservedKeysInContextError < StandardError; end
6
+ class UnusableExpectKeyDefaultError < StandardError; end
6
7
  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
@@ -1,10 +1,16 @@
1
1
  module LightService
2
2
  module Organizer
3
3
  class WithReducer
4
- attr_reader :context
4
+ attr_reader :context
5
+ attr_accessor :organizer
6
+
7
+ def initialize(monitored_organizer = nil)
8
+ @organizer = monitored_organizer
9
+ end
5
10
 
6
11
  def with(data = {})
7
12
  @context = LightService::Context.make(data)
13
+ @context.organized_by = organizer
8
14
  self
9
15
  end
10
16
 
@@ -24,6 +30,7 @@ module LightService
24
30
  def reduce(*actions)
25
31
  raise "No action(s) were provided" if actions.empty?
26
32
 
33
+ @context.around_actions ||= around_each_handler
27
34
  actions.flatten!
28
35
 
29
36
  actions.each_with_object(context) do |action, current_context|
@@ -53,12 +60,10 @@ module LightService
53
60
  private
54
61
 
55
62
  def invoke_action(current_context, action)
56
- around_each_handler.call(current_context) do
57
- if action.respond_to?(:call)
58
- action.call(current_context)
59
- else
60
- action.execute(current_context)
61
- end
63
+ if action.respond_to?(:call)
64
+ action.call(current_context)
65
+ else
66
+ action.execute(current_context)
62
67
  end
63
68
  end
64
69
 
@@ -4,7 +4,7 @@ module LightService
4
4
  def self.make(monitored_organizer)
5
5
  logger = monitored_organizer.logger ||
6
6
  LightService::Configuration.logger
7
- decorated = WithReducer.new
7
+ decorated = WithReducer.new(monitored_organizer)
8
8
 
9
9
  return decorated if logger.nil?
10
10
 
@@ -8,6 +8,9 @@ module LightService
8
8
  def initialize(organizer, decorated: WithReducer.new, logger:)
9
9
  @decorated = decorated
10
10
  @organizer = organizer
11
+
12
+ decorated.organizer = organizer
13
+
11
14
  @logger = logger
12
15
  @logged = false
13
16
  end
@@ -41,6 +41,10 @@ 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
@@ -65,9 +69,18 @@ module LightService
65
69
  @logger
66
70
  end
67
71
 
68
- def add_to_context(**args)
69
- args.map do |key, value|
70
- execute(->(ctx) { ctx[key.to_sym] = value })
72
+ # Set the value as a key on the context hash
73
+ # and also create convenience accessors for the keys
74
+ def add_to_context(args)
75
+ Context::ReservedKeysViaOrganizerVerifier.new(args).verify
76
+
77
+ Hash(args).map do |key, value|
78
+ context_key = lambda do |ctx|
79
+ ctx[key.to_sym] = value
80
+ ctx.define_accessor_methods_for_keys(key)
81
+ end
82
+
83
+ execute(context_key)
71
84
  end
72
85
  end
73
86
 
@@ -1,3 +1,3 @@
1
1
  module LightService
2
- VERSION = "0.13.0".freeze
2
+ VERSION = "0.17.0".freeze
3
3
  end
data/lib/light-service.rb CHANGED
@@ -13,6 +13,7 @@ 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'
17
18
  require 'light-service/organizer/iterate'
18
19
  require 'light-service/organizer/execute'
@@ -15,11 +15,16 @@ 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.5.0'
18
19
 
19
- gem.add_runtime_dependency("activesupport", ">= 3.0.0")
20
+ gem.add_runtime_dependency("activesupport", ">= 4.0.0")
20
21
 
22
+ gem.add_development_dependency("generator_spec", "~> 0.9.4")
23
+ gem.add_development_dependency("test-unit", "~> 3.0") # Needed for generator specs.
24
+ gem.add_development_dependency("appraisal", "~> 2.3")
21
25
  gem.add_development_dependency("rspec", "~> 3.0")
22
26
  gem.add_development_dependency("simplecov", "~> 0.17")
27
+ gem.add_development_dependency("codecov", "~> 0.1")
23
28
  gem.add_development_dependency("rubocop", "~> 0.68.0")
24
29
  gem.add_development_dependency("rubocop-performance", "~> 1.2.0")
25
30
  gem.add_development_dependency("pry", "~> 0.12.2")
@@ -65,6 +65,23 @@ RSpec.describe 'Action after_actions' do
65
65
  end
66
66
  end
67
67
 
68
+ context 'with callbacks' do
69
+ it 'ensures the correct :current_action is set' do
70
+ TestDoubles::TestWithCallback.after_actions = [
71
+ lambda do |ctx|
72
+ if ctx.current_action == TestDoubles::IterateCollectionAction
73
+ ctx.total -= 1000
74
+ end
75
+ end
76
+ ]
77
+
78
+ result = TestDoubles::TestWithCallback.call
79
+
80
+ expect(result.counter).to eq(3)
81
+ expect(result.total).to eq(-994)
82
+ end
83
+ end
84
+
68
85
  describe 'after_actions can be appended' do
69
86
  it 'adds to the :_after_actions collection' do
70
87
  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
@@ -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
@@ -20,6 +20,13 @@ RSpec.describe LightService::Organizer do
20
20
  expect(result.number).to eq(5)
21
21
  end
22
22
 
23
+ it "knows that it's being iterated from within an organizer" do
24
+ result = TestDoubles::TestIterate.call(:number => 1,
25
+ :counters => [1, 2, 3, 4])
26
+
27
+ expect(result.organized_by).to eq TestDoubles::TestIterate
28
+ end
29
+
23
30
  it 'will not iterate over a failed context' do
24
31
  empty_context.fail!('Something bad happened')
25
32
 
@@ -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
@@ -49,6 +49,12 @@ RSpec.describe LightService::Organizer do
49
49
  expect(result).to be_success
50
50
  end
51
51
 
52
+ it "knows that it's being conditionally reduced from within an organizer" do
53
+ result = TestReduceIf.call(:number => 2)
54
+
55
+ expect(result.organized_by).to eq TestReduceIf
56
+ end
57
+
52
58
  it 'skips actions within in its own scope' do
53
59
  org = Class.new do
54
60
  extend LightService::Organizer
@@ -40,4 +40,10 @@ RSpec.describe LightService::Organizer do
40
40
  result = TestReduceUntil.call(empty_context)
41
41
  expect(result).to be_success
42
42
  end
43
+
44
+ it "is expected to know its organizer when reducing until a condition" do
45
+ result = TestReduceUntil.call(:number => 1)
46
+
47
+ expect(result.organized_by).to eq TestReduceUntil
48
+ end
43
49
  end