Class: SagaForge::Definition
- Inherits:
-
Object
- Object
- SagaForge::Definition
- Defined in:
- lib/saga_forge/definition.rb
Overview
Immutable boot-compiled metadata for one saga class: the chain, the event→state stall table, handler registry, compensation catalog.
Defined Under Namespace
Classes: Handler
Constant Summary collapse
- START =
:__start__
Instance Attribute Summary collapse
-
#compensations ⇒ Object
readonly
Returns the value of attribute compensations.
-
#handlers_by_event ⇒ Object
readonly
Returns the value of attribute handlers_by_event.
-
#klass ⇒ Object
readonly
Returns the value of attribute klass.
-
#start_event ⇒ Object
readonly
Returns the value of attribute start_event.
-
#states ⇒ Object
readonly
Returns the value of attribute states.
-
#terminal_states ⇒ Object
readonly
Returns the value of attribute terminal_states.
Class Method Summary collapse
Instance Method Summary collapse
- #correlate(payload, event_name) ⇒ Object
- #declared?(state) ⇒ Boolean
- #events ⇒ Object
- #events_for_state(state) ⇒ Object
- #handler_for(event) ⇒ Object
-
#initialize(klass) ⇒ Definition
constructor
A new instance of Definition.
-
#jump_targets ⇒ Object
Best-effort literal scan for
transition_to :symin handler blocks (jumps are opaque Ruby; unresolvable ones are simply not drawn). -
#retry_policy_for(handler) ⇒ Object
handler override → class default → step_default.
- #state_for_event(event) ⇒ Object
- #successor_of(state) ⇒ Object
- #terminal?(state) ⇒ Boolean
-
#to_graph ⇒ Object
Structured graph (chain + jump), the sibling of to_mermaid.
- #to_mermaid ⇒ Object
Constructor Details
#initialize(klass) ⇒ Definition
Returns a new instance of Definition.
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 |
# File 'lib/saga_forge/definition.rb', line 13 def initialize(klass) @klass = klass @handlers_by_event = {} @compensations = {} @terminal_states = [] during_states = [] start_decls = [] klass.declarations.each do |d| case d[:kind] when :start start_decls << d register_handler(START, d) when :during during_states << d[:state] unless during_states.include?(d[:state]) register_handler(d[:state], d) when :finish @terminal_states << d[:state] unless @terminal_states.include?(d[:state]) when :compensation @compensations[d[:name]] = d[:block] end end validate_shape!(start_decls) @start_event = start_decls.first[:event] @states = during_states + @terminal_states @successors = build_successors(during_states) validate_compensations! validate_timeouts! deep_freeze! end |
Instance Attribute Details
#compensations ⇒ Object (readonly)
Returns the value of attribute compensations.
9 10 11 |
# File 'lib/saga_forge/definition.rb', line 9 def compensations @compensations end |
#handlers_by_event ⇒ Object (readonly)
Returns the value of attribute handlers_by_event.
9 10 11 |
# File 'lib/saga_forge/definition.rb', line 9 def handlers_by_event @handlers_by_event end |
#klass ⇒ Object (readonly)
Returns the value of attribute klass.
9 10 11 |
# File 'lib/saga_forge/definition.rb', line 9 def klass @klass end |
#start_event ⇒ Object (readonly)
Returns the value of attribute start_event.
9 10 11 |
# File 'lib/saga_forge/definition.rb', line 9 def start_event @start_event end |
#states ⇒ Object (readonly)
Returns the value of attribute states.
9 10 11 |
# File 'lib/saga_forge/definition.rb', line 9 def states @states end |
#terminal_states ⇒ Object (readonly)
Returns the value of attribute terminal_states.
9 10 11 |
# File 'lib/saga_forge/definition.rb', line 9 def terminal_states @terminal_states end |
Class Method Details
.compile(klass) ⇒ Object
11 |
# File 'lib/saga_forge/definition.rb', line 11 def self.compile(klass) = new(klass).freeze |
Instance Method Details
#correlate(payload, event_name) ⇒ Object
65 66 67 68 69 70 71 72 |
# File 'lib/saga_forge/definition.rb', line 65 def correlate(payload, event_name) correlator = klass.correlator value = (correlator.arity == 1) ? correlator.call(payload) : correlator.call(payload, event_name) if value.nil? raise MissingCorrelationError, "#{klass} registered #{event_name.inspect} but correlate_by returned nil" end value.to_s end |
#declared?(state) ⇒ Boolean
60 61 62 63 |
# File 'lib/saga_forge/definition.rb', line 60 def declared?(state) s = state.to_sym @states.include?(s) || terminal?(s) end |
#events ⇒ Object
49 |
# File 'lib/saga_forge/definition.rb', line 49 def events = @handlers_by_event.keys |
#events_for_state(state) ⇒ Object
51 |
# File 'lib/saga_forge/definition.rb', line 51 def events_for_state(state) = @handlers_by_event.values.select { |h| h.state == state.to_sym }.map(&:event) |
#handler_for(event) ⇒ Object
45 |
# File 'lib/saga_forge/definition.rb', line 45 def handler_for(event) = @handlers_by_event[event.to_sym] |
#jump_targets ⇒ Object
Best-effort literal scan for transition_to :sym in handler blocks
(jumps are opaque Ruby; unresolvable ones are simply not drawn). Each
match is attributed to a handler only if the match's line falls within
that handler's EXACT block extent (via RubyVM::InstructionSequence's
code_location), so two sagas sharing one file never cross-attribute a
jump. Anything we can't precisely locate is simply not drawn — a wrong
edge is worse than a missing one.
109 110 111 112 113 114 115 116 |
# File 'lib/saga_forge/definition.rb', line 109 def jump_targets scan_handlers(/transition_to[\s(]+:(\w+)/).filter_map do |(state, captures)| target = captures.first next unless declared?(target) from = (state == START) ? "[*]" : state [from, target.to_sym] end.uniq end |
#retry_policy_for(handler) ⇒ Object
handler override → class default → step_default. (Compensation blocks use RetryPolicy.compensation_default — see CompensationRunner, Task 8.)
76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/saga_forge/definition.rb', line 76 def retry_policy_for(handler) override = handler.retry_policy policy = case override when nil then nil when Hash then RetryPolicy.new(**override) when Array then CompositeRetryPolicy.new(override) else override end policy || klass.default_retry_policy || RetryPolicy.step_default end |
#state_for_event(event) ⇒ Object
47 |
# File 'lib/saga_forge/definition.rb', line 47 def state_for_event(event) = handler_for(event)&.state |
#successor_of(state) ⇒ Object
53 |
# File 'lib/saga_forge/definition.rb', line 53 def successor_of(state) = @successors.fetch(state.to_sym) |
#terminal?(state) ⇒ Boolean
55 56 57 58 |
# File 'lib/saga_forge/definition.rb', line 55 def terminal?(state) s = state.to_sym @terminal_states.include?(s) || %i[compensated cancelled].include?(s) end |
#to_graph ⇒ Object
Structured graph (chain + jump), the sibling of to_mermaid.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/saga_forge/definition.rb', line 119 def to_graph nodes = [SagaForge::Dashboard::Node.new(id: START.to_s, label: "start", kind: :start)] (@states - @terminal_states).each do |s| nodes << SagaForge::Dashboard::Node.new(id: s.to_s, label: s.to_s, kind: :state) end # Only @terminal_states.first is on the chain below (build_successors # picks it as the chain's sink too). Additional terminals are only # wired in when a handler literally `transition_to`s them (jump_targets, # below) — the normal multi-terminal pattern. A terminal reached only # by a computed/conditional transition, or not reached at all, has no # edge and renders as an isolated node: that's deliberate, not a bug — # it's the same best-effort honesty jump_targets already carries (a # wrong edge is worse than a missing one). @terminal_states.each do |s| nodes << SagaForge::Dashboard::Node.new(id: s.to_s, label: s.to_s, kind: :terminal) end edges = [] chain = [START] + (@states - @terminal_states) + [@terminal_states.first] chain.each_cons(2) do |from, to| label = (from == START) ? @start_event.to_s : events_for_state(from).join(" / ") edges << SagaForge::Dashboard::Edge.new(from: from.to_s, to: to.to_s, kind: :chain, label: label) end jump_targets.each do |(from, to)| from_id = (from == "[*]") ? START.to_s : from.to_s edges << SagaForge::Dashboard::Edge.new(from: from_id, to: to.to_s, kind: :jump, label: "jump") end nodes.each(&:freeze) edges.each(&:freeze) SagaForge::Dashboard::Graph.new(nodes.freeze, edges.freeze).freeze end |
#to_mermaid ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/saga_forge/definition.rb', line 88 def to_mermaid lines = ["stateDiagram-v2"] chain = [START] + @states.reject { |s| @terminal_states.include?(s) } + [@terminal_states.first] chain.each_cons(2) do |from, to| events_from = (from == START) ? [@start_event] : events_for_state(from) label = events_from.join(" / ") from_name = (from == START) ? "[*]" : from lines << " #{from_name} --> #{to}: #{label}" end @terminal_states.each { |t| lines << " #{t} --> [*]" } jump_targets.each { |(from, to)| lines << " #{from} --> #{to}: jump" } lines.join("\n") end |