functional-light-service 0.3.4 → 0.5.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +47 -0
  3. data/.rubocop.yml +103 -15
  4. data/.solargraph.yml +11 -0
  5. data/Appraisals +6 -2
  6. data/CHANGELOG.md +128 -0
  7. data/Gemfile +1 -3
  8. data/README.md +1492 -1424
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/functional-light-service.gemspec +19 -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.rb +6 -0
  22. data/lib/functional-light-service/organizer/with_reducer_factory.rb +1 -1
  23. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +5 -2
  24. data/lib/functional-light-service/organizer.rb +10 -0
  25. data/lib/functional-light-service/testing/context_factory.rb +2 -0
  26. data/lib/functional-light-service/version.rb +1 -1
  27. data/spec/acceptance/fail_spec.rb +42 -16
  28. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  29. data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
  30. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  31. data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
  32. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  33. data/spec/action_spec.rb +8 -0
  34. data/spec/context/inspect_spec.rb +6 -21
  35. data/spec/context_spec.rb +1 -1
  36. data/spec/lib/deterministic/monad_axioms.rb +2 -0
  37. data/spec/lib/deterministic/monad_spec.rb +2 -0
  38. data/spec/lib/deterministic/null_spec.rb +2 -0
  39. data/spec/lib/deterministic/option_spec.rb +18 -14
  40. data/spec/lib/enum_spec.rb +3 -1
  41. data/spec/organizer_spec.rb +21 -0
  42. data/spec/sample/looks_up_tax_percentage_action_spec.rb +3 -1
  43. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  44. data/spec/spec_helper.rb +8 -5
  45. data/spec/test_doubles.rb +56 -9
  46. metadata +156 -26
  47. data/.travis.yml +0 -24
  48. data/gemfiles/dry_inflector_0_2.gemfile +0 -8
  49. data/gemfiles/i18n_1_8.gemfile +0 -8
data/Rakefile CHANGED
@@ -9,4 +9,4 @@ RuboCop::RakeTask.new
9
9
  RSpec::Core::RakeTask.new(:spec)
10
10
 
11
11
  task(:default).clear
12
- task :default => %i[spec rubocop]
12
+ task :default => %i[spec rubocop]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.4
1
+ 0.5.4
@@ -15,14 +15,25 @@ Gem::Specification.new do |gem|
15
15
  gem.name = "functional-light-service"
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = FunctionalLightService::VERSION
18
+ gem.required_ruby_version = '>= 2.6.0'
18
19
 
19
- gem.add_dependency("dry-inflector", ">= 0.2.0")
20
- gem.add_dependency("i18n", ">= 1.8.2")
20
+ gem.add_runtime_dependency("dry-inflector", "~> 0.2", ">= 0.2.1")
21
+ gem.add_runtime_dependency("i18n", "~> 1.8", ">= 1.8.11")
21
22
 
22
- gem.add_development_dependency("i18n", ">= 1.8.2")
23
- gem.add_development_dependency("dry-inflector", ">= 0.2.0")
24
- gem.add_development_dependency("rspec", "~> 3.0")
25
- gem.add_development_dependency("simplecov", "~> 0.16.1")
26
- gem.add_development_dependency("rubocop", "~> 0.63.1")
27
- gem.add_development_dependency("pry", "~> 0.12.2")
23
+
24
+ gem.add_development_dependency("rake", "~> 13.0.6", ">= 13.0.6")
25
+ gem.add_development_dependency("i18n", "~> 1.8", ">= 1.8.11")
26
+ gem.add_development_dependency("dry-inflector", "~> 0.2", ">= 0.2.1")
27
+ gem.add_development_dependency("rspec", "~> 3.10.0")
28
+ gem.add_development_dependency("rspec-mocks", "= 3.10.2")
29
+ gem.add_development_dependency("simplecov", "~> 0.21.2")
30
+ gem.add_development_dependency("simplecov-cobertura", "~> 2.1.0")
31
+ # rexml >= 3.3 ha un parser XML piu severo che rompe simplecov-cobertura 2.1.0
32
+ # (Malformed XML: No root element). Blocco alla 3.2.x compatibile.
33
+ gem.add_development_dependency("rexml", "< 3.3")
34
+ gem.add_development_dependency("rubocop", "~> 1.25.0")
35
+ gem.add_development_dependency("rubocop-performance", "~> 1.13.2")
36
+ gem.add_development_dependency("pry", "~> 0.14.1")
37
+ gem.add_development_dependency("solargraph", "~> 0.44.2")
38
+ gem.add_development_dependency("nokogiri", "~> 1.12.5")
28
39
  end
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "dry-inflector", "~> 0.2.1"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "i18n", "~> 1.8.11"
4
+
5
+ gemspec :path => "../"
@@ -24,7 +24,7 @@ module FunctionalLightService
24
24
 
25
25
  def error_message
26
26
  "#{type_name} #{format_keys(keys_not_found(keys))} " \
27
- "to be in the context during #{action}"
27
+ "to be in the context during #{action}"
28
28
  end
29
29
 
30
30
  def throw_error_predicate(_keys)
@@ -95,7 +95,7 @@ module FunctionalLightService
95
95
 
96
96
  def error_message
97
97
  "promised or expected keys cannot be a " \
98
- "reserved key: [#{format_keys(violated_keys)}]"
98
+ "reserved key: [#{format_keys(violated_keys)}]"
99
99
  end
100
100
 
101
101
  def keys
@@ -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, :organized_by
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
@@ -2,9 +2,15 @@ module FunctionalLightService
2
2
  module Organizer
3
3
  class WithReducer
4
4
  attr_reader :context
5
+ attr_accessor :organizer
6
+
7
+ def initialize(monitored_organizer = nil)
8
+ @organizer = monitored_organizer
9
+ end
5
10
 
6
11
  def with(data = {})
7
12
  @context = FunctionalLightService::Context.make(data)
13
+ @context.organized_by = organizer
8
14
  self
9
15
  end
10
16
 
@@ -3,7 +3,7 @@ module FunctionalLightService
3
3
  class WithReducerFactory
4
4
  def self.make(monitored_organizer)
5
5
  logger = monitored_organizer.logger || FunctionalLightService::Configuration.logger
6
- decorated = WithReducer.new
6
+ decorated = WithReducer.new(monitored_organizer)
7
7
 
8
8
  return decorated if logger.nil?
9
9
 
@@ -5,9 +5,12 @@ 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
+
12
+ decorated.organizer = organizer
13
+
11
14
  @logger = logger
12
15
  @logged = false
13
16
  end
@@ -19,7 +22,7 @@ module FunctionalLightService
19
22
 
20
23
  logger.info do
21
24
  "[FunctionalLightService] - keys in context: " \
22
- "#{extract_keys(decorated.context.keys)}"
25
+ "#{extract_keys(decorated.context.keys)}"
23
26
  end
24
27
  self
25
28
  end
@@ -63,6 +63,16 @@ module FunctionalLightService
63
63
  def logger
64
64
  @logger
65
65
  end
66
+
67
+ def add_to_context(**args)
68
+ args.map do |key, value|
69
+ execute(->(ctx) { ctx[key.to_sym] = value })
70
+ end
71
+ end
72
+
73
+ def add_aliases(args)
74
+ execute(->(ctx) { ctx.assign_aliases(ctx.aliases.merge(args)) })
75
+ end
66
76
  end
67
77
 
68
78
  module Macros
@@ -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.5.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
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe FunctionalLightService::Organizer do
4
+ class TestAddAliases
5
+ extend FunctionalLightService::Organizer
6
+
7
+ def self.call(context = FunctionalLightService::Context.make)
8
+ with(context).reduce(steps)
9
+ end
10
+
11
+ def self.steps
12
+ [
13
+ add_to_context(:my_message => "Hello There"),
14
+ # This will add the alias `:a_message` which points
15
+ # to the :my_message key's value
16
+ add_aliases(:my_message => :a_message),
17
+ TestDoubles::CapitalizeMessage
18
+ ]
19
+ end
20
+ end
21
+
22
+ it "adds aliases to the context embedded in the series of actions" do
23
+ result = TestAddAliases.call
24
+
25
+ expect(result).to be_success
26
+ expect(result.final_message).to eq("HELLO THERE")
27
+ end
28
+ end