light-service 0.10.2 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +6 -0
  4. data/.travis.yml +12 -10
  5. data/Appraisals +4 -0
  6. data/README.md +61 -21
  7. data/RELEASES.md +16 -0
  8. data/gemfiles/activesupport_6.gemfile +8 -0
  9. data/lib/light-service.rb +1 -0
  10. data/lib/light-service/context.rb +6 -2
  11. data/lib/light-service/localization_adapter.rb +1 -1
  12. data/lib/light-service/organizer.rb +32 -0
  13. data/lib/light-service/organizer/with_reducer.rb +11 -6
  14. data/lib/light-service/organizer/with_reducer_factory.rb +11 -7
  15. data/lib/light-service/organizer/with_reducer_log_decorator.rb +5 -2
  16. data/lib/light-service/testing/context_factory.rb +19 -22
  17. data/lib/light-service/version.rb +1 -1
  18. data/light-service.gemspec +5 -4
  19. data/spec/acceptance/after_actions_spec.rb +13 -0
  20. data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
  21. data/spec/acceptance/fail_spec.rb +42 -16
  22. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  23. data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
  24. data/spec/acceptance/organizer/execute_spec.rb +1 -1
  25. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  26. data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
  27. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  28. data/spec/acceptance/testing/context_factory_spec.rb +25 -4
  29. data/spec/action_spec.rb +8 -0
  30. data/spec/organizer_spec.rb +42 -14
  31. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  32. data/spec/spec_helper.rb +2 -1
  33. data/spec/test_doubles.rb +186 -0
  34. data/spec/testing/context_factory/iterate_spec.rb +39 -0
  35. data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
  36. data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
  37. data/spec/testing/context_factory/with_callback_spec.rb +38 -0
  38. data/spec/testing/context_factory_spec.rb +28 -6
  39. metadata +40 -15
  40. data/gemfiles/activesupport_3.gemfile.lock +0 -76
  41. data/gemfiles/activesupport_4.gemfile.lock +0 -82
  42. data/gemfiles/activesupport_5.gemfile.lock +0 -82
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 839bc255742513ba7c0cabc80bd8f72e35d4a442
4
- data.tar.gz: 5957a2d95ac83d37cb9ba40629446ba4952ab835
2
+ SHA256:
3
+ metadata.gz: 10ae707b6938f2898615668928728eb8e562fb9ed2f0ecf17e50deff76226a44
4
+ data.tar.gz: c870f719d663dd1aec96d895196f57b2caaae1cb9e61d073bd5fcef7ce33cff5
5
5
  SHA512:
6
- metadata.gz: 2c992973e9289bbf41dd73f77f9ee2e12d1e6bc2f55ad0d6af293e026a785931ee52dabe6ce2b5d85cf00afb86ffdf1218087a3cb1bdb6b525fa102ddd800a8c
7
- data.tar.gz: 7663059f0fad0ee5235306928110b6f7c5d77a057e0c34055fa9ac5eaae5756308855532023348c1a968120d8ed6b8a97649e8db5a7a3e973f3b69cc98dc1f7a
6
+ metadata.gz: 3062b1c4d10519f2f60c527ecedee3647d13c85419e5ad38db7ec7046b0cba68b122d044a1045724bc034c64d6e3b4afc3482cfa9bb8a87ee77ef5f847e0e728
7
+ data.tar.gz: c240aec3f4244f7ebb17f06c98e21c03495c1718d34dafcd862209fc95323bde27db6e9144e38ad285d0b83b2182713d5d1690dc43f28c95b591dd9d56aec7b7
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ test/version_tmp
17
17
  tmp
18
18
  vendor/bundle
19
19
  bin
20
+ .idea
@@ -1,4 +1,7 @@
1
+ require: rubocop-performance
2
+
1
3
  AllCops:
4
+ TargetRubyVersion: 2.3
2
5
  Exclude:
3
6
  - 'lib/light-service.rb'
4
7
  - 'vendor/bundle/**/*'
@@ -46,3 +49,6 @@ Metrics/BlockLength:
46
49
 
47
50
  Layout/TrailingBlankLines:
48
51
  Enabled: false
52
+
53
+ Layout/EndOfLine:
54
+ EnforcedStyle: lf
@@ -4,16 +4,15 @@ env:
4
4
  - RUN_COVERAGE_REPORT=true
5
5
 
6
6
  rvm:
7
- - 2.2.2
8
- - 2.3.3
9
- - 2.4.1
10
- - 2.5.0
7
+ - 2.4.2
8
+ - 2.5.3
9
+ - 2.6.0
10
+ - 2.7.0
11
11
 
12
12
  before_install:
13
13
  - 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc'
14
14
  - gem install bundler
15
- - bundle update simplecov
16
- - bundle install --path vendor/bundle
15
+ - bundle install --clean --path vendor/bundle
17
16
 
18
17
  # uncomment this line if your project needs to run something other than `rake`:
19
18
  script:
@@ -24,10 +23,13 @@ gemfile:
24
23
  - gemfiles/activesupport_3.gemfile
25
24
  - gemfiles/activesupport_4.gemfile
26
25
  - gemfiles/activesupport_5.gemfile
26
+ - gemfiles/activesupport_6.gemfile
27
27
 
28
28
  matrix:
29
29
  exclude:
30
- - rvm: 2.1.8
31
- gemfile: gemfiles/activesupport_5.gemfile
32
- - rvm: 2.2.2
33
- gemfile: gemfiles/activesupport_5.gemfile
30
+ - rvm: 2.4.2
31
+ gemfile: gemfiles/activesupport_6.gemfile
32
+ - rvm: 2.7.0
33
+ gemfile: gemfiles/activesupport_3.gemfile
34
+ - rvm: 2.7.0
35
+ gemfile: gemfiles/activesupport_4.gemfile
data/Appraisals CHANGED
@@ -9,3 +9,7 @@ end
9
9
  appraise "activesupport-5" do
10
10
  gem "activesupport", "~> 5.0"
11
11
  end
12
+
13
+ appraise "activesupport-6" do
14
+ gem "activesupport", "~> 6.0"
15
+ end
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  ![LightService](https://raw.githubusercontent.com/adomokos/light-service/master/resources/light-service.png)
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/light-service.svg)](https://rubygems.org/gems/light-service)
4
- [![Build Status](https://secure.travis-ci.org/adomokos/light-service.png)](http://travis-ci.org/adomokos/light-service)
5
- [![Code Climate](https://codeclimate.com/github/adomokos/light-service.png)](https://codeclimate.com/github/adomokos/light-service)
6
- [![Dependency Status](https://beta.gemnasium.com/badges/github.com/adomokos/light-service.svg)](https://beta.gemnasium.com/projects/github.com/adomokos/light-service)
4
+ [![Build Status](https://secure.travis-ci.org/adomokos/light-service.svg)](http://travis-ci.org/adomokos/light-service)
5
+ [![Code Climate](https://codeclimate.com/github/adomokos/light-service.svg)](https://codeclimate.com/github/adomokos/light-service)
7
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT)
7
+ [![Download Count](https://ruby-gem-downloads-badge.herokuapp.com/light-service?type=total)](https://rubygems.org/gems/light-service)
8
8
 
9
9
  <br><br>
10
10
 
@@ -79,7 +79,7 @@ Wouldn't it be nice to see this instead?
79
79
  (
80
80
  LooksUpTaxPercentage,
81
81
  CalculatesOrderTax,
82
- ChecksFreeShipping
82
+ ProvidesFreeShipping
83
83
  )
84
84
  ```
85
85
 
@@ -257,12 +257,12 @@ Check out this example:
257
257
 
258
258
  ```ruby
259
259
  class LogDuration
260
- def self.call(action, context)
260
+ def self.call(context)
261
261
  start_time = Time.now
262
262
  result = yield
263
263
  duration = Time.now - start_time
264
264
  LightService::Configuration.logger.info(
265
- :action => action,
265
+ :action => context.current_action,
266
266
  :duration => duration
267
267
  )
268
268
 
@@ -295,14 +295,16 @@ Consider this code:
295
295
  class SomeOrganizer
296
296
  extend LightService::Organizer
297
297
 
298
- def call(ctx)
298
+ def self.call(ctx)
299
299
  with(ctx).reduce(actions)
300
300
  end
301
301
 
302
- def actions
303
- OneAction,
304
- TwoAction,
305
- ThreeAction
302
+ def self.actions
303
+ [
304
+ OneAction,
305
+ TwoAction,
306
+ ThreeAction
307
+ ]
306
308
  end
307
309
  end
308
310
 
@@ -346,14 +348,16 @@ class SomeOrganizer
346
348
  end
347
349
  end)
348
350
 
349
- def call(ctx)
351
+ def self.call(ctx)
350
352
  with(ctx).reduce(actions)
351
353
  end
352
354
 
353
- def actions
354
- OneAction,
355
- TwoAction,
356
- ThreeAction
355
+ def self.actions
356
+ [
357
+ OneAction,
358
+ TwoAction,
359
+ ThreeAction
360
+ ]
357
361
  end
358
362
  end
359
363
 
@@ -452,9 +456,9 @@ class AnOrganizer
452
456
 
453
457
  def self.call(order)
454
458
  with(:order => order).reduce(
455
- AnAction,
456
- AnotherAction,
457
- )
459
+ AnAction,
460
+ AnotherAction,
461
+ )
458
462
  end
459
463
  end
460
464
 
@@ -535,6 +539,15 @@ I, [DATE] INFO -- : [LightService] - ;-) has de
535
539
  I, [DATE] INFO -- : [LightService] - context message: Can't make a latte with a fatty milk like that!
536
540
  ```
537
541
 
542
+ You can specify the logger on the organizer level, so the organizer does not use the global logger.
543
+
544
+ ```ruby
545
+ class FooOrganizer
546
+ extend LightService::Organizer
547
+ log_with Logger.new("/my/special.log")
548
+ end
549
+ ```
550
+
538
551
  ## Error Codes
539
552
  You can add some more structure to your error handling by taking advantage of error codes in the context.
540
553
  Normally, when something goes wrong in your actions, you fail the process by setting the context to failure:
@@ -611,7 +624,28 @@ Using the `rolled_back` macro is optional for the actions in the chain. You shou
611
624
 
612
625
  The actions are rolled back in reversed order from the point of failure starting with the action that triggered it.
613
626
 
614
- See [this](spec/acceptance/rollback_spec.rb) acceptance test to learn more about this functionality.
627
+ See [this acceptance test](spec/acceptance/rollback_spec.rb) to learn more about this functionality.
628
+
629
+ You may find yourself directly using an action that can roll back by calling `.execute` instead of using it from within an Organizer.
630
+ If this action fails and attempts a rollback, a `FailWithRollbackError` exception will be raised. This is so that the organizer can
631
+ rollback the actions one by one. If you don't want to wrap your call to the action with a `begin, rescue FailWithRollbackError`
632
+ block, you can introspect the context like so, and keep your usage of the action clean:
633
+
634
+ ```ruby
635
+ class FooAction
636
+ extend LightService::Action
637
+
638
+ executed do |context|
639
+ # context.organized_by will be nil if run from an action,
640
+ # or will be the class name if run from an organizer
641
+ if context.organized_by.nil?
642
+ context.fail!
643
+ else
644
+ context.fail_with_rollback!
645
+ end
646
+ end
647
+ end
648
+ ```
615
649
 
616
650
  ## Localizing Messages
617
651
  By default LightService provides a mechanism for easily translating your error or success messages via I18n. You can also provide your own custom localization adapter if your application's logic is more complex than what is shown here.
@@ -746,13 +780,15 @@ end
746
780
 
747
781
  This code is much easier to reason about, it's less noisy and it captures the goal of LightService well: simple, declarative code that's easy to understand.
748
782
 
749
- The 5 different orchestrator constructs an organizer can have:
783
+ The 7 different orchestrator constructs an organizer can have:
750
784
 
751
785
  1. `reduce_until`
752
786
  2. `reduce_if`
753
787
  3. `iterate`
754
788
  4. `execute`
755
789
  5. `with_callback`
790
+ 6. `add_to_context`
791
+ 7. `add_aliases`
756
792
 
757
793
  `reduce_until` behaves like a while loop in imperative languages, it iterates until the provided predicate in the lambda evaluates to true. Take a look at [this acceptance test](spec/acceptance/organizer/reduce_until_spec.rb) to see how it's used.
758
794
 
@@ -764,6 +800,10 @@ To take advantage of another organizer or action, you might need to tweak the co
764
800
 
765
801
  Use `with_callback` when you want to execute actions with a deferred and controlled callback. It works similar to a Sax parser, I've used it for processing large files. The advantage of it is not having to keep large amount of data in memory. See [this acceptance test](spec/acceptance/organizer/with_callback_spec.rb) as a working example.
766
802
 
803
+ `add_to_context` can add key-value pairs on the fly to the context. This functionality is useful when you need a value injected into the context under a specific key right before the subsequent actions are executed. [This test](spec/acceptance/organizer/add_to_context_spec.rb) describes its functionality.
804
+
805
+ Your action needs a certain key in the context but it's under a different one? Use the function `add_aliases` to alias an existing key in the context under the desired key. Take a look at [this test](spec/acceptance/organizer/add_aliases_spec.rb) to see an example.
806
+
767
807
  ## ContextFactory for Faster Action Testing
768
808
 
769
809
  As the complexity of your workflow increases, you will find yourself spending more and more time creating a context (LightService::Context it is) for your action tests. Some of this code can be reused by clever factories, but still, you are using a context that is artificial, and can be different from what the previous actions produced. This is especially true, when you use LightService in ETLs, where you start out with initial data and your actions are mutating its state.
@@ -1,5 +1,21 @@
1
1
  A brief list of new features and changes introduced with the specified version.
2
2
 
3
+ ### 0.14.0
4
+ * [Add 'organized_by' to context](https://github.com/adomokos/light-service/pull/192) - Context now have an #organized_by attribute
5
+
6
+ ### 0.13.0
7
+ * [Add 'add_to_context' and 'add_aliases'](https://github.com/adomokos/light-service/pull/172) - Updating Ruby compatibility, minor fixes
8
+
9
+ ### 0.12.0
10
+ * [Per organizer logger](https://github.com/adomokos/light-service/pull/162)
11
+ * [Fix 'fail_and_return!' not accepting 'error_code' option](https://github.com/adomokos/light-service/pull/168)
12
+
13
+ ### 0.11.0
14
+ * [Switch to 'each_with_object' in WithReducer](https://github.com/adomokos/light-service/pull/149).
15
+
16
+ ### 0.10.3
17
+ * [Adding ContextFactory](https://github.com/adomokos/light-service/pull/147).
18
+
3
19
  ### 0.10.2
4
20
  * [Revert 0.10.1](https://github.com/adomokos/light-service/pull/146), it breaks tests in our apps :-(.
5
21
 
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activesupport", "~> 6.0"
6
+ gem "appraisal", "~> 2.0"
7
+
8
+ gemspec :path => "../"
@@ -1,4 +1,5 @@
1
1
  require 'logger'
2
+ require 'active_support/core_ext/string'
2
3
 
3
4
  require 'light-service/version'
4
5
 
@@ -8,7 +8,7 @@ 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, :organized_by
12
12
 
13
13
  def initialize(context = {},
14
14
  outcome = Outcomes::SUCCESS,
@@ -18,6 +18,7 @@ module LightService
18
18
  @message = message
19
19
  @error_code = error_code
20
20
  @skip_remaining = false
21
+
21
22
  context.to_hash.each { |k, v| self[k] = v }
22
23
  end
23
24
 
@@ -88,7 +89,7 @@ module LightService
88
89
 
89
90
  def fail_and_return!(*args)
90
91
  fail!(*args)
91
- throw(:jump_when_failed, *args)
92
+ throw(:jump_when_failed)
92
93
  end
93
94
 
94
95
  def fail_with_rollback!(message = nil, error_code = nil)
@@ -115,8 +116,10 @@ module LightService
115
116
 
116
117
  def define_accessor_methods_for_keys(keys)
117
118
  return if keys.nil?
119
+
118
120
  keys.each do |key|
119
121
  next if respond_to?(key.to_sym)
122
+
120
123
  define_singleton_method(key.to_s) { fetch(key) }
121
124
  define_singleton_method("#{key}=") { |value| self[key] = value }
122
125
  end
@@ -161,6 +164,7 @@ module LightService
161
164
 
162
165
  def check_nil(value)
163
166
  return 'nil' unless value
167
+
164
168
  "'#{value}'"
165
169
  end
166
170
  end
@@ -34,7 +34,7 @@ module LightService
34
34
  scope = i18n_scope_from_class(action_class, type)
35
35
  options[:scope] = scope
36
36
 
37
- I18n.t(key, options)
37
+ I18n.t(key, **options)
38
38
  end
39
39
 
40
40
  def i18n_scope_from_class(action_class, type)
@@ -56,6 +56,24 @@ module LightService
56
56
  def with_callback(action, steps)
57
57
  WithCallback.run(self, action, steps)
58
58
  end
59
+
60
+ def log_with(logger)
61
+ @logger = logger
62
+ end
63
+
64
+ def logger
65
+ @logger
66
+ end
67
+
68
+ def add_to_context(**args)
69
+ args.map do |key, value|
70
+ execute(->(ctx) { ctx[key.to_sym] = value })
71
+ end
72
+ end
73
+
74
+ def add_aliases(args)
75
+ execute(->(ctx) { ctx.assign_aliases(ctx.aliases.merge(args)) })
76
+ end
59
77
  end
60
78
 
61
79
  module Macros
@@ -63,6 +81,8 @@ module LightService
63
81
  @aliases = key_hash
64
82
  end
65
83
 
84
+ # This looks like an accessor,
85
+ # but it's used as a macro in the Organizer
66
86
  def before_actions(*logic)
67
87
  self.before_actions = logic
68
88
  end
@@ -71,6 +91,13 @@ module LightService
71
91
  @before_actions = [logic].flatten
72
92
  end
73
93
 
94
+ def append_before_actions(action)
95
+ @before_actions ||= []
96
+ @before_actions.push(action)
97
+ end
98
+
99
+ # This looks like an accessor,
100
+ # but it's used as a macro in the Organizer
74
101
  def after_actions(*logic)
75
102
  self.after_actions = logic
76
103
  end
@@ -78,6 +105,11 @@ module LightService
78
105
  def after_actions=(logic)
79
106
  @after_actions = [logic].flatten
80
107
  end
108
+
109
+ def append_after_actions(action)
110
+ @after_actions ||= []
111
+ @after_actions.push(action)
112
+ end
81
113
  end
82
114
  end
83
115
  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
 
@@ -23,19 +29,18 @@ module LightService
23
29
 
24
30
  def reduce(*actions)
25
31
  raise "No action(s) were provided" if actions.empty?
32
+
26
33
  actions.flatten!
27
34
 
28
- actions.reduce(context) do |current_context, action|
35
+ actions.each_with_object(context) do |action, current_context|
29
36
  begin
30
- result = invoke_action(current_context, action)
37
+ invoke_action(current_context, action)
31
38
  rescue FailWithRollbackError
32
- result = reduce_rollback(actions)
39
+ reduce_rollback(actions)
33
40
  ensure
34
41
  # For logging
35
42
  yield(current_context, action) if block_given?
36
43
  end
37
-
38
- result
39
44
  end
40
45
  end
41
46
 
@@ -2,13 +2,17 @@ module LightService
2
2
  module Organizer
3
3
  class WithReducerFactory
4
4
  def self.make(monitored_organizer)
5
- if LightService::Configuration.logger.nil?
6
- # :nocov:
7
- WithReducer.new
8
- # :nocov:
9
- else
10
- WithReducerLogDecorator.new(monitored_organizer, WithReducer.new)
11
- end
5
+ logger = monitored_organizer.logger ||
6
+ LightService::Configuration.logger
7
+ decorated = WithReducer.new(monitored_organizer)
8
+
9
+ return decorated if logger.nil?
10
+
11
+ WithReducerLogDecorator.new(
12
+ monitored_organizer,
13
+ :decorated => decorated,
14
+ :logger => logger
15
+ )
12
16
  end
13
17
  end
14
18
  end