functional-light-service 0.2.5 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) 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 +7 -5
  6. data/Appraisals +2 -2
  7. data/CHANGELOG.md +108 -0
  8. data/Gemfile +2 -2
  9. data/README.md +3 -1
  10. data/VERSION +1 -1
  11. data/functional-light-service.gemspec +14 -6
  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/action.rb +3 -4
  15. data/lib/functional-light-service/configuration.rb +1 -1
  16. data/lib/functional-light-service/context/key_verifier.rb +2 -2
  17. data/lib/functional-light-service/context.rb +152 -165
  18. data/lib/functional-light-service/functional/enum.rb +3 -7
  19. data/lib/functional-light-service/functional/maybe.rb +1 -0
  20. data/lib/functional-light-service/functional/null.rb +1 -1
  21. data/lib/functional-light-service/functional/option.rb +0 -2
  22. data/lib/functional-light-service/functional/result.rb +4 -10
  23. data/lib/functional-light-service/localization_adapter.rb +5 -2
  24. data/lib/functional-light-service/organizer/iterate.rb +4 -1
  25. data/lib/functional-light-service/organizer/verify_call_method_exists.rb +3 -2
  26. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +2 -2
  27. data/lib/functional-light-service/organizer.rb +4 -5
  28. data/lib/functional-light-service/testing/context_factory.rb +2 -0
  29. data/lib/functional-light-service/version.rb +1 -1
  30. data/lib/functional-light-service.rb +0 -1
  31. data/spec/acceptance/fail_spec.rb +42 -16
  32. data/spec/acceptance/include_warning_spec.rb +14 -14
  33. data/spec/acceptance/not_having_call_method_warning_spec.rb +4 -11
  34. data/spec/acceptance/organizer/reduce_if_spec.rb +32 -0
  35. data/spec/context/inspect_spec.rb +6 -21
  36. data/spec/context_spec.rb +1 -1
  37. data/spec/lib/deterministic/monad_axioms.rb +2 -0
  38. data/spec/lib/deterministic/monad_spec.rb +2 -0
  39. data/spec/lib/deterministic/null_spec.rb +2 -0
  40. data/spec/lib/enum_spec.rb +3 -1
  41. data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
  42. data/spec/spec_helper.rb +9 -12
  43. data/spec/test_doubles.rb +21 -9
  44. metadata +144 -22
  45. data/gemfiles/activesupport_5.gemfile +0 -8
  46. data/gemfiles/activesupport_5.gemfile.lock +0 -82
  47. data/spec/acceptance/skip_all_warning_spec.rb +0 -20
@@ -1,165 +1,152 @@
1
- require 'active_support/deprecation'
2
-
3
- module FunctionalLightService
4
- # rubocop:disable ClassLength
5
- class Context < Hash
6
- include FunctionalLightService::Prelude::Option
7
- include FunctionalLightService::Prelude::Result
8
- attr_accessor :outcome, :current_action
9
-
10
- def initialize(context = {},
11
- outcome = Success(:message => '', :error => nil))
12
- @outcome = outcome
13
- @skip_remaining = false
14
- context.to_hash.each { |k, v| self[k] = v }
15
- end
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
- success? == false
39
- end
40
-
41
- def skip_remaining?
42
- @skip_remaining
43
- end
44
-
45
- def reset_skip_remaining!
46
- @outcome = Success(nil)
47
- @skip_remaining = false
48
- end
49
-
50
- def message
51
- @outcome.value.dig(:message)
52
- end
53
-
54
- def error_code
55
- @outcome.value.dig(: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, *args)
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_all!(message = nil)
94
- warning_msg = "Using skip_all! has been deprecated, " \
95
- "please use `skip_remaining!` instead."
96
- ActiveSupport::Deprecation.warn(warning_msg)
97
-
98
- skip_remaining!(message)
99
- end
100
-
101
- def skip_remaining!(message = nil)
102
- @outcome = Success(:message => message)
103
- @skip_remaining = true
104
- end
105
-
106
- def stop_processing?
107
- failure? || skip_remaining?
108
- end
109
-
110
- def define_accessor_methods_for_keys(keys)
111
- return if keys.nil?
112
-
113
- keys.each do |key|
114
- next if respond_to?(key.to_sym)
115
-
116
- define_singleton_method(key.to_s) { fetch(key) }
117
- define_singleton_method("#{key}=") { |value| self[key] = value }
118
- end
119
- end
120
-
121
- def assign_aliases(aliases)
122
- @aliases = aliases
123
-
124
- aliases.each_pair do |key, key_alias|
125
- self[key_alias] = self[key]
126
- end
127
- end
128
-
129
- def aliases
130
- @aliases ||= {}
131
- end
132
-
133
- def [](key)
134
- key = aliases.key(key) || key
135
- return super(key)
136
- end
137
-
138
- def fetch(key, default = nil, &blk)
139
- self[key] ||= if block_given?
140
- super(key, &blk)
141
- else
142
- super
143
- end
144
- end
145
-
146
- def inspect
147
- "#{self.class}(#{self}, " \
148
- + "success: #{success?}, " \
149
- + "message: #{check_nil(message)}, " \
150
- + "error_code: #{check_nil(error_code)}, " \
151
- + "skip_remaining: #{@skip_remaining}, " \
152
- + "aliases: #{@aliases}" \
153
- + ")"
154
- end
155
-
156
- private
157
-
158
- def check_nil(value)
159
- return 'nil' unless value
160
-
161
- "'#{value}'"
162
- end
163
- end
164
- # rubocop:enable ClassLength
165
- 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
 
@@ -219,7 +215,7 @@ module FunctionalLightService
219
215
 
220
216
  args = params_spec.map { |spec| spec[1] }
221
217
 
222
- type = Kernel.eval("#{mod.name}::#{m}")
218
+ type = mod.const_get(m)
223
219
 
224
220
  guard = nil if guard && !guard.is_a?(Proc)
225
221
 
@@ -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
@@ -17,20 +17,14 @@ module FunctionalLightService
17
17
  # rubocop:disable Metrics/BlockLength
18
18
  FunctionalLightService.impl(Result) do
19
19
  def map(proc = nil, &block)
20
- match do
21
- Success() { |_| bind(proc || block) }
22
- Failure() { |_| self }
23
- end
20
+ success? ? bind(proc || block) : self
24
21
  end
25
22
 
26
23
  alias :>> :map
27
24
  alias :and_then :map
28
25
 
29
26
  def map_err(proc = nil, &block)
30
- match do
31
- Success() { |_| self }
32
- Failure() { |_| bind(proc || block) }
33
- end
27
+ failure? ? bind(proc || block) : self
34
28
  end
35
29
 
36
30
  alias :or_else :map_err
@@ -1,3 +1,5 @@
1
+ require 'i18n'
2
+
1
3
  module FunctionalLightService
2
4
  class LocalizationAdapter
3
5
  def failure(message_or_key, action_class, i18n_options = {})
@@ -34,11 +36,12 @@ module FunctionalLightService
34
36
  scope = i18n_scope_from_class(action_class, type)
35
37
  options[:scope] = scope
36
38
 
37
- I18n.t(key, options)
39
+ I18n.t(key, **options)
38
40
  end
39
41
 
40
42
  def i18n_scope_from_class(action_class, type)
41
- "#{action_class.name.underscore}.light_service.#{type.to_s.pluralize}"
43
+ inflector = Dry::Inflector.new
44
+ "#{inflector.underscore(action_class.name)}.light_service.#{inflector.pluralize(type)}"
42
45
  end
43
46
  end
44
47
  end
@@ -1,3 +1,5 @@
1
+ require "dry/inflector"
2
+
1
3
  module FunctionalLightService
2
4
  module Organizer
3
5
  class Iterate
@@ -8,7 +10,8 @@ module FunctionalLightService
8
10
  return ctx if ctx.stop_processing?
9
11
 
10
12
  collection = ctx[collection_key]
11
- item_key = collection_key.to_s.singularize.to_sym
13
+ inflector = Dry::Inflector.new
14
+ item_key = inflector.singularize(collection_key).to_sym
12
15
  collection.each do |item|
13
16
  ctx[item_key] = item
14
17
  ctx = scoped_reduce(organizer, ctx, steps)
@@ -11,11 +11,12 @@ module FunctionalLightService
11
11
  call_method_exists = klass.methods.include?(:call)
12
12
  return if call_method_exists
13
13
 
14
- warning_msg = "The <#{klass.name}> class is an organizer, " \
14
+ warning_msg = "DEPRECATION WARNING:" \
15
+ "The <#{klass.name}> class is an organizer, " \
15
16
  "its entry method (the one that calls with & reduce) " \
16
17
  "should be named `call`. " \
17
18
  "Please use #{klass}.call going forward."
18
- ActiveSupport::Deprecation.warn(warning_msg)
19
+ print warning_msg
19
20
  end
20
21
 
21
22
  def self.caller_method(first_caller)
@@ -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
@@ -1,5 +1,3 @@
1
- require 'active_support/deprecation'
2
-
3
1
  module FunctionalLightService
4
2
  module Organizer
5
3
  def self.extended(base_class)
@@ -8,9 +6,10 @@ module FunctionalLightService
8
6
  end
9
7
 
10
8
  def self.included(base_class)
11
- warning_msg = "including FunctionalLightService::Organizer is deprecated. " \
12
- "Please use `extend FunctionalLightService::Organizer` instead"
13
- ActiveSupport::Deprecation.warn(warning_msg)
9
+ msg = "DEPRECATION WARNING:\n" \
10
+ "Including FunctionalLightService::Organizer is deprecated\n" \
11
+ "Please use `extend FunctionalLightService::Organizer` instead"
12
+ print msg
14
13
  extended(base_class)
15
14
  end
16
15
 
@@ -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.2.5".freeze
2
+ VERSION = "0.4.4".freeze
3
3
  end
@@ -1,5 +1,4 @@
1
1
  require 'logger'
2
- require 'active_support/core_ext/string'
3
2
 
4
3
  require 'functional-light-service/version'
5
4
 
@@ -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
@@ -3,27 +3,27 @@ require 'spec_helper'
3
3
  describe "Including is discouraged" do
4
4
  context "when including FunctionalLightService::Organizer" do
5
5
  it "gives warning" do
6
- expected_msg = "including FunctionalLightService::Organizer is deprecated. " \
6
+ expected_msg = "DEPRECATION WARNING:\n" \
7
+ "Including FunctionalLightService::Organizer is deprecated\n" \
7
8
  "Please use `extend FunctionalLightService::Organizer` instead"
8
- expect(ActiveSupport::Deprecation).to receive(:warn)
9
- .with(expected_msg)
10
-
11
- class OrganizerIncludingLS
12
- include FunctionalLightService::Organizer
13
- end
9
+ expect do
10
+ class OrganizerIncludingLS
11
+ include FunctionalLightService::Organizer
12
+ end
13
+ end.to output(expected_msg).to_stdout
14
14
  end
15
15
  end
16
16
 
17
17
  context "when including FunctionalLightService::Action" do
18
18
  it "gives warning" do
19
- expected_msg = "including FunctionalLightService::Action is deprecated. " \
19
+ expected_msg = "DEPRECATION WARNING:\n" \
20
+ "Including FunctionalLightService::Action is deprecated\n" \
20
21
  "Please use `extend FunctionalLightService::Action` instead"
21
- expect(ActiveSupport::Deprecation).to receive(:warn)
22
- .with(expected_msg)
23
-
24
- class ActionIncludingLS
25
- include FunctionalLightService::Action
26
- end
22
+ expect do
23
+ class ActionIncludingLS
24
+ include FunctionalLightService::Action
25
+ end
26
+ end.to output(expected_msg).to_stdout
27
27
  end
28
28
  end
29
29
  end