functional-light-service 0.3.4 → 0.4.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +39 -0
  3. data/.rubocop.yml +103 -15
  4. data/.solargraph.yml +11 -0
  5. data/.travis.yml +5 -5
  6. data/Appraisals +2 -2
  7. data/CHANGELOG.md +48 -0
  8. data/Gemfile +2 -2
  9. data/README.md +3 -1
  10. data/VERSION +1 -1
  11. data/functional-light-service.gemspec +14 -8
  12. data/gemfiles/dry_inflector_0_2_1.gemfile +5 -0
  13. data/gemfiles/i18n_1_8_11.gemfile +5 -0
  14. data/lib/functional-light-service/context/key_verifier.rb +2 -2
  15. data/lib/functional-light-service/context.rb +152 -154
  16. data/lib/functional-light-service/functional/enum.rb +2 -6
  17. data/lib/functional-light-service/functional/maybe.rb +1 -0
  18. data/lib/functional-light-service/functional/null.rb +1 -1
  19. data/lib/functional-light-service/functional/option.rb +0 -2
  20. data/lib/functional-light-service/functional/result.rb +2 -2
  21. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +2 -2
  22. data/lib/functional-light-service/testing/context_factory.rb +2 -0
  23. data/lib/functional-light-service/version.rb +1 -1
  24. data/spec/acceptance/fail_spec.rb +42 -16
  25. data/spec/acceptance/organizer/reduce_if_spec.rb +32 -0
  26. data/spec/context/inspect_spec.rb +6 -21
  27. data/spec/context_spec.rb +1 -1
  28. data/spec/lib/deterministic/monad_axioms.rb +2 -0
  29. data/spec/lib/deterministic/monad_spec.rb +2 -0
  30. data/spec/lib/deterministic/null_spec.rb +2 -0
  31. data/spec/lib/enum_spec.rb +3 -1
  32. data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
  33. data/spec/spec_helper.rb +9 -8
  34. data/spec/test_doubles.rb +21 -9
  35. metadata +107 -25
  36. data/gemfiles/dry_inflector_0_2.gemfile +0 -8
  37. data/gemfiles/i18n_1_8.gemfile +0 -8
@@ -1,154 +1,152 @@
1
- module FunctionalLightService
2
- # rubocop:disable ClassLength
3
- class Context < Hash
4
- include FunctionalLightService::Prelude::Option
5
- include FunctionalLightService::Prelude::Result
6
- attr_accessor :outcome, :current_action
7
-
8
- def initialize(context = {}, outcome = Success(:message => '', :error => nil))
9
- @outcome = outcome
10
- @skip_remaining = false
11
- context.to_hash.each { |k, v| self[k] = v }
12
- end
13
-
14
- def self.make(context = {})
15
- unless context.is_a?(Hash) || context.is_a?(FunctionalLightService::Context)
16
- msg = 'Argument must be Hash or FunctionalLightService::Context'
17
- raise ArgumentError, msg
18
- end
19
-
20
- context = new(context) unless context.is_a?(Context)
21
-
22
- context.assign_aliases(context.delete(:_aliases)) if context[:_aliases]
23
- context
24
- end
25
-
26
- def add_to_context(values)
27
- merge! values
28
- end
29
-
30
- def success?
31
- @outcome.success?
32
- end
33
-
34
- def failure?
35
- @outcome.failure?
36
- end
37
-
38
- def skip_remaining?
39
- @skip_remaining
40
- end
41
-
42
- def reset_skip_remaining!
43
- @outcome = Success(:message => '', :error => nil)
44
- @skip_remaining = false
45
- end
46
-
47
- def message
48
- @outcome.value.dig(:message)
49
- end
50
-
51
- def error_code
52
- @outcome.value.dig(:error)
53
- end
54
-
55
- def succeed!(message = nil, options = {})
56
- message = Configuration.localization_adapter.success(message,
57
- current_action,
58
- options)
59
- @outcome = Success(:message => message)
60
- end
61
-
62
- def fail!(message = nil, options_or_error_code = {})
63
- options_or_error_code ||= {}
64
-
65
- if options_or_error_code.is_a?(Hash)
66
- error_code = options_or_error_code.delete(:error_code)
67
- options = options_or_error_code
68
- else
69
- error_code = options_or_error_code
70
- options = {}
71
- end
72
-
73
- message = Configuration.localization_adapter.failure(message,
74
- current_action,
75
- options)
76
-
77
- @outcome = Failure(:message => message, :error => error_code)
78
- end
79
-
80
- def fail_and_return!(*args)
81
- fail!(*args)
82
- throw(:jump_when_failed)
83
- end
84
-
85
- def fail_with_rollback!(message = nil, error_code = nil)
86
- fail!(message, error_code)
87
- raise FailWithRollbackError
88
- end
89
-
90
- def skip_remaining!(message = nil)
91
- @outcome = Success(:message => message)
92
- @skip_remaining = true
93
- end
94
-
95
- def stop_processing?
96
- failure? || skip_remaining?
97
- end
98
-
99
- def define_accessor_methods_for_keys(keys)
100
- return if keys.nil?
101
-
102
- keys.each do |key|
103
- next if respond_to?(key.to_sym)
104
-
105
- define_singleton_method(key.to_s) { fetch(key) }
106
- define_singleton_method("#{key}=") { |value| self[key] = value }
107
- end
108
- end
109
-
110
- def assign_aliases(aliases)
111
- @aliases = aliases
112
-
113
- aliases.each_pair do |key, key_alias|
114
- self[key_alias] = self[key]
115
- end
116
- end
117
-
118
- def aliases
119
- @aliases ||= {}
120
- end
121
-
122
- def [](key)
123
- key = aliases.key(key) || key
124
- return super(key)
125
- end
126
-
127
- def fetch(key, default = nil, &blk)
128
- self[key] ||= if block_given?
129
- super(key, &blk)
130
- else
131
- super
132
- end
133
- end
134
-
135
- def inspect
136
- "#{self.class}(#{self}, " \
137
- + "success: #{success?}, " \
138
- + "message: #{check_nil(message)}, " \
139
- + "error_code: #{check_nil(error_code)}, " \
140
- + "skip_remaining: #{@skip_remaining}, " \
141
- + "aliases: #{@aliases}" \
142
- + ")"
143
- end
144
-
145
- private
146
-
147
- def check_nil(value)
148
- return 'nil' unless value
149
-
150
- "'#{value}'"
151
- end
152
- end
153
- # rubocop:enable ClassLength
154
- end
1
+ module FunctionalLightService
2
+ # rubocop:disable Metrics/ClassLength
3
+ class Context < Hash
4
+ include FunctionalLightService::Prelude::Option
5
+ include FunctionalLightService::Prelude::Result
6
+ attr_accessor :outcome, :current_action
7
+
8
+ # rubocop:disable Lint/MissingSuper
9
+ def initialize(context = {},
10
+ outcome = Success(:message => '', :error => nil))
11
+ @outcome = outcome
12
+ @skip_remaining = false
13
+ context.to_hash.each { |k, v| self[k] = v }
14
+ end
15
+ # rubocop:enable Lint/MissingSuper
16
+
17
+ def self.make(context = {})
18
+ unless context.is_a?(Hash) || context.is_a?(FunctionalLightService::Context)
19
+ msg = 'Argument must be Hash or FunctionalLightService::Context'
20
+ raise ArgumentError, msg
21
+ end
22
+
23
+ context = new(context) unless context.is_a?(Context)
24
+
25
+ context.assign_aliases(context.delete(:_aliases)) if context[:_aliases]
26
+ context
27
+ end
28
+
29
+ def add_to_context(values)
30
+ merge! values
31
+ end
32
+
33
+ def success?
34
+ @outcome.success?
35
+ end
36
+
37
+ def failure?
38
+ @outcome.failure?
39
+ end
40
+
41
+ def skip_remaining?
42
+ @skip_remaining
43
+ end
44
+
45
+ def reset_skip_remaining!
46
+ @outcome = Success(:message => '', :error => nil)
47
+ @skip_remaining = false
48
+ end
49
+
50
+ def message
51
+ @outcome.value[:message]
52
+ end
53
+
54
+ def error_code
55
+ @outcome.value[:error]
56
+ end
57
+
58
+ def succeed!(message = nil, options = {})
59
+ message = Configuration.localization_adapter.success(message,
60
+ current_action,
61
+ options)
62
+ @outcome = Success(:message => message)
63
+ end
64
+
65
+ def fail!(message = nil, options_or_error_code = {})
66
+ options_or_error_code ||= {}
67
+
68
+ if options_or_error_code.is_a?(Hash)
69
+ error_code = options_or_error_code.delete(:error_code)
70
+ options = options_or_error_code
71
+ else
72
+ error_code = options_or_error_code
73
+ options = {}
74
+ end
75
+
76
+ message = Configuration.localization_adapter.failure(message,
77
+ current_action,
78
+ options)
79
+
80
+ @outcome = Failure(:message => message, :error => error_code)
81
+ end
82
+
83
+ def fail_and_return!(*args)
84
+ fail!(*args)
85
+ throw(:jump_when_failed)
86
+ end
87
+
88
+ def fail_with_rollback!(message = nil, error_code = nil)
89
+ fail!(message, error_code)
90
+ raise FailWithRollbackError
91
+ end
92
+
93
+ def skip_remaining!(message = nil)
94
+ @outcome = Success(:message => message)
95
+ @skip_remaining = true
96
+ end
97
+
98
+ def stop_processing?
99
+ failure? || skip_remaining?
100
+ end
101
+
102
+ def define_accessor_methods_for_keys(keys)
103
+ return if keys.nil?
104
+
105
+ keys.each do |key|
106
+ next if respond_to?(key.to_sym)
107
+
108
+ define_singleton_method(key.to_s) { fetch(key) }
109
+ define_singleton_method("#{key}=") { |value| self[key] = value }
110
+ end
111
+ end
112
+
113
+ def assign_aliases(aliases)
114
+ @aliases = aliases
115
+
116
+ aliases.each_pair do |key, key_alias|
117
+ self[key_alias] = self[key]
118
+ end
119
+ end
120
+
121
+ def aliases
122
+ @aliases ||= {}
123
+ end
124
+
125
+ def [](key)
126
+ key = aliases.key(key) || key
127
+ return super(key)
128
+ end
129
+
130
+ def fetch(key, default = nil, &blk)
131
+ self[key] ||= if block_given?
132
+ super(key, &blk)
133
+ else
134
+ super
135
+ end
136
+ end
137
+
138
+ def inspect
139
+ "#{self.class}(#{self}, success: #{success?}, message: #{check_nil(message)}, error_code: " \
140
+ "#{check_nil(error_code)}, skip_remaining: #{@skip_remaining}, aliases: #{@aliases})"
141
+ end
142
+
143
+ private
144
+
145
+ def check_nil(value)
146
+ return 'nil' unless value
147
+
148
+ "'#{value}'"
149
+ end
150
+ end
151
+ # rubocop:enable Metrics/ClassLength
152
+ end
@@ -53,9 +53,9 @@ module FunctionalLightService
53
53
  end
54
54
 
55
55
  @value = if init.count == 1 && init[0].is_a?(Hash)
56
- Hash[args.zip(init[0].values)]
56
+ args.zip(init[0].values).to_h
57
57
  else
58
- Hash[args.zip(init)]
58
+ args.zip(init).to_h
59
59
  end
60
60
  end
61
61
 
@@ -67,7 +67,6 @@ module FunctionalLightService
67
67
 
68
68
  # rubocop:disable Metrics/MethodLength
69
69
  def self.create(parent, args)
70
- # rubocop:disable Style/AccessModifierDeclarations
71
70
  if args.include? :value
72
71
  raise ArgumentError, "#{args} may not contain the reserved name :value"
73
72
  end
@@ -106,14 +105,11 @@ module FunctionalLightService
106
105
  end
107
106
 
108
107
  dt
109
- # rubocop:enable Style/AccessModifierDeclarations
110
108
  end
111
109
  # rubocop:enable Metrics/MethodLength
112
110
 
113
111
  class << self
114
- # rubocop:disable Style/AccessModifierDeclarations
115
112
  public :new
116
- # rubocop:enable Style/AccessModifierDeclarations
117
113
  end
118
114
  end
119
115
 
@@ -7,6 +7,7 @@ class Object
7
7
  true
8
8
  end
9
9
  end
10
+
10
11
  # rubocop:disable Naming/MethodName
11
12
  def Maybe(obj)
12
13
  obj.nil? ? Null.instance : obj
@@ -58,7 +58,7 @@ class Null
58
58
  false
59
59
  end
60
60
 
61
- def respond_to?(m, include_private = false)
61
+ def respond_to?(m)
62
62
  return true if @methods.empty? || @methods.include?(m)
63
63
 
64
64
  super
@@ -28,8 +28,6 @@ module FunctionalLightService
28
28
 
29
29
  # rubocop:disable Metrics/BlockLength
30
30
  impl(Option) do
31
- class NoneValueError < StandardError; end
32
-
33
31
  def fmap
34
32
  match do
35
33
  Some() { |s| self.class.new(yield(s)) }
@@ -8,8 +8,8 @@ module FunctionalLightService
8
8
  class << self
9
9
  def try!
10
10
  Success.new(yield)
11
- rescue StandardError => err
12
- Failure.new(err)
11
+ rescue StandardError => e
12
+ Failure.new(e)
13
13
  end
14
14
  end
15
15
  end
@@ -5,7 +5,7 @@ module FunctionalLightService
5
5
 
6
6
  alias logged? logged
7
7
 
8
- def initialize(organizer, decorated: WithReducer.new, logger:)
8
+ def initialize(organizer, logger:, decorated: WithReducer.new)
9
9
  @decorated = decorated
10
10
  @organizer = organizer
11
11
  @logger = logger
@@ -19,7 +19,7 @@ module FunctionalLightService
19
19
 
20
20
  logger.info do
21
21
  "[FunctionalLightService] - keys in context: " \
22
- "#{extract_keys(decorated.context.keys)}"
22
+ "#{extract_keys(decorated.context.keys)}"
23
23
  end
24
24
  self
25
25
  end
@@ -26,11 +26,13 @@ module FunctionalLightService
26
26
 
27
27
  # More than one arguments can be passed to the
28
28
  # Organizer's #call method
29
+ # rubocop:disable Style/ArgumentsForwarding
29
30
  def with(*args, &block)
30
31
  catch(:return_ctx_from_execution) do
31
32
  @organizer.call(*args, &block)
32
33
  end
33
34
  end
35
+ # rubocop:enable Style/ArgumentsForwarding
34
36
 
35
37
  def initialize(organizer)
36
38
  @organizer = organizer
@@ -1,3 +1,3 @@
1
1
  module FunctionalLightService
2
- VERSION = "0.3.4".freeze
2
+ VERSION = "0.4.4".freeze
3
3
  end
@@ -1,24 +1,50 @@
1
1
  require 'spec_helper'
2
2
 
3
- RSpec.describe "fail! returns immediately from executed block" do
4
- class FailAction
5
- extend FunctionalLightService::Action
6
- promises :one, :two
7
-
8
- executed do |ctx|
9
- ctx.one = 1
10
- # Have to set it in Context
11
- ctx.two = nil
12
-
13
- ctx.fail_and_return!('Something went wrong')
14
- ctx.two = 2
3
+ RSpec.describe "fail_and_return!" do
4
+ describe "returns immediately from executed block" do
5
+ class FailAndReturnAction
6
+ extend FunctionalLightService::Action
7
+ promises :one, :two
8
+
9
+ executed do |ctx|
10
+ ctx.one = 1
11
+ # Have to set it in Context
12
+ ctx.two = nil
13
+
14
+ ctx.fail_and_return!('Something went wrong')
15
+ ctx.two = 2
16
+ end
17
+ end
18
+
19
+ it "returns immediately from executed block" do
20
+ result = FailAndReturnAction.execute
21
+
22
+ expect(result).to be_failure
23
+ expect(result.two).to be_nil
15
24
  end
16
25
  end
17
26
 
18
- it "returns immediately from executed block" do
19
- result = FailAction.execute
27
+ describe "accepts error_code option" do
28
+ class FailAndReturnWithErrorCodeAction
29
+ extend FunctionalLightService::Action
30
+ promises :one, :two
20
31
 
21
- expect(result).to be_failure
22
- expect(result.two).to be_nil
32
+ executed do |ctx|
33
+ ctx.one = 1
34
+ # Have to set it in Context
35
+ ctx.two = nil
36
+
37
+ ctx.fail_and_return!('Something went wrong', :error_code => 401)
38
+ ctx.two = 2
39
+ end
40
+ end
41
+
42
+ it "returned context contains the error_code" do
43
+ result = FailAndReturnWithErrorCodeAction.execute
44
+
45
+ expect(result).to be_failure
46
+ expect(result.error_code).to eq 401
47
+ expect(result.two).to be_nil
48
+ end
23
49
  end
24
50
  end
@@ -48,4 +48,36 @@ RSpec.describe FunctionalLightService::Organizer do
48
48
  result = TestReduceIf.call(empty_context)
49
49
  expect(result).to be_success
50
50
  end
51
+
52
+ it 'skips actions within in its own scope' do
53
+ org = Class.new do
54
+ extend FunctionalLightService::Organizer
55
+
56
+ def self.call
57
+ reduce(actions)
58
+ end
59
+
60
+ def self.actions
61
+ [
62
+ reduce_if(
63
+ ->(c) { !c.nil? },
64
+ [
65
+ execute(->(c) { c[:first_reduce_if] = true }),
66
+ execute(->(c) { c.skip_remaining! }),
67
+ execute(->(c) { c[:second_reduce_if] = true })
68
+ ]
69
+ ),
70
+ execute(->(c) { c[:last_outside] = true })
71
+ ]
72
+ end
73
+ end
74
+
75
+ result = org.call
76
+
77
+ aggregate_failures do
78
+ expect(result[:first_reduce_if]).to be true
79
+ expect(result[:second_reduce_if]).to be_nil
80
+ expect(result[:last_outside]).to be true
81
+ end
82
+ end
51
83
  end
@@ -13,13 +13,8 @@ RSpec.describe FunctionalLightService::Context do
13
13
  describe '#inspect' do
14
14
  it 'inspects the hash with all the fields' do
15
15
  inspected_context =
16
- 'FunctionalLightService::Context({}, ' \
17
- + 'success: true, ' \
18
- + 'message: \'\', ' \
19
- + 'error_code: nil, ' \
20
- + 'skip_remaining: false, ' \
21
- + 'aliases: {}' \
22
- + ')'
16
+ "FunctionalLightService::Context({}, success: true, message: '', error_code: nil, " \
17
+ "skip_remaining: false, aliases: {})"
23
18
 
24
19
  expect(context.inspect).to eq(inspected_context)
25
20
  end
@@ -28,13 +23,8 @@ RSpec.describe FunctionalLightService::Context do
28
23
  context.fail!('There was an error')
29
24
 
30
25
  inspected_context =
31
- 'FunctionalLightService::Context({}, ' \
32
- + 'success: false, ' \
33
- + 'message: \'There was an error\', ' \
34
- + 'error_code: nil, ' \
35
- + 'skip_remaining: false, ' \
36
- + 'aliases: {}' \
37
- + ')'
26
+ "FunctionalLightService::Context({}, success: false, message: 'There was an error', " \
27
+ "error_code: nil, skip_remaining: false, aliases: {})"
38
28
 
39
29
  expect(context.inspect).to eq(inspected_context)
40
30
  end
@@ -43,13 +33,8 @@ RSpec.describe FunctionalLightService::Context do
43
33
  context.skip_remaining!('No need to process')
44
34
 
45
35
  inspected_context =
46
- 'FunctionalLightService::Context({}, ' \
47
- + 'success: true, ' \
48
- + 'message: \'No need to process\', ' \
49
- + 'error_code: nil, ' \
50
- + 'skip_remaining: true, ' \
51
- + 'aliases: {}' \
52
- + ')'
36
+ "FunctionalLightService::Context({}, success: true, message: 'No need to process', " \
37
+ "error_code: nil, skip_remaining: true, aliases: {})"
53
38
 
54
39
  expect(context.inspect).to eq(inspected_context)
55
40
  end
data/spec/context_spec.rb CHANGED
@@ -167,7 +167,7 @@ RSpec.describe FunctionalLightService::Context do
167
167
  end
168
168
 
169
169
  it "allows a default block value for #fetch" do
170
- expect(context.fetch(:madeup) { :default }).to eq(:default)
170
+ expect(context.fetch(:madeup, :default)).to eq(:default)
171
171
  end
172
172
 
173
173
  context "when aliases are included via .make" do
@@ -34,7 +34,9 @@ shared_examples 'a Monad' do
34
34
 
35
35
  it '#bind must return a monad' do
36
36
  expect(monad.new(1).bind { |v| monad.new(v) }).to eq monad.new(1)
37
+ # rubocop:disable Lint/EmptyBlock
37
38
  expect { monad.new(1).bind {} }.to raise_error(FunctionalLightService::Monad::NotMonadError)
39
+ # rubocop:enable Lint/EmptyBlock
38
40
  end
39
41
 
40
42
  it '#new must return a monad' do
@@ -21,8 +21,10 @@ describe FunctionalLightService::Monad do
21
21
 
22
22
  context '#bind' do
23
23
  it "raises an error if the passed function does not return a monad of the same class" do
24
+ # rubocop:disable Lint/EmptyBlock
24
25
  expect { Identity.new(1).bind {} }.to \
25
26
  raise_error(FunctionalLightService::Monad::NotMonadError)
27
+ # rubocop:enable Lint/EmptyBlock
26
28
  end
27
29
  specify { expect(Identity.new(1).bind { |value| Identity.new(value) }).to eq Identity.new(1) }
28
30
 
@@ -38,7 +38,9 @@ describe Null do
38
38
  null = Null.instance
39
39
  expect(null.to_str).to eq ""
40
40
  expect(null.to_ary).to eq []
41
+ # rubocop:disable Style/StringConcatenation
41
42
  expect("" + null).to eq ""
43
+ # rubocop:enable Style/StringConcatenation
42
44
 
43
45
  a, b, c = null
44
46
  expect(a).to be_nil
@@ -8,7 +8,7 @@ describe FunctionalLightService::Enum do
8
8
  InvalidEnum = FunctionalLightService.enum do
9
9
  Unary(:value)
10
10
  end
11
- end .to raise_error ArgumentError
11
+ end.to raise_error ArgumentError
12
12
  end
13
13
 
14
14
  context "Nullary, Unary, Binary" do
@@ -32,7 +32,9 @@ describe FunctionalLightService::Enum do
32
32
  expect { n.value }.to raise_error NoMethodError
33
33
  expect(n.inspect).to eq "Nullary"
34
34
  expect(n.to_s).to eq ""
35
+ # rubocop:disable Lint/EmptyBlock
35
36
  expect(n.fmap {}).to eq n
37
+ # rubocop:enable Lint/EmptyBlock
36
38
  end
37
39
 
38
40
  it "Unary" do
@@ -1,7 +1,9 @@
1
1
  require 'spec_helper'
2
2
  require_relative 'tax/looks_up_tax_percentage_action'
3
3
 
4
- class TaxRange; end
4
+ class TaxRange
5
+ extend FunctionalLightService::Action
6
+ end
5
7
 
6
8
  describe LooksUpTaxPercentageAction do
7
9
  let(:region) { double('region') }