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,250 +1,286 @@
1
- module FunctionalLightService
2
- module Enum
3
- class MatchError < StandardError; end
4
- end
5
-
6
- class EnumBuilder
7
- def initialize(parent)
8
- @parent = parent
9
- end
10
-
11
- class DataType
12
- module AnyEnum
13
- include FunctionalLightService::Monad
14
-
15
- def match(&block)
16
- parent.match(self, &block)
17
- end
18
-
19
- def to_s
20
- value.to_s
21
- end
22
-
23
- def name
24
- self.class.name.split("::")[-1]
25
- end
26
-
27
- # Returns array. Will fail on Nullary objects.
28
- # TODO: define a Unary module so we can define this method differently on Unary vs Binary
29
- def wrapped_values
30
- if is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
31
- value.values
32
- else
33
- [value]
34
- end
35
- end
36
- end
37
-
38
- module Nullary
39
- def initialize(*_args)
40
- @value = nil
41
- end
42
-
43
- def inspect
44
- name
45
- end
46
- end
47
-
48
- # TODO: this should probably be named Multary
49
- module Binary
50
- def initialize(*init)
51
- unless (init.count == 1 && init[0].is_a?(Hash)) || init.count == args.count
52
- raise ArgumentError, "Expected arguments for #{args}, got #{init}"
53
- end
54
-
55
- @value = if init.count == 1 && init[0].is_a?(Hash)
56
- args.zip(init[0].values).to_h
57
- else
58
- args.zip(init).to_h
59
- end
60
- end
61
-
62
- def inspect
63
- params = value.map { |k, v| "#{k}: #{v.inspect}" }
64
- "#{name}(#{params.join(', ')})"
65
- end
66
- end
67
-
68
- # rubocop:disable Metrics/MethodLength
69
- def self.create(parent, args)
70
- if args.include? :value
71
- raise ArgumentError, "#{args} may not contain the reserved name :value"
72
- end
73
-
74
- dt = Class.new(parent)
75
-
76
- dt.instance_eval do
77
- public_class_method :new
78
- include AnyEnum
79
- define_method(:args) { args }
80
-
81
- define_method(:parent) { parent }
82
- private :parent
83
- end
84
-
85
- case args.count
86
- when 0
87
- dt.instance_eval do
88
- include Nullary
89
- private :value
90
- end
91
- when 1
92
- dt.instance_eval do
93
- define_method(args[0].to_sym) { value }
94
- end
95
- else
96
- dt.instance_eval do
97
- include Binary
98
-
99
- args.each do |m|
100
- define_method(m) do
101
- @value[m]
102
- end
103
- end
104
- end
105
- end
106
-
107
- dt
108
- end
109
- # rubocop:enable Metrics/MethodLength
110
-
111
- class << self
112
- public :new
113
- end
114
- end
115
-
116
- def method_missing(m, *args)
117
- if @parent.const_defined?(m)
118
- super
119
- else
120
- @parent.const_set(m, DataType.create(@parent, args))
121
- end
122
- end
123
- end
124
-
125
- module_function
126
-
127
- # rubocop:disable Metrics/AbcSize
128
- # rubocop:disable Metrics/CyclomaticComplexity
129
- # rubocop:disable Metrics/MethodLength
130
- # rubocop:disable Metrics/PerceivedComplexity
131
- def enum(&block)
132
- mod = Class.new do # the enum to be built
133
- private_class_method :new
134
-
135
- def self.match(obj, &block)
136
- caller_ctx = block.binding.eval 'self'
137
-
138
- matcher = self::Matcher.new(obj)
139
- matcher.instance_eval(&block)
140
-
141
- variants_in_match = matcher.matches.collect do |e|
142
- e[1].name.split('::')[-1].to_sym
143
- end.uniq.sort
144
- variants_not_covered = variants - variants_in_match
145
- unless variants_not_covered.empty?
146
- raise Enum::MatchError, "Match is non-exhaustive, #{variants_not_covered} not covered"
147
- end
148
-
149
- type_matches = matcher.matches.select { |r| r[0].is_a?(r[1]) }
150
-
151
- type_matches.each do |match|
152
- obj, _type, block, args, guard = match
153
-
154
- return caller_ctx.instance_eval(&block) if args.count.zero?
155
-
156
- if args.count != obj.args.count
157
- msg = "Pattern (#{args.join(', ')}) must match (#{obj.args.join(', ')})"
158
- raise Enum::MatchError, msg
159
- end
160
-
161
- guard_ctx = guard_context(obj, args)
162
- return caller_ctx.instance_exec(* obj.wrapped_values, &block) unless guard
163
-
164
- if guard && guard_ctx.instance_exec(obj, &guard)
165
- return caller_ctx.instance_exec(* obj.wrapped_values, &block)
166
- end
167
- end
168
-
169
- raise Enum::MatchError, "No match could be made"
170
- end
171
-
172
- def self.variants
173
- constants - %i[Matcher MatchError]
174
- end
175
-
176
- def self.guard_context(obj, args)
177
- if obj.is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
178
- Struct.new(*args).new(*obj.value.values)
179
- else
180
- Struct.new(*args).new(obj.value)
181
- end
182
- end
183
- end
184
- enum = EnumBuilder.new(mod)
185
- enum.instance_eval(&block)
186
-
187
- type_variants = mod.constants
188
-
189
- matcher = Class.new do
190
- def initialize(obj)
191
- @obj = obj
192
- @matches = []
193
- @vars = []
194
- end
195
-
196
- attr_reader :matches, :vars
197
-
198
- def where(&guard)
199
- guard
200
- end
201
-
202
- type_variants.each do |m|
203
- define_method(m) do |guard = nil, &inner_block|
204
- raise ArgumentError, "No block given to `#{m}`" if inner_block.nil?
205
-
206
- params_spec = inner_block.parameters
207
- if params_spec.any? { |spec| spec.size < 2 }
208
- msg = "Unnamed param found in block parameters: #{params_spec.inspect}"
209
- raise ArgumentError, msg
210
- end
211
- if params_spec.any? { |spec| spec[0] != :req && spec[0] != :opt }
212
- msg = "Only :req & :opt params allowed; parameters=#{params_spec.inspect}"
213
- raise ArgumentError, msg
214
- end
215
-
216
- args = params_spec.map { |spec| spec[1] }
217
-
218
- type = mod.const_get(m)
219
-
220
- guard = nil if guard && !guard.is_a?(Proc)
221
-
222
- @matches << [@obj, type, inner_block, args, guard]
223
- end
224
- end
225
- end
226
-
227
- mod.const_set(:Matcher, matcher)
228
-
229
- type_variants.each do |variant|
230
- mod.singleton_class.class_exec do
231
- define_method(variant) do |*args|
232
- const_get(variant).new(*args)
233
- end
234
- end
235
- end
236
- mod
237
- end
238
- # rubocop:enable Metrics/AbcSize
239
- # rubocop:enable Metrics/CyclomaticComplexity
240
- # rubocop:enable Metrics/MethodLength
241
- # rubocop:enable Metrics/PerceivedComplexity
242
-
243
- def impl(enum_type, &block)
244
- enum_type.variants.each do |v|
245
- name = "#{enum_type.name}::#{v}"
246
- type = Kernel.eval(name)
247
- type.class_eval(&block)
248
- end
249
- end
250
- end
1
+ # frozen_string_literal: true
2
+
3
+ module FunctionalLightService
4
+ module Enum
5
+ class MatchError < StandardError; end
6
+ end
7
+
8
+ class EnumBuilder
9
+ def initialize(parent)
10
+ @parent = parent
11
+ end
12
+
13
+ class DataType
14
+ module AnyEnum
15
+ include FunctionalLightService::Monad
16
+
17
+ def match(&)
18
+ parent.match(self, &)
19
+ end
20
+
21
+ def to_s
22
+ value.to_s
23
+ end
24
+
25
+ def name
26
+ self.class.name.split("::")[-1]
27
+ end
28
+
29
+ # Returns array. Will fail on Nullary objects.
30
+ # TODO: define a Unary module so we can define this method differently on Unary vs Binary
31
+ def wrapped_values
32
+ if is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
33
+ value.values
34
+ else
35
+ [value]
36
+ end
37
+ end
38
+
39
+ # Supporto al pattern matching nativo di Ruby (case/in):
40
+ # case result
41
+ # in FunctionalLightService::Result::Success(s) then ...
42
+ # in FunctionalLightService::Result::Failure(f) then ...
43
+ # end
44
+ def deconstruct
45
+ is_a?(FunctionalLightService::EnumBuilder::DataType::Nullary) ? [] : wrapped_values
46
+ end
47
+
48
+ def deconstruct_keys(_keys)
49
+ if is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
50
+ value.dup
51
+ elsif is_a?(FunctionalLightService::EnumBuilder::DataType::Nullary)
52
+ {}
53
+ else
54
+ { args[0] => value }
55
+ end
56
+ end
57
+ end
58
+
59
+ module Nullary
60
+ def initialize(*_args)
61
+ @value = nil
62
+ end
63
+
64
+ def inspect
65
+ name
66
+ end
67
+ end
68
+
69
+ # TODO: this should probably be named Multary
70
+ module Binary
71
+ def initialize(*init)
72
+ unless (init.one? && init[0].is_a?(Hash)) || init.count == args.count
73
+ raise ArgumentError, "Expected arguments for #{args}, got #{init}"
74
+ end
75
+
76
+ @value = if init.one? && init[0].is_a?(Hash)
77
+ args.zip(init[0].values).to_h
78
+ else
79
+ args.zip(init).to_h
80
+ end
81
+ end
82
+
83
+ def inspect
84
+ params = value.map { |k, v| "#{k}: #{v.inspect}" }
85
+ "#{name}(#{params.join(', ')})"
86
+ end
87
+ end
88
+
89
+ # rubocop:disable Metrics/MethodLength
90
+ def self.create(parent, args)
91
+ if args.include? :value
92
+ raise ArgumentError, "#{args} may not contain the reserved name :value"
93
+ end
94
+
95
+ dt = Class.new(parent)
96
+
97
+ dt.instance_eval do
98
+ public_class_method :new
99
+ include AnyEnum
100
+
101
+ define_method(:args) { args }
102
+
103
+ define_method(:parent) { parent }
104
+ private :parent
105
+ end
106
+
107
+ case args.count
108
+ when 0
109
+ dt.instance_eval do
110
+ include Nullary
111
+
112
+ private :value
113
+ end
114
+ when 1
115
+ dt.instance_eval do
116
+ define_method(args[0].to_sym) { value }
117
+ end
118
+ else
119
+ dt.instance_eval do
120
+ include Binary
121
+
122
+ args.each do |m|
123
+ define_method(m) do
124
+ @value[m]
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ dt
131
+ end
132
+ # rubocop:enable Metrics/MethodLength
133
+
134
+ class << self
135
+ public :new
136
+ end
137
+ end
138
+
139
+ def method_missing(m, *args)
140
+ if @parent.const_defined?(m)
141
+ raise ArgumentError, "variant #{m} is already defined for this enum"
142
+ end
143
+
144
+ @parent.const_set(m, DataType.create(@parent, args))
145
+ end
146
+
147
+ def respond_to_missing?(_m, _include_all = false)
148
+ true
149
+ end
150
+ end
151
+
152
+ module_function
153
+
154
+ # rubocop:disable Metrics/AbcSize
155
+ # rubocop:disable Metrics/CyclomaticComplexity
156
+ # rubocop:disable Metrics/MethodLength
157
+ # rubocop:disable Metrics/PerceivedComplexity
158
+ def enum(&block)
159
+ mod = Class.new do # the enum to be built
160
+ private_class_method :new
161
+
162
+ def self.match(obj, &block)
163
+ # Binding#receiver: stesso risultato di binding.eval('self') senza eval
164
+ caller_ctx = block.binding.receiver
165
+
166
+ matcher = self::Matcher.new(obj)
167
+ matcher.instance_eval(&block)
168
+
169
+ # exhaustiveness check su classi memoizzate: niente split/sort di
170
+ # stringhe per chiamata
171
+ covered = matcher.matches.map { |e| e[1] }
172
+ missing = variant_classes.reject { |klass| covered.include?(klass) }
173
+ unless missing.empty?
174
+ missing_names = missing.map { |klass| klass.name.split('::')[-1].to_sym }
175
+ raise Enum::MatchError, "Match is non-exhaustive, #{missing_names} not covered"
176
+ end
177
+
178
+ type_matches = matcher.matches.select { |r| r[0].is_a?(r[1]) }
179
+
180
+ type_matches.each do |match|
181
+ obj, _type, block, args, guard = match
182
+
183
+ return caller_ctx.instance_eval(&block) if args.empty?
184
+
185
+ if args.count != obj.args.count
186
+ msg = "Pattern (#{args.join(', ')}) must match (#{obj.args.join(', ')})"
187
+ raise Enum::MatchError, msg
188
+ end
189
+
190
+ guard_ctx = guard_context(obj, args)
191
+ return caller_ctx.instance_exec(* obj.wrapped_values, &block) unless guard
192
+
193
+ if guard && guard_ctx.instance_exec(obj, &guard)
194
+ return caller_ctx.instance_exec(* obj.wrapped_values, &block)
195
+ end
196
+ end
197
+
198
+ raise Enum::MatchError, "No match could be made"
199
+ end
200
+
201
+ def self.variants
202
+ constants - %i[Matcher MatchError]
203
+ end
204
+
205
+ def self.variant_classes
206
+ @variant_classes ||= variants.map { |v| const_get(v) }.freeze
207
+ end
208
+
209
+ def self.guard_context(obj, args)
210
+ # Struct.new definisce una classe: va fatto una volta per firma,
211
+ # non a ogni match con guard
212
+ @guard_structs ||= {}
213
+ struct = @guard_structs[args] ||= Struct.new(*args)
214
+
215
+ if obj.is_a?(FunctionalLightService::EnumBuilder::DataType::Binary)
216
+ struct.new(*obj.value.values)
217
+ else
218
+ struct.new(obj.value)
219
+ end
220
+ end
221
+ end
222
+ enum = EnumBuilder.new(mod)
223
+ enum.instance_eval(&block)
224
+
225
+ type_variants = mod.constants
226
+
227
+ matcher = Class.new do
228
+ def initialize(obj)
229
+ @obj = obj
230
+ @matches = []
231
+ @vars = []
232
+ end
233
+
234
+ attr_reader :matches, :vars
235
+
236
+ def where(&guard)
237
+ guard
238
+ end
239
+
240
+ type_variants.each do |m|
241
+ define_method(m) do |guard = nil, &inner_block|
242
+ raise ArgumentError, "No block given to `#{m}`" if inner_block.nil?
243
+
244
+ params_spec = inner_block.parameters
245
+ if params_spec.any? { |spec| spec.size < 2 }
246
+ msg = "Unnamed param found in block parameters: #{params_spec.inspect}"
247
+ raise ArgumentError, msg
248
+ end
249
+ if params_spec.any? { |spec| spec[0] != :req && spec[0] != :opt }
250
+ msg = "Only :req & :opt params allowed; parameters=#{params_spec.inspect}"
251
+ raise ArgumentError, msg
252
+ end
253
+
254
+ args = params_spec.map { |spec| spec[1] }
255
+
256
+ type = mod.const_get(m)
257
+
258
+ guard = nil if guard && !guard.is_a?(Proc)
259
+
260
+ @matches << [@obj, type, inner_block, args, guard]
261
+ end
262
+ end
263
+ end
264
+
265
+ mod.const_set(:Matcher, matcher)
266
+
267
+ type_variants.each do |variant|
268
+ mod.singleton_class.class_exec do
269
+ define_method(variant) do |*args|
270
+ const_get(variant).new(*args)
271
+ end
272
+ end
273
+ end
274
+ mod
275
+ end
276
+ # rubocop:enable Metrics/AbcSize
277
+ # rubocop:enable Metrics/CyclomaticComplexity
278
+ # rubocop:enable Metrics/MethodLength
279
+ # rubocop:enable Metrics/PerceivedComplexity
280
+
281
+ def impl(enum_type, &block)
282
+ enum_type.variants.each do |v|
283
+ enum_type.const_get(v).class_eval(&block)
284
+ end
285
+ end
286
+ end
@@ -1,15 +1,21 @@
1
- class Object
2
- def null?
3
- false
4
- end
5
-
6
- def some?
7
- true
8
- end
9
- end
10
-
11
- # rubocop:disable Naming/MethodName
12
- def Maybe(obj)
13
- obj.nil? ? Null.instance : obj
14
- end
15
- # rubocop:enable Naming/MethodName
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ def null?
5
+ false
6
+ end
7
+
8
+ def some?
9
+ true
10
+ end
11
+ end
12
+
13
+ # rubocop:disable Naming/MethodName
14
+ def Maybe(obj)
15
+ FunctionalLightService::Deprecations.warn(
16
+ "Maybe()/Null are deprecated and will be removed in a future release; " \
17
+ "use FunctionalLightService::Option (Some/None) instead"
18
+ )
19
+ obj.nil? ? Null.instance : obj
20
+ end
21
+ # rubocop:enable Naming/MethodName