functional-light-service 6.0.0 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +26 -0
  4. data/README.md +130 -11
  5. data/functional-light-service.gemspec +3 -1
  6. data/lib/functional-light-service/action.rb +48 -0
  7. data/lib/functional-light-service/configuration.rb +12 -2
  8. data/lib/functional-light-service/context/key_verifier.rb +1 -1
  9. data/lib/functional-light-service/context.rb +15 -2
  10. data/lib/functional-light-service/errors.rb +1 -0
  11. data/lib/functional-light-service/functional/sequencer.rb +144 -0
  12. data/lib/functional-light-service/i18n/localization_adapter.rb +50 -0
  13. data/lib/functional-light-service/localization_adapter.rb +19 -28
  14. data/lib/functional-light-service/localization_map.rb +9 -0
  15. data/lib/functional-light-service/organizer/reduce_case.rb +50 -0
  16. data/lib/functional-light-service/organizer/reduce_if_else.rb +23 -0
  17. data/lib/functional-light-service/organizer/reduce_until.rb +1 -1
  18. data/lib/functional-light-service/organizer/reduce_while.rb +31 -0
  19. data/lib/functional-light-service/organizer/scoped_reducable.rb +2 -2
  20. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +2 -1
  21. data/lib/functional-light-service/organizer.rb +18 -3
  22. data/lib/functional-light-service/version.rb +1 -1
  23. data/lib/functional-light-service.rb +6 -0
  24. data/spec/acceptance/organizer/add_to_context_spec.rb +24 -0
  25. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +22 -0
  26. data/spec/acceptance/organizer/execute_spec.rb +21 -0
  27. data/spec/acceptance/organizer/reduce_case_spec.rb +65 -0
  28. data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
  29. data/spec/acceptance/organizer/reduce_while_spec.rb +96 -0
  30. data/spec/acceptance/skip_all_remaining_spec.rb +139 -0
  31. data/spec/action_optional_expected_keys_spec.rb +107 -0
  32. data/spec/context/inspect_spec.rb +13 -3
  33. data/spec/context_spec.rb +12 -0
  34. data/spec/i18n_localization_adapter_spec.rb +83 -0
  35. data/spec/lib/deterministic/sequencer_spec.rb +506 -0
  36. data/spec/localization_adapter_spec.rb +66 -83
  37. data/spec/spec_helper.rb +3 -0
  38. data/spec/test_doubles.rb +28 -0
  39. metadata +27 -14
@@ -1,48 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'i18n'
3
+ require 'dry/inflector'
4
4
 
5
5
  module FunctionalLightService
6
+ # Adapter built-in basato su LocalizationMap: nessuna dipendenza da I18n.
7
+ # Viene selezionato da Configuration quando la costante ::I18n non esiste
6
8
  class LocalizationAdapter
7
- def failure(message_or_key, action_class, i18n_options = {})
9
+ INFLECTOR = Dry::Inflector.new
10
+
11
+ def failure(message_or_key, action_class, options = {})
8
12
  find_translated_message(message_or_key,
9
- action_class,
10
- i18n_options,
11
- :type => :failure)
13
+ INFLECTOR.underscore(action_class.to_s),
14
+ options.merge(:type => :failures))
12
15
  end
13
16
 
14
- def success(message_or_key, action_class, i18n_options = {})
17
+ def success(message_or_key, action_class, options = {})
15
18
  find_translated_message(message_or_key,
16
- action_class,
17
- i18n_options,
18
- :type => :success)
19
+ INFLECTOR.underscore(action_class.to_s),
20
+ options.merge(:type => :successes))
19
21
  end
20
22
 
21
23
  private
22
24
 
23
- def find_translated_message(message_or_key,
24
- action_class,
25
- i18n_options,
26
- type)
25
+ def find_translated_message(message_or_key, action_class, options)
27
26
  if message_or_key.is_a?(Symbol)
28
- translate(message_or_key, action_class, i18n_options.merge(type))
27
+ FunctionalLightService::LocalizationMap.instance.dig(
28
+ FunctionalLightService::Configuration.locale,
29
+ action_class.to_sym,
30
+ :light_service,
31
+ options[:type],
32
+ message_or_key
33
+ )
29
34
  else
30
35
  message_or_key
31
36
  end
32
37
  end
33
-
34
- def translate(key, action_class, options = {})
35
- type = options.delete(:type)
36
-
37
- scope = i18n_scope_from_class(action_class, type)
38
- options[:scope] = scope
39
-
40
- I18n.t(key, **options)
41
- end
42
-
43
- def i18n_scope_from_class(action_class, type)
44
- inflector = Dry::Inflector.new
45
- "#{inflector.underscore(action_class.name)}.light_service.#{inflector.pluralize(type)}"
46
- end
47
38
  end
48
39
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module FunctionalLightService
6
+ class LocalizationMap < Hash
7
+ include ::Singleton
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Organizer
5
+ class ReduceCase
6
+ extend ScopedReducable
7
+
8
+ class Arguments
9
+ attr_reader :value, :when, :else
10
+
11
+ def initialize(**args)
12
+ validate_arguments(**args)
13
+ @value = args[:value]
14
+ @when = args[:when]
15
+ @else = args[:else]
16
+ end
17
+
18
+ private
19
+
20
+ # rubocop:disable Style/MultilineIfModifier
21
+ def validate_arguments(**args)
22
+ raise(
23
+ ArgumentError,
24
+ "Expected keyword arguments: [:value, :when, :else]. Given: #{args.keys}"
25
+ ) unless args.keys.intersection(mandatory_arguments).count == mandatory_arguments.count
26
+ end
27
+ # rubocop:enable Style/MultilineIfModifier
28
+
29
+ def mandatory_arguments
30
+ %i[value when else]
31
+ end
32
+ end
33
+
34
+ def self.run(organizer, **args)
35
+ arguments = Arguments.new(**args)
36
+
37
+ ->(ctx) do
38
+ return ctx if ctx.stop_processing?
39
+
40
+ matched_case = arguments.when.keys.find { |k| k.eql?(ctx[arguments.value]) }
41
+ steps = arguments.when[matched_case] || arguments.else
42
+
43
+ ctx = scoped_reduce(organizer, ctx, steps)
44
+
45
+ ctx
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Organizer
5
+ class ReduceIfElse
6
+ extend ScopedReducable
7
+
8
+ def self.run(organizer, condition_block, if_steps, else_steps)
9
+ ->(ctx) do
10
+ return ctx if ctx.stop_processing?
11
+
12
+ ctx = if condition_block.call(ctx)
13
+ scoped_reduce(organizer, ctx, if_steps)
14
+ else
15
+ scoped_reduce(organizer, ctx, else_steps)
16
+ end
17
+
18
+ ctx
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -11,7 +11,7 @@ module FunctionalLightService
11
11
 
12
12
  loop do
13
13
  ctx = scoped_reduce(organizer, ctx, steps)
14
- break if condition_block.call(ctx) || ctx.failure?
14
+ break if condition_block.call(ctx) || ctx.stop_processing?
15
15
  end
16
16
 
17
17
  ctx
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Organizer
5
+ class ReduceWhile
6
+ def self.run(organizer, condition_block, steps)
7
+ ->(ctx) do
8
+ return ctx if ctx.stop_processing?
9
+
10
+ reset_skip(ctx)
11
+
12
+ Array(steps).each do |step|
13
+ break unless condition_block.call(ctx)
14
+
15
+ ctx = organizer.with(ctx).reduce([step])
16
+ break if ctx.stop_processing?
17
+ end
18
+
19
+ reset_skip(ctx)
20
+
21
+ ctx
22
+ end
23
+ end
24
+
25
+ def self.reset_skip(ctx)
26
+ ctx.reset_skip_remaining! unless ctx.failure? || ctx.skip_all_remaining?
27
+ end
28
+ private_class_method :reset_skip
29
+ end
30
+ end
31
+ end
@@ -4,9 +4,9 @@ module FunctionalLightService
4
4
  module Organizer
5
5
  module ScopedReducable
6
6
  def scoped_reduce(organizer, ctx, steps)
7
- ctx.reset_skip_remaining! unless ctx.failure?
7
+ ctx.reset_skip_remaining! unless ctx.failure? || ctx.skip_all_remaining?
8
8
  ctx = organizer.with(ctx).reduce([steps])
9
- ctx.reset_skip_remaining! unless ctx.failure?
9
+ ctx.reset_skip_remaining! unless ctx.failure? || ctx.skip_all_remaining?
10
10
 
11
11
  ctx
12
12
  end
@@ -93,7 +93,8 @@ module FunctionalLightService
93
93
  end
94
94
 
95
95
  def skip_remaining?(context)
96
- context.respond_to?(:skip_remaining?) && context.skip_remaining?
96
+ (context.respond_to?(:skip_remaining?) && context.skip_remaining?) ||
97
+ (context.respond_to?(:skip_all_remaining?) && context.skip_all_remaining?)
97
98
  end
98
99
 
99
100
  def write_skip_remaining_log(context, action)
@@ -36,16 +36,28 @@ module FunctionalLightService
36
36
  ReduceIf.run(self, condition_block, steps)
37
37
  end
38
38
 
39
+ def reduce_if_else(condition_block, if_steps, else_steps)
40
+ ReduceIfElse.run(self, condition_block, if_steps, else_steps)
41
+ end
42
+
39
43
  def reduce_until(condition_block, steps)
40
44
  ReduceUntil.run(self, condition_block, steps)
41
45
  end
42
46
 
47
+ def reduce_case(**args)
48
+ ReduceCase.run(self, **args)
49
+ end
50
+
51
+ def reduce_while(condition_block, steps)
52
+ ReduceWhile.run(self, condition_block, steps)
53
+ end
54
+
43
55
  def iterate(collection_key, steps)
44
56
  Iterate.run(self, collection_key, steps)
45
57
  end
46
58
 
47
- def execute(code_block)
48
- Execute.run(code_block)
59
+ def execute(code_block = nil, &block)
60
+ Execute.run(code_block || block)
49
61
  end
50
62
 
51
63
  def with_callback(action, steps)
@@ -62,7 +74,10 @@ module FunctionalLightService
62
74
 
63
75
  def add_to_context(**args)
64
76
  args.map do |key, value|
65
- execute(->(ctx) { ctx[key.to_sym] = value })
77
+ execute(->(ctx) do
78
+ ctx[key.to_sym] = value
79
+ ctx.define_accessor_methods_for_keys([key])
80
+ end)
66
81
  end
67
82
  end
68
83
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FunctionalLightService
4
- VERSION = "6.0.0"
4
+ VERSION = "6.2.0"
5
5
  end
@@ -12,9 +12,12 @@ require 'functional-light-service/functional/enum'
12
12
  require 'functional-light-service/functional/result'
13
13
  require 'functional-light-service/functional/option'
14
14
  require 'functional-light-service/functional/null'
15
+ require 'functional-light-service/functional/sequencer'
15
16
  require 'functional-light-service/errors'
16
17
  require 'functional-light-service/configuration'
18
+ require 'functional-light-service/i18n/localization_adapter'
17
19
  require 'functional-light-service/localization_adapter'
20
+ require 'functional-light-service/localization_map'
18
21
  require 'functional-light-service/context'
19
22
  require 'functional-light-service/context/key_verifier'
20
23
  require 'functional-light-service/organizer/scoped_reducable'
@@ -22,7 +25,10 @@ require 'functional-light-service/organizer/with_reducer'
22
25
  require 'functional-light-service/organizer/with_reducer_log_decorator'
23
26
  require 'functional-light-service/organizer/with_reducer_factory'
24
27
  require 'functional-light-service/organizer/reduce_if'
28
+ require 'functional-light-service/organizer/reduce_if_else'
29
+ require 'functional-light-service/organizer/reduce_case'
25
30
  require 'functional-light-service/organizer/reduce_until'
31
+ require 'functional-light-service/organizer/reduce_while'
26
32
  require 'functional-light-service/organizer/iterate'
27
33
  require 'functional-light-service/organizer/execute'
28
34
  require 'functional-light-service/organizer/with_callback'
@@ -27,4 +27,28 @@ RSpec.describe FunctionalLightService::Organizer do
27
27
  expect(result.number).to eq(1)
28
28
  expect(result[:something]).to eq("hello")
29
29
  end
30
+
31
+ it "defines accessors for the added keys" do
32
+ result = TestAddToContext.call
33
+
34
+ expect(result.something).to eq("hello")
35
+ end
36
+
37
+ it "raises when the added key conflicts with a Context method" do
38
+ organizer = Class.new do
39
+ extend FunctionalLightService::Organizer
40
+
41
+ def self.call
42
+ reduce(steps)
43
+ end
44
+
45
+ def self.steps
46
+ [add_to_context(:message => "boom")]
47
+ end
48
+ end
49
+
50
+ expect { organizer.call }
51
+ .to raise_error(FunctionalLightService::ReservedKeysInContextError,
52
+ /:message conflicts/)
53
+ end
30
54
  end
@@ -31,6 +31,21 @@ RSpec.describe FunctionalLightService::Organizer do
31
31
  end
32
32
  end
33
33
 
34
+ class TestSkipAllFromNestedScope
35
+ extend FunctionalLightService::Organizer
36
+
37
+ def self.call
38
+ with(:number => 1)
39
+ .reduce([
40
+ reduce_if(->(_ctx) { true }, [
41
+ TestDoubles::AddsOneAction,
42
+ execute(lambda(&:skip_all_remaining!))
43
+ ]),
44
+ TestDoubles::AddsOneAction
45
+ ])
46
+ end
47
+ end
48
+
34
49
  class TestContextFailure
35
50
  extend FunctionalLightService::Organizer
36
51
 
@@ -59,6 +74,13 @@ RSpec.describe FunctionalLightService::Organizer do
59
74
  expect(result[:number]).to eq(3)
60
75
  end
61
76
 
77
+ it 'does not reset skip_all_remaining at the end of a nested scope' do
78
+ result = TestSkipAllFromNestedScope.call
79
+
80
+ expect(result).to be_success
81
+ expect(result[:number]).to eq(2)
82
+ end
83
+
62
84
  it 'respects failure across all nestings' do
63
85
  result = TestContextFailure.call
64
86
 
@@ -43,4 +43,25 @@ RSpec.describe FunctionalLightService::Organizer do
43
43
  result = TestExecute.call(empty_context)
44
44
  expect(result).to be_success
45
45
  end
46
+
47
+ it 'accepts a block instead of a lambda' do
48
+ organizer = Class.new do
49
+ extend FunctionalLightService::Organizer
50
+
51
+ def self.call(context)
52
+ with(context).reduce(steps)
53
+ end
54
+
55
+ def self.steps
56
+ [
57
+ execute { |ctx| ctx[:number] += 1 }
58
+ ]
59
+ end
60
+ end
61
+
62
+ result = organizer.call(:number => 1)
63
+
64
+ expect(result).to be_success
65
+ expect(result[:number]).to eq(2)
66
+ end
46
67
  end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe FunctionalLightService::Organizer do
5
+ class TestReduceCase
6
+ extend FunctionalLightService::Organizer
7
+
8
+ def self.call(context)
9
+ with(context).reduce(actions)
10
+ end
11
+
12
+ def self.actions
13
+ [
14
+ reduce_case(
15
+ :value => :incr_num,
16
+ :when => {
17
+ :one => [TestDoubles::AddsOneAction],
18
+ :two => [TestDoubles::AddsTwoAction],
19
+ :three => [TestDoubles::AddsThreeAction]
20
+ },
21
+ :else => [TestDoubles::FailureAction]
22
+ )
23
+ ]
24
+ end
25
+ end
26
+
27
+ it 'adds one if the incr_num is one' do
28
+ result = TestReduceCase.call(:number => 0, :incr_num => :one)
29
+
30
+ expect(result).to be_success
31
+ expect(result[:number]).to eq(1)
32
+ end
33
+
34
+ it 'adds two if the incr_num is two' do
35
+ result = TestReduceCase.call(:number => 0, :incr_num => :two)
36
+
37
+ expect(result).to be_success
38
+ expect(result[:number]).to eq(2)
39
+ end
40
+
41
+ it 'adds three if the incr_num is three' do
42
+ result = TestReduceCase.call(:number => 0, :incr_num => :three)
43
+
44
+ expect(result).to be_success
45
+ expect(result[:number]).to eq(3)
46
+ end
47
+
48
+ it 'will fail if the incr_num is neither one, two, or three' do
49
+ result = TestReduceCase.call(:number => 0, :incr_num => :four)
50
+
51
+ expect(result).to be_failure
52
+ end
53
+
54
+ it 'raises ArgumentError when a mandatory keyword argument is missing' do
55
+ expect do
56
+ Class.new do
57
+ extend FunctionalLightService::Organizer
58
+
59
+ def self.actions
60
+ [reduce_case(:value => :incr_num, :when => {})]
61
+ end
62
+ end.actions
63
+ end.to raise_error(ArgumentError, /Expected keyword arguments/)
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe FunctionalLightService::Organizer do
5
+ class TestReduceIfElse
6
+ extend FunctionalLightService::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_else(
16
+ ->(ctx) { ctx.number == 1 },
17
+ [TestDoubles::AddsOneAction],
18
+ [TestDoubles::AddsTwoAction]
19
+ )
20
+ ]
21
+ end
22
+ end
23
+
24
+ let(:empty_context) { FunctionalLightService::Context.make }
25
+
26
+ it 'reduces the if_steps if the condition is true' do
27
+ result = TestReduceIfElse.call(:number => 0)
28
+
29
+ expect(result).to be_success
30
+ expect(result[:number]).to eq(2)
31
+ end
32
+
33
+ it 'reduces the else_steps if the condition is false' do
34
+ result = TestReduceIfElse.call(:number => 2)
35
+
36
+ expect(result).to be_success
37
+ expect(result[:number]).to eq(5)
38
+ end
39
+
40
+ it 'will not reduce over a failed context' do
41
+ empty_context.fail!('Something bad happened')
42
+
43
+ result = TestReduceIfElse.call(empty_context)
44
+
45
+ expect(result).to be_failure
46
+ end
47
+
48
+ it 'does not reduce over a skipped context' do
49
+ empty_context.skip_remaining!('No more needed')
50
+
51
+ result = TestReduceIfElse.call(empty_context)
52
+ expect(result).to be_success
53
+ end
54
+
55
+ it "knows that it's being conditionally reduced from within an organizer" do
56
+ result = TestReduceIfElse.call(:number => 2)
57
+
58
+ expect(result.organized_by).to eq TestReduceIfElse
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+ require 'test_doubles'
3
+
4
+ RSpec.describe FunctionalLightService::Organizer do
5
+ class TestReduceWhile
6
+ extend FunctionalLightService::Organizer
7
+
8
+ def self.call(context)
9
+ with(context).reduce(actions)
10
+ end
11
+
12
+ def self.actions
13
+ [
14
+ reduce_while(->(ctx) { ctx[:number] < 3 }, [
15
+ TestDoubles::AddsOneAction,
16
+ TestDoubles::AddsTwoAction
17
+ ])
18
+ ]
19
+ end
20
+ end
21
+
22
+ let(:empty_context) { FunctionalLightService::Context.make }
23
+
24
+ it 'reduces while the block evaluates to true' do
25
+ result = TestReduceWhile.call(:number => 0)
26
+
27
+ expect(result).to be_success
28
+ expect(result[:number]).to eq(3)
29
+ end
30
+
31
+ it 'checks the condition before each action' do
32
+ result = TestReduceWhile.call(:number => 2)
33
+
34
+ expect(result).to be_success
35
+ expect(result[:number]).to eq(3)
36
+ end
37
+
38
+ it 'does not execute any steps when the condition is false from the start' do
39
+ result = TestReduceWhile.call(:number => 5)
40
+
41
+ expect(result).to be_success
42
+ expect(result[:number]).to eq(5)
43
+ end
44
+
45
+ it 'does not execute on failed context' do
46
+ empty_context.fail!('Something bad happened')
47
+
48
+ result = TestReduceWhile.call(empty_context)
49
+ expect(result).to be_failure
50
+ end
51
+
52
+ it 'does not execute a skipped context' do
53
+ empty_context.skip_remaining!('No more needed')
54
+
55
+ result = TestReduceWhile.call(empty_context)
56
+ expect(result).to be_success
57
+ end
58
+
59
+ it "is expected to know its organizer when reducing while a condition" do
60
+ result = TestReduceWhile.call(:number => 0)
61
+
62
+ expect(result.organized_by).to eq TestReduceWhile
63
+ end
64
+
65
+ it 'skips actions within its own scope' do
66
+ org = Class.new do
67
+ extend FunctionalLightService::Organizer
68
+
69
+ def self.call
70
+ reduce(actions)
71
+ end
72
+
73
+ def self.actions
74
+ [
75
+ reduce_while(
76
+ ->(c) { !c.nil? },
77
+ [
78
+ execute(->(c) { c[:first_reduce_while] = true }),
79
+ execute(lambda(&:skip_remaining!)),
80
+ execute(->(c) { c[:second_reduce_while] = true })
81
+ ]
82
+ ),
83
+ execute(->(c) { c[:last_outside] = true })
84
+ ]
85
+ end
86
+ end
87
+
88
+ result = org.call
89
+
90
+ aggregate_failures do
91
+ expect(result[:first_reduce_while]).to be true
92
+ expect(result[:second_reduce_while]).to be_nil
93
+ expect(result[:last_outside]).to be true
94
+ end
95
+ end
96
+ end