functional-light-service 0.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/project-build.yml +35 -11
- data/.rubocop.yml +101 -160
- data/AUDIT-functional-light-service.md +352 -0
- data/CHANGELOG.md +38 -0
- data/README.md +54 -2
- data/audit/bench.rb +99 -0
- data/audit/verify_findings.rb +172 -0
- data/functional-light-service.gemspec +15 -21
- data/lib/functional-light-service/action.rb +97 -101
- data/lib/functional-light-service/configuration.rb +26 -24
- data/lib/functional-light-service/context/key_verifier.rb +124 -118
- data/lib/functional-light-service/context.rb +63 -20
- data/lib/functional-light-service/deprecations.rb +26 -0
- data/lib/functional-light-service/errors.rb +8 -6
- data/lib/functional-light-service/functional/enum.rb +286 -250
- data/lib/functional-light-service/functional/maybe.rb +21 -15
- data/lib/functional-light-service/functional/monad.rb +77 -66
- data/lib/functional-light-service/functional/null.rb +88 -74
- data/lib/functional-light-service/functional/option.rb +100 -97
- data/lib/functional-light-service/functional/result.rb +129 -116
- data/lib/functional-light-service/localization_adapter.rb +48 -47
- data/lib/functional-light-service/organizer/execute.rb +16 -14
- data/lib/functional-light-service/organizer/iterate.rb +30 -25
- data/lib/functional-light-service/organizer/reduce_if.rb +19 -17
- data/lib/functional-light-service/organizer/reduce_until.rb +22 -20
- data/lib/functional-light-service/organizer/scoped_reducable.rb +15 -13
- data/lib/functional-light-service/organizer/with_callback.rb +28 -26
- data/lib/functional-light-service/organizer/with_reducer.rb +81 -77
- data/lib/functional-light-service/organizer/with_reducer_factory.rb +20 -18
- data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +110 -108
- data/lib/functional-light-service/organizer.rb +114 -114
- data/lib/functional-light-service/testing/context_factory.rb +48 -42
- data/lib/functional-light-service/testing.rb +3 -1
- data/lib/functional-light-service/version.rb +5 -3
- data/lib/functional-light-service.rb +30 -28
- data/spec/acceptance/after_actions_spec.rb +87 -71
- data/spec/acceptance/before_actions_spec.rb +115 -98
- data/spec/acceptance/custom_log_from_organizer_spec.rb +61 -60
- data/spec/acceptance/deprecation_warnings_spec.rb +82 -0
- data/spec/acceptance/fail_spec.rb +52 -50
- data/spec/acceptance/message_localization_spec.rb +119 -118
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +68 -65
- data/spec/acceptance/organizer/reduce_if_spec.rb +89 -89
- data/spec/acceptance/organizer/with_callback_spec.rb +113 -110
- data/spec/acceptance/{not_having_call_method_warning_spec.rb → organizer_entry_point_spec.rb} +10 -7
- data/spec/acceptance/rollback_spec.rb +183 -132
- data/spec/action_expects_and_promises_spec.rb +97 -93
- data/spec/action_promised_keys_spec.rb +126 -122
- data/spec/context_spec.rb +289 -197
- data/spec/examples/controller_spec.rb +63 -63
- data/spec/examples/validate_address_spec.rb +38 -37
- data/spec/lib/deterministic/currify_spec.rb +90 -88
- data/spec/lib/deterministic/null_spec.rb +6 -1
- data/spec/lib/deterministic/option_spec.rb +140 -137
- data/spec/lib/deterministic/result/result_map_spec.rb +155 -154
- data/spec/lib/deterministic/result/result_shared.rb +3 -2
- data/spec/lib/deterministic/result_spec.rb +2 -2
- data/spec/lib/edge_cases_spec.rb +156 -0
- data/spec/lib/enum_spec.rb +1 -1
- data/spec/lib/native_pattern_matching_spec.rb +74 -0
- data/spec/organizer_spec.rb +115 -114
- data/spec/readme_spec.rb +45 -47
- data/spec/sample/calculates_order_tax_action_spec.rb +16 -16
- data/spec/sample/calculates_tax_spec.rb +1 -1
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +55 -55
- data/spec/sample/tax/calculates_order_tax_action.rb +10 -9
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +28 -27
- data/spec/sample/tax/provides_free_shipping_action.rb +11 -10
- data/spec/spec_helper.rb +6 -0
- data/spec/test_doubles.rb +628 -599
- data/spec/testing/context_factory_spec.rb +21 -0
- metadata +45 -161
- data/lib/functional-light-service/organizer/verify_call_method_exists.rb +0 -29
- data/spec/acceptance/include_warning_spec.rb +0 -29
|
@@ -1,118 +1,124 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
return super(key)
|
|
156
|
+
super(resolve_key(key))
|
|
128
157
|
end
|
|
129
158
|
|
|
130
|
-
def
|
|
131
|
-
|
|
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: #{
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class
|
|
5
|
-
class
|
|
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
|