light-service 0.13.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
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