functional-light-service 0.4.4 → 6.0.0

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/project-build.yml +43 -11
  3. data/.rubocop.yml +101 -160
  4. data/AUDIT-functional-light-service.md +352 -0
  5. data/Appraisals +4 -0
  6. data/CHANGELOG.md +118 -0
  7. data/Gemfile +0 -2
  8. data/README.md +1544 -1426
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/audit/bench.rb +99 -0
  12. data/audit/verify_findings.rb +172 -0
  13. data/functional-light-service.gemspec +15 -16
  14. data/lib/functional-light-service/action.rb +97 -101
  15. data/lib/functional-light-service/configuration.rb +26 -24
  16. data/lib/functional-light-service/context/key_verifier.rb +124 -118
  17. data/lib/functional-light-service/context.rb +63 -20
  18. data/lib/functional-light-service/deprecations.rb +26 -0
  19. data/lib/functional-light-service/errors.rb +8 -6
  20. data/lib/functional-light-service/functional/enum.rb +286 -250
  21. data/lib/functional-light-service/functional/maybe.rb +21 -15
  22. data/lib/functional-light-service/functional/monad.rb +77 -66
  23. data/lib/functional-light-service/functional/null.rb +88 -74
  24. data/lib/functional-light-service/functional/option.rb +100 -97
  25. data/lib/functional-light-service/functional/result.rb +129 -116
  26. data/lib/functional-light-service/localization_adapter.rb +48 -47
  27. data/lib/functional-light-service/organizer/execute.rb +16 -14
  28. data/lib/functional-light-service/organizer/iterate.rb +30 -25
  29. data/lib/functional-light-service/organizer/reduce_if.rb +19 -17
  30. data/lib/functional-light-service/organizer/reduce_until.rb +22 -20
  31. data/lib/functional-light-service/organizer/scoped_reducable.rb +15 -13
  32. data/lib/functional-light-service/organizer/with_callback.rb +28 -26
  33. data/lib/functional-light-service/organizer/with_reducer.rb +81 -71
  34. data/lib/functional-light-service/organizer/with_reducer_factory.rb +20 -18
  35. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +110 -105
  36. data/lib/functional-light-service/organizer.rb +114 -104
  37. data/lib/functional-light-service/testing/context_factory.rb +48 -42
  38. data/lib/functional-light-service/testing.rb +3 -1
  39. data/lib/functional-light-service/version.rb +5 -3
  40. data/lib/functional-light-service.rb +30 -28
  41. data/spec/acceptance/after_actions_spec.rb +87 -71
  42. data/spec/acceptance/before_actions_spec.rb +115 -98
  43. data/spec/acceptance/custom_log_from_organizer_spec.rb +61 -60
  44. data/spec/acceptance/deprecation_warnings_spec.rb +82 -0
  45. data/spec/acceptance/fail_spec.rb +52 -50
  46. data/spec/acceptance/message_localization_spec.rb +119 -118
  47. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  48. data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
  49. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +68 -65
  50. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  51. data/spec/acceptance/organizer/reduce_if_spec.rb +89 -83
  52. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  53. data/spec/acceptance/organizer/with_callback_spec.rb +113 -110
  54. data/spec/acceptance/{not_having_call_method_warning_spec.rb → organizer_entry_point_spec.rb} +10 -7
  55. data/spec/acceptance/rollback_spec.rb +183 -132
  56. data/spec/action_expects_and_promises_spec.rb +97 -93
  57. data/spec/action_promised_keys_spec.rb +126 -122
  58. data/spec/action_spec.rb +8 -0
  59. data/spec/context_spec.rb +289 -197
  60. data/spec/examples/controller_spec.rb +63 -63
  61. data/spec/examples/validate_address_spec.rb +38 -37
  62. data/spec/lib/deterministic/currify_spec.rb +90 -88
  63. data/spec/lib/deterministic/null_spec.rb +6 -1
  64. data/spec/lib/deterministic/option_spec.rb +140 -133
  65. data/spec/lib/deterministic/result/result_map_spec.rb +155 -154
  66. data/spec/lib/deterministic/result/result_shared.rb +3 -2
  67. data/spec/lib/deterministic/result_spec.rb +2 -2
  68. data/spec/lib/edge_cases_spec.rb +156 -0
  69. data/spec/lib/enum_spec.rb +1 -1
  70. data/spec/lib/native_pattern_matching_spec.rb +74 -0
  71. data/spec/organizer_spec.rb +115 -93
  72. data/spec/readme_spec.rb +45 -47
  73. data/spec/sample/calculates_order_tax_action_spec.rb +16 -16
  74. data/spec/sample/calculates_tax_spec.rb +1 -1
  75. data/spec/sample/looks_up_tax_percentage_action_spec.rb +55 -55
  76. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  77. data/spec/sample/tax/calculates_order_tax_action.rb +10 -9
  78. data/spec/sample/tax/looks_up_tax_percentage_action.rb +28 -27
  79. data/spec/sample/tax/provides_free_shipping_action.rb +11 -10
  80. data/spec/spec_helper.rb +21 -13
  81. data/spec/test_doubles.rb +628 -564
  82. data/spec/testing/context_factory_spec.rb +21 -0
  83. metadata +49 -117
  84. data/.travis.yml +0 -24
  85. data/lib/functional-light-service/organizer/verify_call_method_exists.rb +0 -29
  86. data/spec/acceptance/include_warning_spec.rb +0 -29
@@ -1,118 +1,124 @@
1
- module FunctionalLightService
2
- class Context
3
- class KeyVerifier
4
- attr_reader :context, :action
5
-
6
- def initialize(context, action)
7
- @context = context
8
- @action = action
9
- end
10
-
11
- def are_all_keys_in_context?(keys)
12
- not_found_keys = keys_not_found(keys)
13
- not_found_keys.none?
14
- end
15
-
16
- def keys_not_found(keys)
17
- keys ||= context.keys
18
- keys - context.keys
19
- end
20
-
21
- def format_keys(keys)
22
- keys.map { |k| ":#{k}" }.join(', ')
23
- end
24
-
25
- def error_message
26
- "#{type_name} #{format_keys(keys_not_found(keys))} " \
27
- "to be in the context during #{action}"
28
- end
29
-
30
- def throw_error_predicate(_keys)
31
- raise NotImplementedError, 'Sorry, you have to override length'
32
- end
33
-
34
- def verify
35
- return context if context.failure?
36
-
37
- if throw_error_predicate(keys)
38
- Configuration.logger.error error_message
39
- raise error_to_throw, error_message
40
- end
41
-
42
- context
43
- end
44
-
45
- def self.verify_keys(context, action, &block)
46
- ReservedKeysVerifier.new(context, action).verify
47
- ExpectedKeyVerifier.new(context, action).verify
48
-
49
- block.call
50
-
51
- PromisedKeyVerifier.new(context, action).verify
52
- end
53
- end
54
-
55
- class ExpectedKeyVerifier < KeyVerifier
56
- def type_name
57
- "expected"
58
- end
59
-
60
- def keys
61
- action.expected_keys
62
- end
63
-
64
- def error_to_throw
65
- ExpectedKeysNotInContextError
66
- end
67
-
68
- def throw_error_predicate(keys)
69
- !are_all_keys_in_context?(keys)
70
- end
71
- end
72
-
73
- class PromisedKeyVerifier < KeyVerifier
74
- def type_name
75
- "promised"
76
- end
77
-
78
- def keys
79
- action.promised_keys
80
- end
81
-
82
- def error_to_throw
83
- PromisedKeysNotInContextError
84
- end
85
-
86
- def throw_error_predicate(keys)
87
- !are_all_keys_in_context?(keys)
88
- end
89
- end
90
-
91
- class ReservedKeysVerifier < KeyVerifier
92
- def violated_keys
93
- (action.promised_keys + action.expected_keys) & reserved_keys
94
- end
95
-
96
- def error_message
97
- "promised or expected keys cannot be a " \
98
- "reserved key: [#{format_keys(violated_keys)}]"
99
- end
100
-
101
- def keys
102
- violated_keys
103
- end
104
-
105
- def error_to_throw
106
- ReservedKeysInContextError
107
- end
108
-
109
- def throw_error_predicate(keys)
110
- keys.any?
111
- end
112
-
113
- def reserved_keys
114
- %i[message error_code current_action].freeze
115
- end
116
- end
117
- end
118
- end
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ class Context
5
+ class KeyVerifier
6
+ attr_reader :context, :action
7
+
8
+ def initialize(context, action)
9
+ @context = context
10
+ @action = action
11
+ end
12
+
13
+ def are_all_keys_in_context?(keys)
14
+ not_found_keys = keys_not_found(keys)
15
+ not_found_keys.none?
16
+ end
17
+
18
+ def keys_not_found(keys)
19
+ keys ||= context.keys
20
+ # context.key? risolve anche gli alias
21
+ keys.reject { |key| context.key?(key) }
22
+ end
23
+
24
+ def format_keys(keys)
25
+ keys.map { |k| ":#{k}" }.join(', ')
26
+ end
27
+
28
+ def error_message
29
+ "#{type_name} #{format_keys(keys_not_found(keys))} " \
30
+ "to be in the context during #{action}"
31
+ end
32
+
33
+ def throw_error_predicate(_keys)
34
+ raise NotImplementedError, 'Sorry, you have to override length'
35
+ end
36
+
37
+ def verify
38
+ return context if context.failure?
39
+
40
+ if throw_error_predicate(keys)
41
+ Configuration.logger.error error_message
42
+ raise error_to_throw, error_message
43
+ end
44
+
45
+ context
46
+ end
47
+
48
+ def self.verify_keys(context, action, &block)
49
+ ReservedKeysVerifier.new(context, action).verify
50
+ ExpectedKeyVerifier.new(context, action).verify
51
+
52
+ block.call
53
+
54
+ PromisedKeyVerifier.new(context, action).verify
55
+ end
56
+ end
57
+
58
+ class ExpectedKeyVerifier < KeyVerifier
59
+ def type_name
60
+ "expected"
61
+ end
62
+
63
+ def keys
64
+ action.expected_keys
65
+ end
66
+
67
+ def error_to_throw
68
+ ExpectedKeysNotInContextError
69
+ end
70
+
71
+ def throw_error_predicate(keys)
72
+ !are_all_keys_in_context?(keys)
73
+ end
74
+ end
75
+
76
+ class PromisedKeyVerifier < KeyVerifier
77
+ def type_name
78
+ "promised"
79
+ end
80
+
81
+ def keys
82
+ action.promised_keys
83
+ end
84
+
85
+ def error_to_throw
86
+ PromisedKeysNotInContextError
87
+ end
88
+
89
+ def throw_error_predicate(keys)
90
+ !are_all_keys_in_context?(keys)
91
+ end
92
+ end
93
+
94
+ class ReservedKeysVerifier < KeyVerifier
95
+ def violated_keys
96
+ (action.promised_keys + action.expected_keys) & reserved_keys
97
+ end
98
+
99
+ def error_message
100
+ "promised or expected keys cannot be a " \
101
+ "reserved key: [#{format_keys(violated_keys)}]"
102
+ end
103
+
104
+ def keys
105
+ violated_keys
106
+ end
107
+
108
+ def error_to_throw
109
+ ReservedKeysInContextError
110
+ end
111
+
112
+ def throw_error_predicate(keys)
113
+ keys.any?
114
+ end
115
+
116
+ def reserved_keys
117
+ # _aliases/_before_actions/_after_actions sono chiavi infrastrutturali
118
+ # scritte da Organizer.with nel context
119
+ %i[message error_code current_action
120
+ _aliases _before_actions _after_actions].freeze
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FunctionalLightService
2
4
  # rubocop:disable Metrics/ClassLength
3
5
  class Context < Hash
4
6
  include FunctionalLightService::Prelude::Option
5
7
  include FunctionalLightService::Prelude::Result
6
- attr_accessor :outcome, :current_action
8
+
9
+ attr_reader :outcome
10
+ attr_accessor :current_action, :organized_by
7
11
 
8
12
  # rubocop:disable Lint/MissingSuper
9
13
  def initialize(context = {},
@@ -43,7 +47,7 @@ module FunctionalLightService
43
47
  end
44
48
 
45
49
  def reset_skip_remaining!
46
- @outcome = Success(:message => '', :error => nil)
50
+ # Resetta soltanto il flag: l'esito (e il suo messaggio) non vanno persi
47
51
  @skip_remaining = false
48
52
  end
49
53
 
@@ -66,8 +70,9 @@ module FunctionalLightService
66
70
  options_or_error_code ||= {}
67
71
 
68
72
  if options_or_error_code.is_a?(Hash)
69
- error_code = options_or_error_code.delete(:error_code)
70
- options = options_or_error_code
73
+ # dup: l'hash di opzioni appartiene al chiamante e non va mutato
74
+ options = options_or_error_code.dup
75
+ error_code = options.delete(:error_code)
71
76
  else
72
77
  error_code = options_or_error_code
73
78
  options = {}
@@ -99,23 +104,48 @@ module FunctionalLightService
99
104
  failure? || skip_remaining?
100
105
  end
101
106
 
107
+ # Registra le chiavi come accessor consentiti: la lettura/scrittura passa
108
+ # da method_missing con whitelist. Rispetto a define_singleton_method non
109
+ # materializza una singleton class per ogni context (audit, finding 3.3)
102
110
  def define_accessor_methods_for_keys(keys)
103
111
  return if keys.nil?
104
112
 
113
+ @accessor_methods ||= {}
105
114
  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 }
115
+ key = key.to_sym
116
+ next if @accessor_methods.key?(key)
117
+
118
+ # Prima il conflitto veniva saltato in silenzio e ctx.size (o :count,
119
+ # :message, ...) ritornava il metodo di Hash invece del valore
120
+ if respond_to?(key) || respond_to?("#{key}=")
121
+ raise ReservedKeysInContextError,
122
+ "expected or promised key :#{key} conflicts with an existing " \
123
+ "#{self.class.name} method: rename the key or access it via ctx[:#{key}]"
124
+ end
125
+
126
+ @accessor_methods[key] = [:reader, key]
127
+ @accessor_methods[:"#{key}="] = [:writer, key]
110
128
  end
111
129
  end
112
130
 
131
+ def method_missing(name, *args)
132
+ accessor = @accessor_methods && @accessor_methods[name]
133
+ return super unless accessor
134
+
135
+ accessor[0] == :reader ? fetch(accessor[1]) : self[accessor[1]] = args.first
136
+ end
137
+
138
+ def respond_to_missing?(name, _include_all = false)
139
+ (!@accessor_methods.nil? && @accessor_methods.key?(name)) || super
140
+ end
141
+
113
142
  def assign_aliases(aliases)
114
143
  @aliases = aliases
144
+ # Hash inverso precomputato: la risoluzione in lettura/scrittura
145
+ # resta O(1) invece del reverse-scan di Hash#key
146
+ @inverse_aliases = aliases.invert
115
147
 
116
- aliases.each_pair do |key, key_alias|
117
- self[key_alias] = self[key]
118
- end
148
+ self
119
149
  end
120
150
 
121
151
  def aliases
@@ -123,25 +153,38 @@ module FunctionalLightService
123
153
  end
124
154
 
125
155
  def [](key)
126
- key = aliases.key(key) || key
127
- return super(key)
156
+ super(resolve_key(key))
128
157
  end
129
158
 
130
- def fetch(key, default = nil, &blk)
131
- self[key] ||= if block_given?
132
- super(key, &blk)
133
- else
134
- super
135
- end
159
+ def []=(key, value)
160
+ super(resolve_key(key), value)
136
161
  end
137
162
 
163
+ def fetch(key, ...)
164
+ super(resolve_key(key), ...)
165
+ end
166
+
167
+ def key?(key)
168
+ super(resolve_key(key))
169
+ end
170
+
171
+ alias has_key? key?
172
+ alias member? key?
173
+ alias include? key?
174
+
138
175
  def inspect
139
176
  "#{self.class}(#{self}, success: #{success?}, message: #{check_nil(message)}, error_code: " \
140
- "#{check_nil(error_code)}, skip_remaining: #{@skip_remaining}, aliases: #{@aliases})"
177
+ "#{check_nil(error_code)}, skip_remaining: #{@skip_remaining}, aliases: #{aliases})"
141
178
  end
142
179
 
143
180
  private
144
181
 
182
+ def resolve_key(key)
183
+ return key unless @inverse_aliases
184
+
185
+ @inverse_aliases[key] || key
186
+ end
187
+
145
188
  def check_nil(value)
146
189
  return 'nil' unless value
147
190
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ # Deprecation warnings non fatali: ogni messaggio viene emesso una sola
5
+ # volta per processo (su stderr) e puo' essere silenziato globalmente,
6
+ # ad esempio nelle suite di test.
7
+ module Deprecations
8
+ @emitted = {}
9
+
10
+ class << self
11
+ attr_accessor :silenced
12
+
13
+ def warn(message)
14
+ return if silenced
15
+ return if @emitted[message]
16
+
17
+ @emitted[message] = true
18
+ Kernel.warn("DEPRECATION WARNING: #{message}")
19
+ end
20
+
21
+ def reset!
22
+ @emitted = {}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,6 +1,8 @@
1
- module FunctionalLightService
2
- class FailWithRollbackError < StandardError; end
3
- class ExpectedKeysNotInContextError < StandardError; end
4
- class PromisedKeysNotInContextError < StandardError; end
5
- class ReservedKeysInContextError < StandardError; end
6
- end
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ class FailWithRollbackError < StandardError; end
5
+ class ExpectedKeysNotInContextError < StandardError; end
6
+ class PromisedKeysNotInContextError < StandardError; end
7
+ class ReservedKeysInContextError < StandardError; end
8
+ end