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
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ class TestExecute
6
+ extend LightService::Organizer
7
+
8
+ def self.call(context)
9
+ with(context).reduce(steps)
10
+ end
11
+
12
+ def self.steps
13
+ [
14
+ TestDoubles::AddsOneAction,
15
+ execute(->(ctx) { ctx.number += 1 }),
16
+ execute(->(ctx) { ctx[:something] = 'hello' }),
17
+ TestDoubles::AddsOneAction
18
+ ]
19
+ end
20
+ end
21
+
22
+ let(:empty_context) { LightService::Context.make }
23
+
24
+ it 'calls the lambda in the execute block using the context' do
25
+ result = TestExecute.call(:number => 0)
26
+
27
+ expect(result).to be_success
28
+ expect(result.number).to eq(3)
29
+ expect(result[:something]).to eq('hello')
30
+ end
31
+
32
+ it 'will not execute a failed context' do
33
+ empty_context.fail!('Something bad happened')
34
+
35
+ result = TestExecute.call(empty_context)
36
+
37
+ expect(result).to be_failure
38
+ end
39
+
40
+ it 'does not execute over a skipped context' do
41
+ empty_context.skip_remaining!('No more needed')
42
+
43
+ result = TestExecute.call(empty_context)
44
+ expect(result).to be_success
45
+ end
46
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ let(:empty_context) { LightService::Context.make }
6
+
7
+ it 'reduces each item of a collection and singularizes the collection key' do
8
+ result = TestDoubles::TestIterate.call(:number => 1,
9
+ :counters => [1, 2, 3, 4])
10
+
11
+ expect(result).to be_success
12
+ expect(result.number).to eq(5)
13
+ end
14
+
15
+ it 'accepts a single action or organizer' do
16
+ result = TestDoubles::TestIterate.call_single(:number => 1,
17
+ :counters => [1, 2, 3, 4])
18
+
19
+ expect(result).to be_success
20
+ expect(result.number).to eq(5)
21
+ end
22
+
23
+ it 'will not iterate over a failed context' do
24
+ empty_context.fail!('Something bad happened')
25
+
26
+ result = TestDoubles::TestIterate.call(empty_context)
27
+
28
+ expect(result).to be_failure
29
+ end
30
+
31
+ it 'does not iterate over a skipped context' do
32
+ empty_context.skip_remaining!('No more needed')
33
+
34
+ result = TestDoubles::TestIterate.call(empty_context)
35
+ expect(result).to be_success
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ class TestReduceIf
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(->(ctx) { ctx.number == 1 },
16
+ TestDoubles::AddsOneAction)
17
+ ]
18
+ end
19
+ end
20
+
21
+ let(:empty_context) { LightService::Context.make }
22
+
23
+ it 'reduces if the block evaluates to true' do
24
+ result = TestReduceIf.call(:number => 0)
25
+
26
+ expect(result).to be_success
27
+ expect(result[:number]).to eq(2)
28
+ end
29
+
30
+ it 'does not reduce if the block evaluates to false' do
31
+ result = TestReduceIf.call(:number => 2)
32
+
33
+ expect(result).to be_success
34
+ expect(result[:number]).to eq(3)
35
+ end
36
+
37
+ it 'will not reduce over a failed context' do
38
+ empty_context.fail!('Something bad happened')
39
+
40
+ result = TestReduceIf.call(empty_context)
41
+
42
+ expect(result).to be_failure
43
+ end
44
+
45
+ it 'does not reduce over a skipped context' do
46
+ empty_context.skip_remaining!('No more needed')
47
+
48
+ result = TestReduceIf.call(empty_context)
49
+ expect(result).to be_success
50
+ end
51
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ class TestReduceUntil
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_until(->(ctx) { ctx.number == 3 },
15
+ TestDoubles::AddsOneAction)
16
+ ]
17
+ end
18
+ end
19
+
20
+ let(:empty_context) { LightService::Context.make }
21
+
22
+ it 'reduces until the block evaluates to true' do
23
+ context = { :number => 1 }
24
+ result = TestReduceUntil.call(context)
25
+
26
+ expect(result).to be_success
27
+ expect(result.number).to eq(3)
28
+ end
29
+
30
+ it 'does not execute on failed context' do
31
+ empty_context.fail!('Something bad happened')
32
+
33
+ result = TestReduceUntil.call(empty_context)
34
+ expect(result).to be_failure
35
+ end
36
+
37
+ it 'does not execute a skipped context' do
38
+ empty_context.skip_remaining!('No more needed')
39
+
40
+ result = TestReduceUntil.call(empty_context)
41
+ expect(result).to be_success
42
+ end
43
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe LightService::Organizer do
5
+ describe 'a simple case with a single callback' do
6
+ it 'calls the actions defined with callback' do
7
+ result = TestDoubles::TestWithCallback.call
8
+
9
+ expect(result.counter).to eq(3)
10
+ expect(result.total).to eq(6)
11
+ end
12
+ end
13
+
14
+ describe 'a more complex example with nested callbacks' do
15
+ class TestWithNestedCallback
16
+ extend LightService::Organizer
17
+
18
+ def self.call(context = {})
19
+ with(context).reduce(actions)
20
+ end
21
+
22
+ def self.actions
23
+ [
24
+ SetUpNestedContextAction,
25
+ with_callback(IterateOuterCollectionAction,
26
+ [IncrementOuterCountAction,
27
+ with_callback(TestDoubles::IterateCollectionAction,
28
+ [TestDoubles::IncrementCountAction,
29
+ TestDoubles::AddToTotalAction])])
30
+ ]
31
+ end
32
+ end
33
+
34
+ class SetUpNestedContextAction
35
+ extend LightService::Action
36
+ promises :outer_numbers, :outer_counter,
37
+ :numbers, :counter, :total
38
+
39
+ executed do |ctx|
40
+ ctx.outer_numbers = [12, 17]
41
+ ctx.outer_counter = 0
42
+ ctx.numbers = [1, 2, 3]
43
+ ctx.counter = 0
44
+ ctx.total = 0
45
+ end
46
+ end
47
+
48
+ class IterateOuterCollectionAction
49
+ extend LightService::Action
50
+ expects :outer_numbers, :callback
51
+ promises :outer_number
52
+
53
+ executed do |ctx|
54
+ ctx.outer_numbers.each do |outer_number|
55
+ ctx.outer_number = outer_number
56
+ ctx.callback.call(ctx)
57
+ end
58
+ end
59
+ end
60
+
61
+ class IncrementOuterCountAction
62
+ extend LightService::Action
63
+ expects :outer_counter
64
+
65
+ executed do |ctx|
66
+ ctx.outer_counter = ctx.outer_counter + 1
67
+ end
68
+ end
69
+
70
+ it 'calls both the action and the nested callbacks' do
71
+ result = TestWithNestedCallback.call
72
+
73
+ expect(result.outer_counter).to eq(2)
74
+ # Counts and total are the duplicates of
75
+ # what you'll see in the simple spec,
76
+ # as the internal callback logic is called
77
+ # twice due to 2 items in the outer_numbers
78
+ # collection.
79
+ expect(result.counter).to eq(6)
80
+ expect(result.total).to eq(12)
81
+ end
82
+ end
83
+
84
+ describe 'with failed or skipped context' do
85
+ class TestWithFailureCallback
86
+ extend LightService::Organizer
87
+
88
+ def self.call(context = {})
89
+ with(context).reduce(actions)
90
+ end
91
+
92
+ def self.actions
93
+ [
94
+ TestDoubles::SetUpContextAction,
95
+ with_callback(TestDoubles::IterateCollectionAction,
96
+ [TestDoubles::IncrementCountAction,
97
+ TestDoubles::FailureAction])
98
+ ]
99
+ end
100
+ end
101
+
102
+ it 'will not process the routine' do
103
+ result = TestWithFailureCallback.call
104
+
105
+ expect(result).to be_failure
106
+ expect(result.counter).to eq(1)
107
+ expect(result.total).to eq(0)
108
+ end
109
+ end
110
+ end
@@ -3,16 +3,13 @@ require 'test_doubles'
3
3
 
4
4
  describe LightService::Organizer::WithReducer do
5
5
  let(:context) { LightService::Context.make }
6
- let(:action1) { double(:action1) }
7
- let(:action2) { double(:action2) }
6
+ let(:action1) { TestDoubles::NullAction }
7
+ let(:action2) { TestDoubles::NullAction.clone }
8
8
  let(:actions) { [action1, action2] }
9
9
 
10
10
  before { context.current_action = action2 }
11
11
 
12
12
  it "reduces the provided actions" do
13
- expect(action1).to receive(:execute).with(context).and_return(context)
14
- expect(action2).to receive(:execute).with(context).and_return(context)
15
-
16
13
  result = described_class.new.with(context).reduce(actions)
17
14
 
18
15
  expect(result).to eq(context)
@@ -21,8 +18,6 @@ describe LightService::Organizer::WithReducer do
21
18
 
22
19
  it "executes a handler around each action and continues reducing" do
23
20
  expect(action1).to receive(:execute).with(context).and_return(context)
24
- expect(TestDoubles::AroundEachNullHandler).to receive(:call)
25
- .with(context).and_yield
26
21
 
27
22
  result = described_class.new.with(context)
28
23
  .around_each(TestDoubles::AroundEachNullHandler)
@@ -6,10 +6,7 @@ require_relative 'tax/provides_free_shipping_action'
6
6
 
7
7
  describe CalculatesTax do
8
8
  let(:order) { double('order') }
9
- let(:ctx) do
10
- double('ctx', :keys => [:user],
11
- :failure? => false, :skip_remaining? => false)
12
- end
9
+ let(:ctx) { LightService::Context.make(:user => nil) }
13
10
 
14
11
  it "calls the actions in order" do
15
12
  allow(LightService::Context).to receive(:make)
@@ -1,10 +1,22 @@
1
1
  $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
2
  $LOAD_PATH << File.join(File.dirname(__FILE__))
3
3
 
4
+ if ENV['RUN_COVERAGE_REPORT']
5
+ require 'simplecov'
6
+
7
+ SimpleCov.start do
8
+ add_filter 'vendor/'
9
+ add_filter %r{^/spec/}
10
+ end
11
+
12
+ SimpleCov.minimum_coverage_by_file 90
13
+ end
14
+
4
15
  require 'light-service'
5
16
  require 'light-service/testing'
6
17
  require 'ostruct'
7
18
  require 'active_support/core_ext/string'
8
19
  require 'pry'
20
+ require 'support'
9
21
 
10
22
  I18n.enforce_available_locales = true
@@ -0,0 +1,9 @@
1
+ RSpec.shared_context 'expect orchestrator warning' do
2
+ before do
3
+ expect(ActiveSupport::Deprecation)
4
+ .to receive(:warn)
5
+ .with(/^`Orchestrator#/)
6
+ .at_least(:once)
7
+ end
8
+ end
9
+
@@ -30,26 +30,15 @@ module TestDoubles
30
30
  executed(&:fail!)
31
31
  end
32
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
33
  class AddTwoOrganizer
45
34
  extend LightService::Organizer
46
35
  def self.call(context)
47
- with(context).reduce([AddOneAction, AddOneAction])
36
+ with(context).reduce([AddsOneAction, AddsOneAction])
48
37
  end
49
38
  end
50
39
 
51
40
  class AroundEachNullHandler
52
- def self.call(_action, _context)
41
+ def self.call(_action)
53
42
  yield
54
43
  end
55
44
  end
@@ -149,13 +138,11 @@ module TestDoubles
149
138
  executed do |context|
150
139
  if context.milk == :very_hot
151
140
  context.fail!("Can't make a latte from a milk that's very hot!")
152
- next context
153
141
  end
154
142
 
155
143
  if context.milk == :super_hot
156
144
  error_message = "Can't make a latte from a milk that's super hot!"
157
145
  context.fail_with_rollback!(error_message)
158
- next context
159
146
  end
160
147
 
161
148
  context[:latte] = "#{context.coffee} - with lots of #{context.milk}"
@@ -258,10 +245,9 @@ module TestDoubles
258
245
  class AddsThreeAction
259
246
  extend LightService::Action
260
247
  expects :number
261
- promises :product
262
248
 
263
249
  executed do |context|
264
- context.product = context.number + 3
250
+ context.number += 3
265
251
  end
266
252
  end
267
253
 
@@ -337,4 +323,85 @@ module TestDoubles
337
323
  ctx.final_key = ctx.expected_key
338
324
  end
339
325
  end
326
+
327
+ class NullAction
328
+ extend LightService::Action
329
+
330
+ executed { |_ctx| }
331
+ end
332
+
333
+ class TestIterate
334
+ extend LightService::Organizer
335
+
336
+ def self.call(context)
337
+ with(context)
338
+ .reduce([iterate(:counters,
339
+ [TestDoubles::AddsOneAction])])
340
+ end
341
+
342
+ def self.call_single(context)
343
+ with(context)
344
+ .reduce([iterate(:counters,
345
+ TestDoubles::AddsOneAction)])
346
+ end
347
+ end
348
+
349
+ class TestWithCallback
350
+ extend LightService::Organizer
351
+
352
+ def self.call(context = {})
353
+ with(context).reduce(actions)
354
+ end
355
+
356
+ def self.actions
357
+ [
358
+ SetUpContextAction,
359
+ with_callback(IterateCollectionAction,
360
+ [IncrementCountAction,
361
+ AddToTotalAction])
362
+ ]
363
+ end
364
+ end
365
+
366
+ class SetUpContextAction
367
+ extend LightService::Action
368
+ promises :numbers, :counter, :total
369
+
370
+ executed do |ctx|
371
+ ctx.numbers = [1, 2, 3]
372
+ ctx.counter = 0
373
+ ctx.total = 0
374
+ end
375
+ end
376
+
377
+ class IterateCollectionAction
378
+ extend LightService::Action
379
+ expects :numbers, :callback
380
+ promises :number
381
+
382
+ executed do |ctx|
383
+ ctx.numbers.each do |number|
384
+ ctx.number = number
385
+ ctx.callback.call(ctx)
386
+ end
387
+ end
388
+ end
389
+
390
+ class IncrementCountAction
391
+ extend LightService::Action
392
+ expects :counter
393
+
394
+ executed do |ctx|
395
+ ctx.counter = ctx.counter + 1
396
+ end
397
+ end
398
+
399
+ class AddToTotalAction
400
+ extend LightService::Action
401
+ expects :number, :total
402
+
403
+ executed do |ctx|
404
+ ctx.total += ctx.number
405
+ end
406
+ end
340
407
  end