functional-light-service 0.2.5 → 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 (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