switchyard 7.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 +7 -0
- data/.github/workflows/project-build.yml +71 -0
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/.rubocop.yml +101 -0
- data/.solargraph.yml +11 -0
- data/Appraisals +7 -0
- data/CHANGELOG.md +303 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +1663 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/audit/bench.rb +99 -0
- data/audit/verify_findings.rb +172 -0
- data/gemfiles/dry_inflector_0_2_1.gemfile +5 -0
- data/gemfiles/i18n_1_8_11.gemfile +5 -0
- data/lib/switchyard/action.rb +145 -0
- data/lib/switchyard/configuration.rb +36 -0
- data/lib/switchyard/context/key_verifier.rb +124 -0
- data/lib/switchyard/context.rb +208 -0
- data/lib/switchyard/deprecations.rb +26 -0
- data/lib/switchyard/errors.rb +9 -0
- data/lib/switchyard/functional/enum.rb +286 -0
- data/lib/switchyard/functional/maybe.rb +21 -0
- data/lib/switchyard/functional/monad.rb +77 -0
- data/lib/switchyard/functional/null.rb +88 -0
- data/lib/switchyard/functional/option.rb +100 -0
- data/lib/switchyard/functional/result.rb +129 -0
- data/lib/switchyard/functional/sequencer.rb +144 -0
- data/lib/switchyard/i18n/localization_adapter.rb +50 -0
- data/lib/switchyard/localization_adapter.rb +39 -0
- data/lib/switchyard/localization_map.rb +9 -0
- data/lib/switchyard/organizer/execute.rb +16 -0
- data/lib/switchyard/organizer/iterate.rb +30 -0
- data/lib/switchyard/organizer/reduce_case.rb +50 -0
- data/lib/switchyard/organizer/reduce_if.rb +19 -0
- data/lib/switchyard/organizer/reduce_if_else.rb +23 -0
- data/lib/switchyard/organizer/reduce_until.rb +22 -0
- data/lib/switchyard/organizer/reduce_while.rb +31 -0
- data/lib/switchyard/organizer/scoped_reducable.rb +15 -0
- data/lib/switchyard/organizer/with_callback.rb +28 -0
- data/lib/switchyard/organizer/with_reducer.rb +81 -0
- data/lib/switchyard/organizer/with_reducer_factory.rb +20 -0
- data/lib/switchyard/organizer/with_reducer_log_decorator.rb +111 -0
- data/lib/switchyard/organizer.rb +129 -0
- data/lib/switchyard/testing/context_factory.rb +48 -0
- data/lib/switchyard/testing.rb +3 -0
- data/lib/switchyard/version.rb +5 -0
- data/lib/switchyard.rb +36 -0
- 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 +11 -0
- data/spec/acceptance/after_actions_spec.rb +87 -0
- data/spec/acceptance/around_each_spec.rb +19 -0
- data/spec/acceptance/before_actions_spec.rb +115 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +61 -0
- data/spec/acceptance/deprecation_warnings_spec.rb +82 -0
- data/spec/acceptance/fail_spec.rb +52 -0
- data/spec/acceptance/log_from_organizer_spec.rb +154 -0
- data/spec/acceptance/message_localization_spec.rb +119 -0
- data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +54 -0
- data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +90 -0
- data/spec/acceptance/organizer/execute_spec.rb +67 -0
- data/spec/acceptance/organizer/iterate_spec.rb +44 -0
- data/spec/acceptance/organizer/reduce_case_spec.rb +65 -0
- data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +89 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +49 -0
- data/spec/acceptance/organizer/reduce_while_spec.rb +96 -0
- data/spec/acceptance/organizer/with_callback_spec.rb +113 -0
- data/spec/acceptance/organizer_entry_point_spec.rb +35 -0
- data/spec/acceptance/rollback_spec.rb +183 -0
- data/spec/acceptance/skip_all_remaining_spec.rb +139 -0
- data/spec/acceptance/testing/context_factory_spec.rb +54 -0
- data/spec/action_expected_keys_spec.rb +63 -0
- data/spec/action_expects_and_promises_spec.rb +97 -0
- data/spec/action_optional_expected_keys_spec.rb +107 -0
- data/spec/action_promised_keys_spec.rb +126 -0
- data/spec/action_spec.rb +97 -0
- data/spec/context/inspect_spec.rb +52 -0
- data/spec/context_spec.rb +301 -0
- data/spec/examples/amount_spec.rb +77 -0
- data/spec/examples/controller_spec.rb +63 -0
- data/spec/examples/validate_address_spec.rb +38 -0
- data/spec/i18n_localization_adapter_spec.rb +83 -0
- data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
- data/spec/lib/deterministic/currify_spec.rb +90 -0
- data/spec/lib/deterministic/monad_axioms.rb +46 -0
- data/spec/lib/deterministic/monad_spec.rb +47 -0
- data/spec/lib/deterministic/null_spec.rb +65 -0
- data/spec/lib/deterministic/option_spec.rb +140 -0
- data/spec/lib/deterministic/result/failure_spec.rb +65 -0
- data/spec/lib/deterministic/result/result_map_spec.rb +155 -0
- data/spec/lib/deterministic/result/result_shared.rb +25 -0
- data/spec/lib/deterministic/result/success_spec.rb +41 -0
- data/spec/lib/deterministic/result_spec.rb +63 -0
- data/spec/lib/deterministic/sequencer_spec.rb +506 -0
- data/spec/lib/edge_cases_spec.rb +156 -0
- data/spec/lib/enum_spec.rb +114 -0
- data/spec/lib/native_pattern_matching_spec.rb +74 -0
- data/spec/localization_adapter_spec.rb +66 -0
- data/spec/organizer/with_reducer_spec.rb +56 -0
- data/spec/organizer_key_aliases_spec.rb +29 -0
- data/spec/organizer_spec.rb +115 -0
- data/spec/readme_spec.rb +45 -0
- data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
- data/spec/sample/calculates_tax_spec.rb +30 -0
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +55 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
- data/spec/sample/tax/calculates_order_tax_action.rb +10 -0
- data/spec/sample/tax/calculates_tax.rb +11 -0
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +28 -0
- data/spec/sample/tax/provides_free_shipping_action.rb +11 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support.rb +1 -0
- data/spec/test_doubles.rb +656 -0
- data/spec/testing/context_factory/iterate_spec.rb +39 -0
- data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
- data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
- data/spec/testing/context_factory/with_callback_spec.rb +38 -0
- data/spec/testing/context_factory_spec.rb +76 -0
- data/switchyard.gemspec +35 -0
- metadata +351 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7.0.0
|
data/audit/bench.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Micro-benchmark dei costi nascosti individuati nell'audit
|
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
3
|
+
require 'switchyard'
|
|
4
|
+
require 'benchmark/ips'
|
|
5
|
+
|
|
6
|
+
include Switchyard::Prelude::Result
|
|
7
|
+
include Switchyard::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?(Switchyard::Option::Some) ? some.value : 0 }
|
|
19
|
+
x.report("case/in nativo Ruby") do
|
|
20
|
+
case some
|
|
21
|
+
in Switchyard::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?(Switchyard::Result::Success) && other.is_a?(Switchyard::Result::Success)
|
|
34
|
+
Switchyard::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?(Switchyard::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 = Switchyard::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
|
+
Switchyard::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 = Switchyard::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 Switchyard::Action
|
|
86
|
+
expects :number
|
|
87
|
+
executed { |ctx| ctx[:number] = ctx[:number] + 1 }
|
|
88
|
+
end
|
|
89
|
+
class BenchOrg
|
|
90
|
+
extend Switchyard::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
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Verifica dei finding dell'audit — eseguito contro lib/ reale
|
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
3
|
+
require 'switchyard'
|
|
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 Switchyard::Action
|
|
16
|
+
expects :number
|
|
17
|
+
promises :number
|
|
18
|
+
executed { |ctx| ctx.number = ctx.number + 1 }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Org1
|
|
22
|
+
extend Switchyard::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 = Switchyard::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 = Switchyard::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 = Switchyard::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 Switchyard::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?(Switchyard::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 = Switchyard::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 = Switchyard::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 Switchyard::Action
|
|
104
|
+
executed { |ctx| ctx.skip_remaining!("basta") if ctx[:counter] == 2 }
|
|
105
|
+
end
|
|
106
|
+
class CollectAction
|
|
107
|
+
extend Switchyard::Action
|
|
108
|
+
executed { |ctx| (ctx[:seen] ||= []) << ctx[:counter] }
|
|
109
|
+
end
|
|
110
|
+
class OrgIter
|
|
111
|
+
extend Switchyard::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 = Switchyard::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 = Switchyard::Context.make(:a => 1)
|
|
133
|
+
sel = ctx.select { |_k, _v| true }
|
|
134
|
+
[!sel.is_a?(Switchyard::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 = Switchyard::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 Switchyard::Action
|
|
146
|
+
executed { |ctx| (ctx[:trace] ||= []) << :a }
|
|
147
|
+
rolled_back { |ctx| (ctx[:rb] ||= []) << :a }
|
|
148
|
+
end
|
|
149
|
+
class RollB
|
|
150
|
+
extend Switchyard::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 Switchyard::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
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchyard
|
|
4
|
+
module Action
|
|
5
|
+
def self.extended(base_class)
|
|
6
|
+
base_class.extend Macros
|
|
7
|
+
base_class.extend Switchyard::Prelude::Result
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.included(base_class)
|
|
11
|
+
Switchyard::Deprecations.warn(
|
|
12
|
+
"Including Switchyard::Action is deprecated; " \
|
|
13
|
+
"use `extend Switchyard::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? Switchyard::Context
|
|
81
|
+
|
|
82
|
+
Switchyard::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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchyard
|
|
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
|
+
Switchyard::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
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchyard
|
|
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
|