functional-light-service 0.3.4 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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') }