functional-light-service 6.2.0 → 6.3.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/lib/functional-light-service.rb +6 -36
- metadata +14 -307
- data/.github/workflows/project-build.yml +0 -71
- data/.gitignore +0 -24
- data/.rspec +0 -3
- data/.rubocop.yml +0 -101
- data/.solargraph.yml +0 -11
- data/AUDIT-functional-light-service.md +0 -352
- data/Appraisals +0 -7
- data/CHANGELOG.md +0 -292
- data/CODE_OF_CONDUCT.md +0 -22
- data/Gemfile +0 -4
- data/LICENSE +0 -22
- data/README.md +0 -1663
- data/Rakefile +0 -12
- data/VERSION +0 -1
- data/audit/bench.rb +0 -99
- data/audit/verify_findings.rb +0 -172
- data/functional-light-service.gemspec +0 -35
- data/gemfiles/dry_inflector_0_2_1.gemfile +0 -5
- data/gemfiles/i18n_1_8_11.gemfile +0 -5
- data/lib/functional-light-service/action.rb +0 -145
- data/lib/functional-light-service/configuration.rb +0 -36
- data/lib/functional-light-service/context/key_verifier.rb +0 -124
- data/lib/functional-light-service/context.rb +0 -208
- data/lib/functional-light-service/deprecations.rb +0 -26
- data/lib/functional-light-service/errors.rb +0 -9
- data/lib/functional-light-service/functional/enum.rb +0 -286
- data/lib/functional-light-service/functional/maybe.rb +0 -21
- data/lib/functional-light-service/functional/monad.rb +0 -77
- data/lib/functional-light-service/functional/null.rb +0 -88
- data/lib/functional-light-service/functional/option.rb +0 -100
- data/lib/functional-light-service/functional/result.rb +0 -129
- data/lib/functional-light-service/functional/sequencer.rb +0 -144
- data/lib/functional-light-service/i18n/localization_adapter.rb +0 -50
- data/lib/functional-light-service/localization_adapter.rb +0 -39
- data/lib/functional-light-service/localization_map.rb +0 -9
- data/lib/functional-light-service/organizer/execute.rb +0 -16
- data/lib/functional-light-service/organizer/iterate.rb +0 -30
- data/lib/functional-light-service/organizer/reduce_case.rb +0 -50
- data/lib/functional-light-service/organizer/reduce_if.rb +0 -19
- data/lib/functional-light-service/organizer/reduce_if_else.rb +0 -23
- data/lib/functional-light-service/organizer/reduce_until.rb +0 -22
- data/lib/functional-light-service/organizer/reduce_while.rb +0 -31
- data/lib/functional-light-service/organizer/scoped_reducable.rb +0 -15
- data/lib/functional-light-service/organizer/with_callback.rb +0 -28
- data/lib/functional-light-service/organizer/with_reducer.rb +0 -81
- data/lib/functional-light-service/organizer/with_reducer_factory.rb +0 -20
- data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +0 -111
- data/lib/functional-light-service/organizer.rb +0 -129
- data/lib/functional-light-service/testing/context_factory.rb +0 -48
- data/lib/functional-light-service/testing.rb +0 -3
- data/lib/functional-light-service/version.rb +0 -5
- data/resources/fail_actions.png +0 -0
- data/resources/light-service.png +0 -0
- data/resources/organizer_and_actions.png +0 -0
- data/resources/skip_actions.png +0 -0
- data/spec/acceptance/add_numbers_spec.rb +0 -11
- data/spec/acceptance/after_actions_spec.rb +0 -87
- data/spec/acceptance/around_each_spec.rb +0 -19
- data/spec/acceptance/before_actions_spec.rb +0 -115
- data/spec/acceptance/custom_log_from_organizer_spec.rb +0 -61
- data/spec/acceptance/deprecation_warnings_spec.rb +0 -82
- data/spec/acceptance/fail_spec.rb +0 -52
- data/spec/acceptance/log_from_organizer_spec.rb +0 -154
- data/spec/acceptance/message_localization_spec.rb +0 -119
- data/spec/acceptance/organizer/add_aliases_spec.rb +0 -28
- data/spec/acceptance/organizer/add_to_context_spec.rb +0 -54
- data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +0 -42
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +0 -90
- data/spec/acceptance/organizer/execute_spec.rb +0 -67
- data/spec/acceptance/organizer/iterate_spec.rb +0 -44
- data/spec/acceptance/organizer/reduce_case_spec.rb +0 -65
- data/spec/acceptance/organizer/reduce_if_else_spec.rb +0 -60
- data/spec/acceptance/organizer/reduce_if_spec.rb +0 -89
- data/spec/acceptance/organizer/reduce_until_spec.rb +0 -49
- data/spec/acceptance/organizer/reduce_while_spec.rb +0 -96
- data/spec/acceptance/organizer/with_callback_spec.rb +0 -113
- data/spec/acceptance/organizer_entry_point_spec.rb +0 -35
- data/spec/acceptance/rollback_spec.rb +0 -183
- data/spec/acceptance/skip_all_remaining_spec.rb +0 -139
- data/spec/acceptance/testing/context_factory_spec.rb +0 -54
- data/spec/action_expected_keys_spec.rb +0 -63
- data/spec/action_expects_and_promises_spec.rb +0 -97
- data/spec/action_optional_expected_keys_spec.rb +0 -107
- data/spec/action_promised_keys_spec.rb +0 -126
- data/spec/action_spec.rb +0 -97
- data/spec/context/inspect_spec.rb +0 -52
- data/spec/context_spec.rb +0 -301
- data/spec/examples/amount_spec.rb +0 -77
- data/spec/examples/controller_spec.rb +0 -63
- data/spec/examples/validate_address_spec.rb +0 -38
- data/spec/i18n_localization_adapter_spec.rb +0 -83
- data/spec/lib/deterministic/class_mixin_spec.rb +0 -24
- data/spec/lib/deterministic/currify_spec.rb +0 -90
- data/spec/lib/deterministic/monad_axioms.rb +0 -46
- data/spec/lib/deterministic/monad_spec.rb +0 -47
- data/spec/lib/deterministic/null_spec.rb +0 -65
- data/spec/lib/deterministic/option_spec.rb +0 -140
- data/spec/lib/deterministic/result/failure_spec.rb +0 -65
- data/spec/lib/deterministic/result/result_map_spec.rb +0 -155
- data/spec/lib/deterministic/result/result_shared.rb +0 -25
- data/spec/lib/deterministic/result/success_spec.rb +0 -41
- data/spec/lib/deterministic/result_spec.rb +0 -63
- data/spec/lib/deterministic/sequencer_spec.rb +0 -506
- data/spec/lib/edge_cases_spec.rb +0 -156
- data/spec/lib/enum_spec.rb +0 -114
- data/spec/lib/native_pattern_matching_spec.rb +0 -74
- data/spec/localization_adapter_spec.rb +0 -66
- data/spec/organizer/with_reducer_spec.rb +0 -56
- data/spec/organizer_key_aliases_spec.rb +0 -29
- data/spec/organizer_spec.rb +0 -115
- data/spec/readme_spec.rb +0 -45
- data/spec/sample/calculates_order_tax_action_spec.rb +0 -16
- data/spec/sample/calculates_tax_spec.rb +0 -30
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +0 -55
- data/spec/sample/provides_free_shipping_action_spec.rb +0 -25
- data/spec/sample/tax/calculates_order_tax_action.rb +0 -10
- data/spec/sample/tax/calculates_tax.rb +0 -11
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +0 -28
- data/spec/sample/tax/provides_free_shipping_action.rb +0 -11
- data/spec/spec_helper.rb +0 -32
- data/spec/support.rb +0 -1
- data/spec/test_doubles.rb +0 -656
- data/spec/testing/context_factory/iterate_spec.rb +0 -39
- data/spec/testing/context_factory/reduce_if_spec.rb +0 -40
- data/spec/testing/context_factory/reduce_until_spec.rb +0 -40
- data/spec/testing/context_factory/with_callback_spec.rb +0 -38
- data/spec/testing/context_factory_spec.rb +0 -76
data/Rakefile
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
require "bundler/gem_tasks"
|
|
2
|
-
|
|
3
|
-
require "rubygems"
|
|
4
|
-
require "bundler/setup"
|
|
5
|
-
|
|
6
|
-
require 'rspec/core/rake_task'
|
|
7
|
-
require 'rubocop/rake_task'
|
|
8
|
-
RuboCop::RakeTask.new
|
|
9
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
10
|
-
|
|
11
|
-
task(:default).clear
|
|
12
|
-
task :default => %i[spec rubocop]
|
data/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.5.4
|
data/audit/bench.rb
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# Micro-benchmark dei costi nascosti individuati nell'audit
|
|
2
|
-
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
3
|
-
require 'functional-light-service'
|
|
4
|
-
require 'benchmark/ips'
|
|
5
|
-
|
|
6
|
-
include FunctionalLightService::Prelude::Result
|
|
7
|
-
include FunctionalLightService::Prelude::Option
|
|
8
|
-
|
|
9
|
-
puts "Ruby #{RUBY_VERSION}, YJIT: #{defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? ? 'on' : 'off'}"
|
|
10
|
-
puts
|
|
11
|
-
|
|
12
|
-
some = Some(42)
|
|
13
|
-
success = Success(42)
|
|
14
|
-
|
|
15
|
-
# 1) Costo del motore match (Option#value_or usa match) vs equivalente diretto
|
|
16
|
-
Benchmark.ips do |x|
|
|
17
|
-
x.report("Option#value_or (match engine)") { some.value_or(0) }
|
|
18
|
-
x.report("equivalente is_a? diretto") { some.is_a?(FunctionalLightService::Option::Some) ? some.value : 0 }
|
|
19
|
-
x.report("case/in nativo Ruby") do
|
|
20
|
-
case some
|
|
21
|
-
in FunctionalLightService::Option::Some then some.value
|
|
22
|
-
else 0
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
x.compare!
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# 2) match esplicito con guard (Struct.new per chiamata) su Result#+
|
|
29
|
-
Benchmark.ips do |x|
|
|
30
|
-
other = Success(1)
|
|
31
|
-
x.report("Result#+ (match con guard -> Struct.new/call)") { success + other }
|
|
32
|
-
x.report("somma diretta is_a?") do
|
|
33
|
-
if success.is_a?(FunctionalLightService::Result::Success) && other.is_a?(FunctionalLightService::Result::Success)
|
|
34
|
-
FunctionalLightService::Result::Success.new(success.value + other.value)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
x.compare!
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# 3) Result#map (bind, senza match) — per confronto: la parte "sana"
|
|
41
|
-
Benchmark.ips do |x|
|
|
42
|
-
f = ->(v) { Success(v + 1) }
|
|
43
|
-
x.report("Result#map via bind") { success.map(f) }
|
|
44
|
-
x.report("lambda diretta") { success.is_a?(FunctionalLightService::Result::Success) ? f.call(success.value) : success }
|
|
45
|
-
x.compare!
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# 4) Overhead di Organizer.with: caller(1..1) + methods.include?(:call)
|
|
49
|
-
Benchmark.ips do |x|
|
|
50
|
-
klass = Class.new { def self.call; end }
|
|
51
|
-
x.report("caller(1..1) + methods.include?") do
|
|
52
|
-
c = caller(1..1).first
|
|
53
|
-
c =~ /`(.*)'/
|
|
54
|
-
klass.methods.include?(:call)
|
|
55
|
-
end
|
|
56
|
-
x.report("respond_to?(:call) soltanto") { klass.respond_to?(:call) }
|
|
57
|
-
x.compare!
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# 5) define_accessor_methods_for_keys: singleton methods per ogni context
|
|
61
|
-
Benchmark.ips do |x|
|
|
62
|
-
keys = %i[number total counter]
|
|
63
|
-
x.report("nuovo Context + define accessor per keys") do
|
|
64
|
-
ctx = FunctionalLightService::Context.make(:number => 1, :total => 2, :counter => 3)
|
|
65
|
-
ctx.define_accessor_methods_for_keys(keys)
|
|
66
|
-
end
|
|
67
|
-
x.report("nuovo Context senza accessor") do
|
|
68
|
-
FunctionalLightService::Context.make(:number => 1, :total => 2, :counter => 3)
|
|
69
|
-
end
|
|
70
|
-
x.compare!
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# 6) Context#[] con alias attivi (Hash#key reverse scan) vs Hash puro
|
|
74
|
-
Benchmark.ips do |x|
|
|
75
|
-
ctx = FunctionalLightService::Context.make(:a => 1, :b => 2, :c => 3)
|
|
76
|
-
ctx.assign_aliases(:a => :alfa)
|
|
77
|
-
h = { :a => 1, :b => 2, :c => 3 }
|
|
78
|
-
x.report("Context#[] (con lookup alias)") { ctx[:b] }
|
|
79
|
-
x.report("Hash#[] puro") { h[:b] }
|
|
80
|
-
x.compare!
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# 7) Organizer end-to-end: quota di overhead per call minimale
|
|
84
|
-
class BenchAdd
|
|
85
|
-
extend FunctionalLightService::Action
|
|
86
|
-
expects :number
|
|
87
|
-
executed { |ctx| ctx[:number] = ctx[:number] + 1 }
|
|
88
|
-
end
|
|
89
|
-
class BenchOrg
|
|
90
|
-
extend FunctionalLightService::Organizer
|
|
91
|
-
def self.call(n)
|
|
92
|
-
with(:number => n).reduce([BenchAdd])
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
Benchmark.ips do |x|
|
|
96
|
-
x.report("Organizer.call 1 action") { BenchOrg.call(1) }
|
|
97
|
-
x.report("lavoro utile equivalente") { { :number => 1 }.tap { |h| h[:number] += 1 } }
|
|
98
|
-
x.compare!
|
|
99
|
-
end
|
data/audit/verify_findings.rb
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# Verifica dei finding dell'audit — eseguito contro lib/ reale
|
|
2
|
-
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
3
|
-
require 'functional-light-service'
|
|
4
|
-
|
|
5
|
-
results = []
|
|
6
|
-
def check(results, name)
|
|
7
|
-
ok, detail = yield
|
|
8
|
-
results << [name, ok, detail]
|
|
9
|
-
rescue => e
|
|
10
|
-
results << [name, :exception, "#{e.class}: #{e.message}"]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# ---------- F1: before_actions perso alla seconda chiamata ----------
|
|
14
|
-
class AddOne
|
|
15
|
-
extend FunctionalLightService::Action
|
|
16
|
-
expects :number
|
|
17
|
-
promises :number
|
|
18
|
-
executed { |ctx| ctx.number = ctx.number + 1 }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
class Org1
|
|
22
|
-
extend FunctionalLightService::Organizer
|
|
23
|
-
before_actions ->(ctx) { ctx.number -= 10 if ctx.current_action == AddOne }
|
|
24
|
-
def self.call(number)
|
|
25
|
-
with(:number => number).reduce([AddOne])
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
check(results, "F1 before_actions perso alla 2a chiamata") do
|
|
30
|
-
r1 = Org1.call(0).fetch(:number) # atteso: -10 + 1 = -9
|
|
31
|
-
r2 = Org1.call(0).fetch(:number) # se bug: hook perso -> 1
|
|
32
|
-
[r1 == -9 && r2 == 1, "prima call=#{r1}, seconda call=#{r2}"]
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# ---------- F2: Context#fetch — nessun KeyError e scrittura su read ----------
|
|
36
|
-
check(results, "F2a fetch(:missing) ritorna nil invece di KeyError e scrive la chiave") do
|
|
37
|
-
ctx = FunctionalLightService::Context.make({})
|
|
38
|
-
v = ctx.fetch(:missing)
|
|
39
|
-
[v.nil? && ctx.to_h.key?(:missing), "valore=#{v.inspect}, chiavi dopo fetch=#{ctx.keys.inspect}"]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
check(results, "F2b fetch con default sovrascrive un valore falsy esistente") do
|
|
43
|
-
ctx = FunctionalLightService::Context.make(:flag => false)
|
|
44
|
-
v = ctx.fetch(:flag, true)
|
|
45
|
-
[v == true && ctx[:flag] == true, "fetch(:flag, true)=#{v.inspect}, ctx[:flag] ora=#{ctx[:flag].inspect} (era false)"]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# ---------- F3: alias asimmetrico lettura/scrittura ----------
|
|
49
|
-
check(results, "F3 scrittura su alias persa in lettura") do
|
|
50
|
-
ctx = FunctionalLightService::Context.make(:codice_fiscale => "ABC")
|
|
51
|
-
ctx.assign_aliases(:codice_fiscale => :cf)
|
|
52
|
-
ctx[:cf] = "NUOVO" # scrive la chiave :cf direttamente
|
|
53
|
-
read = ctx[:cf] # legge tradotto -> :codice_fiscale -> "ABC"
|
|
54
|
-
[read == "ABC", "dopo ctx[:cf]='NUOVO', ctx[:cf]=#{read.inspect}; hash=#{ctx.to_h.inspect}"]
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# ---------- F4: accessor non definito per chiavi che collidono con metodi Hash ----------
|
|
58
|
-
class UsesSize
|
|
59
|
-
extend FunctionalLightService::Action
|
|
60
|
-
expects :size
|
|
61
|
-
executed { |ctx| ctx[:observed] = ctx.size } # l'utente si aspetta il valore di :size
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
check(results, "F4 expects :size -> accessor non definito, ritorna Hash#size") do
|
|
65
|
-
result = UsesSize.execute(:size => 999)
|
|
66
|
-
[result[:observed] != 999, "ctx.size dentro l'action=#{result[:observed].inspect} (atteso dal punto di vista utente: 999)"]
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# ---------- F5: @ctx a livello di classe Action (retention + race) ----------
|
|
70
|
-
check(results, "F5 la classe Action trattiene l'ultimo context in @ctx") do
|
|
71
|
-
AddOne.execute(:number => 5)
|
|
72
|
-
held = AddOne.instance_variable_get(:@ctx)
|
|
73
|
-
[held.is_a?(FunctionalLightService::Context), "AddOne @ctx = #{held.inspect[0,80]}"]
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# ---------- F6: fail! muta l'hash di opzioni del chiamante ----------
|
|
77
|
-
check(results, "F6 fail! cancella :error_code dall'hash del chiamante") do
|
|
78
|
-
ctx = FunctionalLightService::Context.make({})
|
|
79
|
-
opts = { :error_code => 500 }
|
|
80
|
-
ctx.fail!("boom", opts)
|
|
81
|
-
[!opts.key?(:error_code), "opts dopo fail! = #{opts.inspect}"]
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# ---------- F7: Null#respond_to? con firma sbagliata ----------
|
|
85
|
-
check(results, "F7 Null#respond_to?(m, true) solleva ArgumentError") do
|
|
86
|
-
begin
|
|
87
|
-
Null.instance.respond_to?(:foo, true)
|
|
88
|
-
[false, "nessuna eccezione"]
|
|
89
|
-
rescue ArgumentError => e
|
|
90
|
-
[true, "ArgumentError: #{e.message}"]
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# ---------- F8: Context#[] con aliases usa Hash#key (reverse lookup O(n)) ----------
|
|
95
|
-
check(results, "F8 lettura di chiave originale con molti alias resta corretta (sanity)") do
|
|
96
|
-
ctx = FunctionalLightService::Context.make(:a => 1)
|
|
97
|
-
ctx.assign_aliases(:a => :b)
|
|
98
|
-
[ctx[:b] == 1 && ctx[:a] == 1, "ctx[:a]=#{ctx[:a]}, ctx[:b]=#{ctx[:b]}"]
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# ---------- F9: skip_remaining! dentro iterate viene resettato ad ogni item ----------
|
|
102
|
-
class SkipAll
|
|
103
|
-
extend FunctionalLightService::Action
|
|
104
|
-
executed { |ctx| ctx.skip_remaining!("basta") if ctx[:counter] == 2 }
|
|
105
|
-
end
|
|
106
|
-
class CollectAction
|
|
107
|
-
extend FunctionalLightService::Action
|
|
108
|
-
executed { |ctx| (ctx[:seen] ||= []) << ctx[:counter] }
|
|
109
|
-
end
|
|
110
|
-
class OrgIter
|
|
111
|
-
extend FunctionalLightService::Organizer
|
|
112
|
-
def self.call(ctx)
|
|
113
|
-
with(ctx).reduce([iterate(:counters, [SkipAll, CollectAction])])
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
check(results, "F9 skip_remaining! dentro iterate NON ferma l'iterazione") do
|
|
118
|
-
r = OrgIter.call(:counters => [1, 2, 3])
|
|
119
|
-
[r[:seen] == [1, 3], "seen=#{r[:seen].inspect} (2 saltato, ma 3 processato: lo skip e' stato resettato)"]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# ---------- F10: succeed! + message perso da reset_skip_remaining! in scoped_reduce ----------
|
|
123
|
-
check(results, "F10 reset_skip_remaining! azzera anche message/outcome") do
|
|
124
|
-
ctx = FunctionalLightService::Context.make({})
|
|
125
|
-
ctx.succeed!("fatto bene")
|
|
126
|
-
ctx.reset_skip_remaining!
|
|
127
|
-
[ctx.message == '', "message dopo reset=#{ctx.message.inspect}"]
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# ---------- F11: Context#select/map degradano a Hash ----------
|
|
131
|
-
check(results, "F11 Context#select ritorna Hash, non Context (perde outcome)") do
|
|
132
|
-
ctx = FunctionalLightService::Context.make(:a => 1)
|
|
133
|
-
sel = ctx.select { |_k, _v| true }
|
|
134
|
-
[!sel.is_a?(FunctionalLightService::Context), "select.class=#{sel.class}"]
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# ---------- F12: Some(nil) e' costruibile ----------
|
|
138
|
-
check(results, "F12 Some(nil) e' consentito (Option non valida il nil)") do
|
|
139
|
-
s = FunctionalLightService::Option::Some.new(nil)
|
|
140
|
-
[s.some? && s.value.nil?, "Some(nil).some?=#{s.some?}, value=nil"]
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# ---------- F13: rollback con azione duplicata nella lista ----------
|
|
144
|
-
class RollA
|
|
145
|
-
extend FunctionalLightService::Action
|
|
146
|
-
executed { |ctx| (ctx[:trace] ||= []) << :a }
|
|
147
|
-
rolled_back { |ctx| (ctx[:rb] ||= []) << :a }
|
|
148
|
-
end
|
|
149
|
-
class RollB
|
|
150
|
-
extend FunctionalLightService::Action
|
|
151
|
-
executed { |ctx| (ctx[:trace] ||= []) << :b; ctx.fail_with_rollback!("ko") if ctx[:trace].count(:b) == 2 }
|
|
152
|
-
rolled_back { |ctx| (ctx[:rb] ||= []) << :b }
|
|
153
|
-
end
|
|
154
|
-
class OrgRoll
|
|
155
|
-
extend FunctionalLightService::Organizer
|
|
156
|
-
def self.call(ctx)
|
|
157
|
-
with(ctx).reduce([RollB, RollA, RollB]) # RollB duplicata; fallisce la SECONDA RollB
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
check(results, "F13 rollback con azione duplicata: index trova la 1a occorrenza -> rollback parziale") do
|
|
162
|
-
r = OrgRoll.call({})
|
|
163
|
-
# eseguite: B, A, B(fail). Rollback atteso: B, A, B. Con il bug: index(RollB)=0 -> solo [RollB].take(1) -> rollback solo B
|
|
164
|
-
[r[:rb] == [:b], "trace=#{r[:trace].inspect}, rollback eseguiti=#{r[:rb].inspect} (attesi: [:b, :a, :b])"]
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
puts
|
|
168
|
-
results.each do |name, ok, detail|
|
|
169
|
-
status = ok == true ? "CONFERMATO" : (ok == :exception ? "ECCEZIONE " : "NON RIPRODOTTO")
|
|
170
|
-
puts "[#{status}] #{name}"
|
|
171
|
-
puts " -> #{detail}"
|
|
172
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
require File.expand_path('../lib/functional-light-service/version', __FILE__)
|
|
2
|
-
|
|
3
|
-
Gem::Specification.new do |gem|
|
|
4
|
-
gem.authors = ["Boscolo Michele"]
|
|
5
|
-
gem.email = ["miboscol@gmail.com"]
|
|
6
|
-
gem.description = %q{FunctionalLightService combines the Organizer/Action/Context pattern of LightService with functional programming constructs (Result/Option monads, pattern matching) inspired by Deterministic: complex workflows are organized into small single-purpose actions with functional error handling.}
|
|
7
|
-
gem.summary = %q{A service skeleton with an emphasis on simplicity with a pinch a functional programming}
|
|
8
|
-
gem.homepage = "https://github.com/sphynx79/functional-light-service"
|
|
9
|
-
gem.license = "MIT"
|
|
10
|
-
|
|
11
|
-
gem.files = `git ls-files`.split($\)
|
|
12
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
-
gem.name = "functional-light-service"
|
|
14
|
-
gem.require_paths = ["lib"]
|
|
15
|
-
gem.version = FunctionalLightService::VERSION
|
|
16
|
-
gem.required_ruby_version = '>= 3.1.0'
|
|
17
|
-
|
|
18
|
-
gem.add_runtime_dependency("dry-inflector", ">= 0.2.1", "< 2")
|
|
19
|
-
# logger non e' piu una default gem da Ruby 3.5/4.0: senza questa
|
|
20
|
-
# dichiarazione `require 'logger'` fallisce sotto Bundler
|
|
21
|
-
gem.add_runtime_dependency("logger", "~> 1.5")
|
|
22
|
-
|
|
23
|
-
# i18n e' opzionale a runtime: l'adapter I18n viene usato solo se
|
|
24
|
-
# l'applicazione ospite carica la gem (vedi Configuration)
|
|
25
|
-
gem.add_development_dependency("i18n", "~> 1.8", ">= 1.8.11")
|
|
26
|
-
gem.add_development_dependency("rake", "~> 13.0")
|
|
27
|
-
gem.add_development_dependency("rspec", "~> 3.13")
|
|
28
|
-
gem.add_development_dependency("simplecov", "~> 0.22")
|
|
29
|
-
gem.add_development_dependency("simplecov-cobertura", "~> 3.0")
|
|
30
|
-
gem.add_development_dependency("rubocop", "~> 1.75")
|
|
31
|
-
gem.add_development_dependency("rubocop-performance", "~> 1.20")
|
|
32
|
-
gem.add_development_dependency("pry", "~> 0.15")
|
|
33
|
-
gem.add_development_dependency("solargraph", "~> 0.50")
|
|
34
|
-
gem.add_development_dependency("benchmark-ips", "~> 2.13")
|
|
35
|
-
end
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunctionalLightService
|
|
4
|
-
module Action
|
|
5
|
-
def self.extended(base_class)
|
|
6
|
-
base_class.extend Macros
|
|
7
|
-
base_class.extend FunctionalLightService::Prelude::Result
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.included(base_class)
|
|
11
|
-
FunctionalLightService::Deprecations.warn(
|
|
12
|
-
"Including FunctionalLightService::Action is deprecated; " \
|
|
13
|
-
"use `extend FunctionalLightService::Action` instead"
|
|
14
|
-
)
|
|
15
|
-
base_class.extend Macros
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
module Macros
|
|
19
|
-
def expects(*args)
|
|
20
|
-
if expect_key_having_default?(args)
|
|
21
|
-
available_defaults[args.first] = args.last[:default]
|
|
22
|
-
|
|
23
|
-
args = [args.first]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
expected_keys.concat(args)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def promises(*args)
|
|
30
|
-
promised_keys.concat(args)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def expected_keys
|
|
34
|
-
@expected_keys ||= []
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def promised_keys
|
|
38
|
-
@promised_keys ||= []
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def executed
|
|
42
|
-
define_singleton_method :execute do |context = {}|
|
|
43
|
-
action_context = create_action_context(context)
|
|
44
|
-
return action_context if action_context.stop_processing?
|
|
45
|
-
|
|
46
|
-
# Store the action within the context
|
|
47
|
-
action_context.current_action = self
|
|
48
|
-
|
|
49
|
-
Context::KeyVerifier.verify_keys(action_context, self) do
|
|
50
|
-
action_context.define_accessor_methods_for_keys(all_keys)
|
|
51
|
-
|
|
52
|
-
catch(:jump_when_failed) do
|
|
53
|
-
call_before_action(action_context)
|
|
54
|
-
yield(action_context)
|
|
55
|
-
call_after_action(action_context)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def rolled_back
|
|
62
|
-
msg = "`rolled_back` macro can not be invoked again"
|
|
63
|
-
raise msg if respond_to?(:rollback)
|
|
64
|
-
|
|
65
|
-
define_singleton_method :rollback do |context = {}|
|
|
66
|
-
yield(context)
|
|
67
|
-
|
|
68
|
-
context
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def create_action_context(context)
|
|
75
|
-
# I default vanno applicati anche quando l'action gira dentro un
|
|
76
|
-
# organizer (il context è già un Context): prima dell'early return.
|
|
77
|
-
# Il guard sull'ivar evita lavoro (e scritture lazy) nel percorso caldo
|
|
78
|
-
apply_expects_defaults(context) if @available_defaults
|
|
79
|
-
|
|
80
|
-
return context if context.is_a? FunctionalLightService::Context
|
|
81
|
-
|
|
82
|
-
FunctionalLightService::Context.make(context)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def apply_expects_defaults(context)
|
|
86
|
-
usable_defaults(context).each do |ctx_key, default|
|
|
87
|
-
context[ctx_key] = extract_default(default, context)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def available_defaults
|
|
92
|
-
@available_defaults ||= {}
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def expect_key_having_default?(key)
|
|
96
|
-
return false unless key.size == 2 && key.last.is_a?(Hash)
|
|
97
|
-
return true if key.last.key?(:default)
|
|
98
|
-
|
|
99
|
-
bad_key = key.last.keys.first
|
|
100
|
-
err_msg = "Specify defaults with a `default` key. You have #{bad_key}."
|
|
101
|
-
raise UnusableExpectKeyDefaultError, err_msg
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def missing_expected_keys(context)
|
|
105
|
-
# context.key? risolve anche gli alias: `expected_keys - context.keys`
|
|
106
|
-
# (upstream) darebbe falsi mancanti sulle chiavi aliasate
|
|
107
|
-
expected_keys.reject { |key| context.key?(key) }
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def usable_defaults(context)
|
|
111
|
-
available_defaults.slice(
|
|
112
|
-
*(missing_expected_keys(context) & available_defaults.keys)
|
|
113
|
-
)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def extract_default(default, context)
|
|
117
|
-
return default unless default.respond_to?(:call)
|
|
118
|
-
|
|
119
|
-
default.call(context)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def all_keys
|
|
123
|
-
expected_keys + promised_keys
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def call_before_action(context)
|
|
127
|
-
invoke_callbacks(context[:_before_actions], context)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def call_after_action(context)
|
|
131
|
-
invoke_callbacks(context[:_after_actions], context)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def invoke_callbacks(callbacks, context)
|
|
135
|
-
return context unless callbacks
|
|
136
|
-
|
|
137
|
-
callbacks.each do |cb|
|
|
138
|
-
cb.call(context)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
context
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FunctionalLightService
|
|
4
|
-
class Configuration
|
|
5
|
-
class << self
|
|
6
|
-
attr_writer :logger, :localization_adapter, :locale
|
|
7
|
-
|
|
8
|
-
def logger
|
|
9
|
-
@logger = _default_logger unless instance_variable_defined?("@logger")
|
|
10
|
-
@logger
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def localization_adapter
|
|
14
|
-
# La gem i18n non è una dipendenza: l'adapter I18n viene scelto solo
|
|
15
|
-
# se la costante è già stata caricata dall'applicazione ospite
|
|
16
|
-
@localization_adapter ||= if Module.const_defined?('I18n')
|
|
17
|
-
FunctionalLightService::I18n::LocalizationAdapter.new
|
|
18
|
-
else
|
|
19
|
-
LocalizationAdapter.new
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def locale
|
|
24
|
-
@locale ||= :en
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def _default_logger
|
|
30
|
-
logger = Logger.new(nil)
|
|
31
|
-
logger.level = Logger::WARN
|
|
32
|
-
logger
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,124 +0,0 @@
|
|
|
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 organized_by
|
|
120
|
-
_aliases _before_actions _after_actions].freeze
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|