light-service 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +121 -8
- data/lib/light-service.rb +2 -0
- data/lib/light-service/action.rb +21 -6
- data/lib/light-service/configuration.rb +15 -9
- data/lib/light-service/context.rb +27 -10
- data/lib/light-service/context_key_verifier.rb +6 -5
- data/lib/light-service/errors.rb +5 -0
- data/lib/light-service/localization_adapter.rb +41 -0
- data/lib/light-service/organizer/with_reducer.rb +30 -2
- data/lib/light-service/organizer/with_reducer_log_decorator.rb +18 -14
- data/lib/light-service/version.rb +1 -1
- data/light-service.gemspec +2 -0
- data/spec/acceptance/add_numbers_spec.rb +4 -44
- data/spec/acceptance/log_from_organizer_spec.rb +19 -2
- data/spec/acceptance/message_localization_spec.rb +116 -0
- data/spec/acceptance/rollback_spec.rb +134 -0
- data/spec/action_promised_keys_spec.rb +21 -9
- data/spec/action_spec.rb +8 -8
- data/spec/context_spec.rb +34 -10
- data/spec/localization_adapter_spec.rb +79 -0
- data/spec/organizer/with_reducer_spec.rb +44 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/test_doubles.rb +54 -7
- metadata +38 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69a84cc464a38e385b929286e71879cfbd7c1a30
|
4
|
+
data.tar.gz: a5c1a3782056e3ef2bc1a2da4e6ab89fe2ef8487
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8242553826eed7d16e4728dda02583b335bee7649749f18661fa5155748b21184523b20f7385ef6d5c4a3c2c644872c29d511ae54317d39974d1b4a8982e590f
|
7
|
+
data.tar.gz: 91dac04863bbff01761bda2f3ddbce112be0b36155c1a7e087b479dba9bce16547b661bc37ad3bbb6a8ea0ecd02034b9c8775b2f6361ef00d9706e7d030b62e2
|
data/README.md
CHANGED
@@ -218,12 +218,6 @@ You can turn off the logger by setting it to nil or `/dev/null`.
|
|
218
218
|
LightService::Configuration.logger = Logger.new('/dev/null')
|
219
219
|
```
|
220
220
|
|
221
|
-
In case you're using LightService with Rails, feel free use Rails logger for LightService:
|
222
|
-
|
223
|
-
```ruby
|
224
|
-
LightService::Configuration.logger = Rails.logger # or config.logger in one of the config files
|
225
|
-
```
|
226
|
-
|
227
221
|
Watch the console while you are executing the workflow through the organizer. You should see something like this:
|
228
222
|
|
229
223
|
```bash
|
@@ -290,18 +284,137 @@ class FooAction
|
|
290
284
|
|
291
285
|
executed do |context|
|
292
286
|
unless (service_call.success?)
|
293
|
-
context.fail!("Service call failed", 1001)
|
287
|
+
context.fail!("Service call failed", error_code: 1001)
|
294
288
|
end
|
295
289
|
|
296
290
|
# Do something else
|
297
291
|
|
298
292
|
unless (entity.save)
|
299
|
-
context.fail!("Saving the entity failed", 2001)
|
293
|
+
context.fail!("Saving the entity failed", error_code: 2001)
|
300
294
|
end
|
301
295
|
end
|
302
296
|
end
|
303
297
|
```
|
304
298
|
|
299
|
+
## Action Rollback
|
300
|
+
|
301
|
+
Sometimes your action has to undo what it did when an error occurs. Think about a chain of actions where you need
|
302
|
+
to persist records in your data store in one action and you have to call an external service in the next. What happens if there
|
303
|
+
is an error when you call the external service? You want to remove the records you previously saved. You can do it now with
|
304
|
+
the `rolled_back` macro.
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
class SaveEntities
|
308
|
+
include LightService::Action
|
309
|
+
expects :user
|
310
|
+
|
311
|
+
executed do |context|
|
312
|
+
context.user.save!
|
313
|
+
end
|
314
|
+
|
315
|
+
rolled_back do |context|
|
316
|
+
context.user.destroy
|
317
|
+
end
|
318
|
+
end
|
319
|
+
```
|
320
|
+
|
321
|
+
You need to call the `fail_with_rollback!` method to initiate a rollback for actions starting with the action where the failure
|
322
|
+
was triggered.
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
class CallExternalApi
|
326
|
+
include LightService::Action
|
327
|
+
|
328
|
+
executed do |context|
|
329
|
+
api_call_result = SomeAPI.save_user(context.user)
|
330
|
+
|
331
|
+
context.fail_with_rollback!("Error when calling external API") if api_call_result.failure?
|
332
|
+
end
|
333
|
+
end
|
334
|
+
```
|
335
|
+
|
336
|
+
Using the `rolled_back` macro is optional for the actions in the chain. You shouldn't care about undoing non-persisted changes.
|
337
|
+
|
338
|
+
The actions are rolled back in reversed order from the point of failure starting with the action that triggered it.
|
339
|
+
|
340
|
+
See [this](spec/acceptance/rollback_spec.rb) acceptance test to learn more about this functionality.
|
341
|
+
|
342
|
+
## Localizing Messages
|
343
|
+
|
344
|
+
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.
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
class FooAction
|
348
|
+
include LightService::Action
|
349
|
+
|
350
|
+
executed do |context|
|
351
|
+
unless service_call.success?
|
352
|
+
context.fail!(:exceeded_api_limit)
|
353
|
+
|
354
|
+
# The failure message used here equates to:
|
355
|
+
# I18n.t(:exceeded_api_limit, scope: "foo_action.light_service.failures")
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
```
|
360
|
+
|
361
|
+
This also works with nested classes via the ActiveSupport `#underscore` method, just as ActiveRecord performs localization lookups on models placed inside a module.
|
362
|
+
|
363
|
+
```ruby
|
364
|
+
module PaymentGateway
|
365
|
+
class CaptureFunds
|
366
|
+
include LightService::Action
|
367
|
+
|
368
|
+
executed do |context|
|
369
|
+
if api_service.failed?
|
370
|
+
context.fail!(:funds_not_available)
|
371
|
+
end
|
372
|
+
|
373
|
+
# this failure message equates to:
|
374
|
+
# I18n.t(:funds_not_available, scope: "payment_gateway/capture_funds.light_service.failures")
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
If you need to provide custom variables for interpolation during localization, pass that along in a hash.
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
module PaymentGateway
|
384
|
+
class CaptureFunds
|
385
|
+
include LightService::Action
|
386
|
+
|
387
|
+
executed do |context|
|
388
|
+
if api_service.failed?
|
389
|
+
context.fail!(:funds_not_available, last_four: "1234")
|
390
|
+
end
|
391
|
+
|
392
|
+
# this failure message equates to:
|
393
|
+
# I18n.t(:funds_not_available, last_four: "1234", scope: "payment_gateway/capture_funds.light_service.failures")
|
394
|
+
|
395
|
+
# the translation string itself being:
|
396
|
+
# => "Unable to process your payment for account ending in %{last_four}"
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
```
|
401
|
+
|
402
|
+
To provide your own custom adapter, use the configuration setting and subclass the default adapter LightService provides.
|
403
|
+
|
404
|
+
```ruby
|
405
|
+
LightService::Configuration.localization_adapter = MyLocalizer.new
|
406
|
+
|
407
|
+
# lib/my_localizer.rb
|
408
|
+
class MyLocalizer < LightService::LocalizationAdapter
|
409
|
+
|
410
|
+
# I just want to change the default lookup path
|
411
|
+
# => "light_service.failures.payment_gateway/capture_funds"
|
412
|
+
def i18n_scope_from_class(action_class, type)
|
413
|
+
"light_service.#{type.pluralize}.#{action_class.name.underscore}"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
```
|
417
|
+
|
305
418
|
## Requirements
|
306
419
|
|
307
420
|
This gem requires ruby 1.9.x
|
data/lib/light-service.rb
CHANGED
@@ -2,7 +2,9 @@ require 'logger'
|
|
2
2
|
|
3
3
|
require "light-service/version"
|
4
4
|
|
5
|
+
require 'light-service/errors'
|
5
6
|
require 'light-service/configuration'
|
7
|
+
require 'light-service/localization_adapter'
|
6
8
|
require 'light-service/context'
|
7
9
|
require 'light-service/context_key_verifier'
|
8
10
|
require 'light-service/organizer/with_reducer'
|
data/lib/light-service/action.rb
CHANGED
@@ -17,34 +17,49 @@ module LightService
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def expected_keys
|
20
|
-
@_expected_keys
|
20
|
+
@_expected_keys ||= []
|
21
21
|
end
|
22
22
|
|
23
23
|
def promised_keys
|
24
|
-
@_promised_keys
|
24
|
+
@_promised_keys ||= []
|
25
25
|
end
|
26
26
|
|
27
27
|
def executed
|
28
|
+
raise "`executed` macro can not be invoked again" if self.respond_to?(:execute)
|
29
|
+
|
28
30
|
define_singleton_method "execute" do |context = {}|
|
29
31
|
action_context = create_action_context(context)
|
30
32
|
return action_context if action_context.stop_processing?
|
31
|
-
action = self
|
32
33
|
|
33
|
-
|
34
|
+
# Store the action within the context
|
35
|
+
action_context.current_action = self
|
36
|
+
|
37
|
+
Context::KeyVerifier.verify_expected_keys_are_in_context(action_context)
|
34
38
|
|
35
39
|
action_context.define_accessor_methods_for_keys(expected_keys)
|
36
40
|
action_context.define_accessor_methods_for_keys(promised_keys)
|
37
41
|
|
38
42
|
yield(action_context)
|
39
43
|
|
40
|
-
Context::KeyVerifier.verify_promised_keys_are_in_context(action_context
|
44
|
+
Context::KeyVerifier.verify_promised_keys_are_in_context(action_context)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def rolled_back
|
49
|
+
raise "`rolled_back` macro can not be invoked again" if self.respond_to?(:rollback)
|
50
|
+
|
51
|
+
define_singleton_method "rollback" do |context = {}|
|
52
|
+
yield(context)
|
53
|
+
|
54
|
+
context
|
41
55
|
end
|
56
|
+
|
42
57
|
end
|
43
58
|
|
44
59
|
private
|
45
60
|
|
46
61
|
def create_action_context(context)
|
47
|
-
if context.is_a?
|
62
|
+
if context.is_a? LightService::Context
|
48
63
|
return context
|
49
64
|
end
|
50
65
|
|
@@ -2,17 +2,23 @@ module LightService
|
|
2
2
|
class Configuration
|
3
3
|
|
4
4
|
class << self
|
5
|
-
attr_writer :logger
|
6
|
-
end
|
5
|
+
attr_writer :logger, :localization_adapter
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def logger
|
8
|
+
@logger ||= _default_logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def localization_adapter
|
12
|
+
@localization_adapter ||= LocalizationAdapter.new
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
17
|
+
def _default_logger
|
18
|
+
logger = Logger.new("/dev/null")
|
19
|
+
logger.level = Logger::INFO
|
20
|
+
logger
|
21
|
+
end
|
16
22
|
end
|
17
23
|
|
18
24
|
end
|
@@ -5,16 +5,17 @@ module LightService
|
|
5
5
|
end
|
6
6
|
|
7
7
|
class Context < Hash
|
8
|
-
attr_accessor :outcome, :message, :error_code
|
8
|
+
attr_accessor :outcome, :message, :error_code, :current_action
|
9
9
|
|
10
|
-
def initialize(context={}, outcome
|
10
|
+
def initialize(context={}, outcome=Outcomes::SUCCESS, message='', error_code=nil)
|
11
11
|
@outcome, @message, @error_code = outcome, message, error_code
|
12
|
+
@skip_all = false
|
12
13
|
context.to_hash.each {|k,v| self[k] = v}
|
13
14
|
self
|
14
15
|
end
|
15
16
|
|
16
17
|
def self.make(context={})
|
17
|
-
unless context.is_a? Hash or context.is_a?
|
18
|
+
unless context.is_a? Hash or context.is_a? LightService::Context
|
18
19
|
raise ArgumentError, 'Argument must be Hash or LightService::Context'
|
19
20
|
end
|
20
21
|
|
@@ -27,7 +28,7 @@ module LightService
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def success?
|
30
|
-
@outcome ==
|
31
|
+
@outcome == Outcomes::SUCCESS
|
31
32
|
end
|
32
33
|
|
33
34
|
def failure?
|
@@ -43,9 +44,9 @@ module LightService
|
|
43
44
|
succeed!(message)
|
44
45
|
end
|
45
46
|
|
46
|
-
def succeed!(message=nil)
|
47
|
-
@message = message
|
48
|
-
@outcome =
|
47
|
+
def succeed!(message=nil, options={})
|
48
|
+
@message = Configuration.localization_adapter.success(message, current_action, options)
|
49
|
+
@outcome = Outcomes::SUCCESS
|
49
50
|
end
|
50
51
|
|
51
52
|
def set_failure!(message)
|
@@ -53,10 +54,25 @@ module LightService
|
|
53
54
|
fail!(message)
|
54
55
|
end
|
55
56
|
|
56
|
-
def fail!(message=nil,
|
57
|
-
|
57
|
+
def fail!(message=nil, options_or_error_code={})
|
58
|
+
options_or_error_code ||= {}
|
59
|
+
|
60
|
+
if options_or_error_code.is_a?(Hash)
|
61
|
+
error_code = options_or_error_code.delete(:error_code)
|
62
|
+
options = options_or_error_code
|
63
|
+
else
|
64
|
+
error_code = options_or_error_code
|
65
|
+
options = {}
|
66
|
+
end
|
67
|
+
|
68
|
+
@message = Configuration.localization_adapter.failure(message, current_action, options)
|
58
69
|
@error_code = error_code
|
59
|
-
@outcome =
|
70
|
+
@outcome = Outcomes::FAILURE
|
71
|
+
end
|
72
|
+
|
73
|
+
def fail_with_rollback!(message=nil, error_code=nil)
|
74
|
+
fail!(message, error_code)
|
75
|
+
raise FailWithRollbackError.new
|
60
76
|
end
|
61
77
|
|
62
78
|
def skip_all!(message=nil)
|
@@ -71,6 +87,7 @@ module LightService
|
|
71
87
|
def define_accessor_methods_for_keys(keys)
|
72
88
|
return if keys.nil?
|
73
89
|
keys.each do |key|
|
90
|
+
next if self.respond_to?(key.to_sym)
|
74
91
|
define_singleton_method("#{key}") { self.fetch(key) }
|
75
92
|
define_singleton_method("#{key}=") { |value| self[key] = value }
|
76
93
|
end
|
@@ -1,11 +1,10 @@
|
|
1
1
|
module LightService
|
2
|
-
class ExpectedKeysNotInContextError < StandardError; end
|
3
|
-
class PromisedKeysNotInContextError < StandardError; end
|
4
|
-
|
5
2
|
class Context
|
6
3
|
class KeyVerifier
|
7
4
|
class << self
|
8
|
-
def verify_expected_keys_are_in_context(context
|
5
|
+
def verify_expected_keys_are_in_context(context)
|
6
|
+
action = context.current_action
|
7
|
+
|
9
8
|
verify_keys_are_in_context(context, action.expected_keys) do |not_found_keys|
|
10
9
|
error_message = "expected #{format_keys(not_found_keys)} to be in the context during #{action}"
|
11
10
|
|
@@ -14,9 +13,11 @@ module LightService
|
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
17
|
-
def verify_promised_keys_are_in_context(context
|
16
|
+
def verify_promised_keys_are_in_context(context)
|
18
17
|
return context if context.failure?
|
19
18
|
|
19
|
+
action = context.current_action
|
20
|
+
|
20
21
|
verify_keys_are_in_context(context, action.promised_keys) do |not_found_keys|
|
21
22
|
error_message = "promised #{format_keys(not_found_keys)} to be in the context during #{action}"
|
22
23
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module LightService
|
2
|
+
class LocalizationAdapter
|
3
|
+
def failure(message_or_key, action_class, i18n_options={})
|
4
|
+
find_translated_message(message_or_key,
|
5
|
+
action_class,
|
6
|
+
i18n_options,
|
7
|
+
{:type => :failure})
|
8
|
+
end
|
9
|
+
|
10
|
+
def success(message_or_key, action_class, i18n_options={})
|
11
|
+
find_translated_message(message_or_key,
|
12
|
+
action_class,
|
13
|
+
i18n_options,
|
14
|
+
{:type => :success})
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def find_translated_message(message_or_key, action_class, i18n_options, type)
|
20
|
+
if message_or_key.is_a?(Symbol)
|
21
|
+
i18n_options.merge!(type)
|
22
|
+
translate(message_or_key, action_class, i18n_options)
|
23
|
+
else
|
24
|
+
message_or_key
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def translate(key, action_class, options={})
|
29
|
+
type = options.delete(:type)
|
30
|
+
|
31
|
+
scope = i18n_scope_from_class(action_class, type)
|
32
|
+
options.merge!(scope: scope)
|
33
|
+
|
34
|
+
I18n.t(key, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def i18n_scope_from_class(action_class, type)
|
38
|
+
"#{action_class.name.underscore}.light_service.#{type.to_s.pluralize}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -12,10 +12,38 @@ module LightService; module Organizer
|
|
12
12
|
actions.flatten!
|
13
13
|
|
14
14
|
actions.reduce(context) do |context, action|
|
15
|
-
|
16
|
-
|
15
|
+
begin
|
16
|
+
result = action.execute(context)
|
17
|
+
rescue FailWithRollbackError
|
18
|
+
result = reduce_rollback(actions)
|
19
|
+
ensure
|
20
|
+
# For logging
|
21
|
+
yield(context, action) if block_given?
|
22
|
+
end
|
23
|
+
|
17
24
|
result
|
18
25
|
end
|
19
26
|
end
|
27
|
+
|
28
|
+
def reduce_rollback(actions)
|
29
|
+
reversable_actions(actions)
|
30
|
+
.reverse
|
31
|
+
.reduce(context) do |context, action|
|
32
|
+
if action.respond_to?(:rollback)
|
33
|
+
action.rollback(context)
|
34
|
+
else
|
35
|
+
context
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def reversable_actions(actions)
|
43
|
+
index_of_current_action = actions.index(@context.current_action) || 0
|
44
|
+
|
45
|
+
# Reverse from the point where the fail was triggered
|
46
|
+
actions.take(index_of_current_action + 1)
|
47
|
+
end
|
20
48
|
end
|
21
49
|
end; end
|