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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +12 -10
- data/Appraisals +4 -0
- data/README.md +61 -21
- data/RELEASES.md +16 -0
- data/gemfiles/activesupport_6.gemfile +8 -0
- data/lib/light-service.rb +1 -0
- data/lib/light-service/context.rb +6 -2
- data/lib/light-service/localization_adapter.rb +1 -1
- data/lib/light-service/organizer.rb +32 -0
- data/lib/light-service/organizer/with_reducer.rb +11 -6
- data/lib/light-service/organizer/with_reducer_factory.rb +11 -7
- data/lib/light-service/organizer/with_reducer_log_decorator.rb +5 -2
- data/lib/light-service/testing/context_factory.rb +19 -22
- data/lib/light-service/version.rb +1 -1
- data/light-service.gemspec +5 -4
- data/spec/acceptance/after_actions_spec.rb +13 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
- data/spec/acceptance/fail_spec.rb +42 -16
- data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
- data/spec/acceptance/organizer/execute_spec.rb +1 -1
- data/spec/acceptance/organizer/iterate_spec.rb +7 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
- data/spec/acceptance/testing/context_factory_spec.rb +25 -4
- data/spec/action_spec.rb +8 -0
- data/spec/organizer_spec.rb +42 -14
- data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -1
- data/spec/test_doubles.rb +186 -0
- data/spec/testing/context_factory/iterate_spec.rb +39 -0
- data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
- data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
- data/spec/testing/context_factory/with_callback_spec.rb +38 -0
- data/spec/testing/context_factory_spec.rb +28 -6
- metadata +40 -15
- data/gemfiles/activesupport_3.gemfile.lock +0 -76
- data/gemfiles/activesupport_4.gemfile.lock +0 -82
- data/gemfiles/activesupport_5.gemfile.lock +0 -82
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 10ae707b6938f2898615668928728eb8e562fb9ed2f0ecf17e50deff76226a44
|
4
|
+
data.tar.gz: c870f719d663dd1aec96d895196f57b2caaae1cb9e61d073bd5fcef7ce33cff5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3062b1c4d10519f2f60c527ecedee3647d13c85419e5ad38db7ec7046b0cba68b122d044a1045724bc034c64d6e3b4afc3482cfa9bb8a87ee77ef5f847e0e728
|
7
|
+
data.tar.gz: c240aec3f4244f7ebb17f06c98e21c03495c1718d34dafcd862209fc95323bde27db6e9144e38ad285d0b83b2182713d5d1690dc43f28c95b591dd9d56aec7b7
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -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
|
data/.travis.yml
CHANGED
@@ -4,16 +4,15 @@ env:
|
|
4
4
|
- RUN_COVERAGE_REPORT=true
|
5
5
|
|
6
6
|
rvm:
|
7
|
-
- 2.
|
8
|
-
- 2.
|
9
|
-
- 2.
|
10
|
-
- 2.
|
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
|
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.
|
31
|
-
gemfile: gemfiles/
|
32
|
-
- rvm: 2.
|
33
|
-
gemfile: gemfiles/
|
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
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.
|
5
|
-
[![Code Climate](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
|
-
|
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(
|
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 =>
|
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
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
355
|
-
|
356
|
-
|
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
|
-
|
456
|
-
|
457
|
-
|
459
|
+
AnAction,
|
460
|
+
AnotherAction,
|
461
|
+
)
|
458
462
|
end
|
459
463
|
end
|
460
464
|
|
@@ -535,6 +539,15 @@ I, [DATE] INFO -- : [LightService] - ;-)
|
|
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)
|
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
|
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.
|
data/RELEASES.md
CHANGED
@@ -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
|
|
data/lib/light-service.rb
CHANGED
@@ -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
|
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
|
@@ -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
|
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.
|
35
|
+
actions.each_with_object(context) do |action, current_context|
|
29
36
|
begin
|
30
|
-
|
37
|
+
invoke_action(current_context, action)
|
31
38
|
rescue FailWithRollbackError
|
32
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|