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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 41c85f3b70d95380e2e7aa2c289f089b5415c357
4
- data.tar.gz: 66e9cb452320935e674a1f1250db139b8a7927ed
3
+ metadata.gz: 69a84cc464a38e385b929286e71879cfbd7c1a30
4
+ data.tar.gz: a5c1a3782056e3ef2bc1a2da4e6ab89fe2ef8487
5
5
  SHA512:
6
- metadata.gz: 5783a1a16b678ad68c7352c74a75c506f8b1c3c41e25b43fa77e154f818a04bcff3a6adc641dd3bba779e6fd6cfe319916f4d1c4e3eacd8c06a192705b16a8ce
7
- data.tar.gz: 0ce7a0c69be92173956db9280aac103654e961913242056c7a7a56264b6b2d22bf3322c764c5ea5d7188a09ebdac0b87de4100e62f0a35ba5508fd37ceffdb8a
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'
@@ -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
- Context::KeyVerifier.verify_expected_keys_are_in_context(action_context, action)
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, action)
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? ::LightService::Context
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
- def self.logger
9
- @logger ||= self._default_logger
10
- end
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
- def self._default_logger
13
- logger = ::Logger.new("/dev/null")
14
- logger.level = ::Logger::INFO
15
- logger
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=::LightService::Outcomes::SUCCESS, message='', error_code=nil)
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? ::LightService::Context
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 == ::LightService::Outcomes::SUCCESS
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 = ::LightService::Outcomes::SUCCESS
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, error_code=nil)
57
- @message = message
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 = ::LightService::Outcomes::FAILURE
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, action)
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, action)
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,5 @@
1
+ module LightService
2
+ class FailWithRollbackError < StandardError; end
3
+ class ExpectedKeysNotInContextError < StandardError; end
4
+ class PromisedKeysNotInContextError < StandardError; end
5
+ end
@@ -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
- result = action.execute(context)
16
- yield(context, action) if block_given?
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