Class: Signoff::Definition
- Inherits:
-
Object
- Object
- Signoff::Definition
- Defined in:
- lib/signoff/definition.rb
Overview
Immutable-once-validated description of a model’s workflow: its states, the allowed forward transitions, the reject state, authorization guards and lifecycle callbacks. Built by Signoff::DSL and stored on the model class as signoff_definition.
Constant Summary collapse
- RESERVED_ACTIONS =
Built-in verbs that own their own transition methods on the model; a custom
actionmay not reuse these names. %i[submit approve reject].freeze
Instance Attribute Summary collapse
-
#actions ⇒ Object
readonly
Returns the value of attribute actions.
-
#after_callbacks ⇒ Object
readonly
Returns the value of attribute after_callbacks.
-
#authorizations ⇒ Object
readonly
Returns the value of attribute authorizations.
-
#before_callbacks ⇒ Object
readonly
Returns the value of attribute before_callbacks.
-
#initial_state ⇒ Object
— queries ———————————————————.
-
#reject_state ⇒ Object
Returns the value of attribute reject_state.
-
#state_column ⇒ Object
readonly
Returns the value of attribute state_column.
-
#states ⇒ Object
readonly
Returns the value of attribute states.
-
#transitions ⇒ Object
readonly
Returns the value of attribute transitions.
Instance Method Summary collapse
-
#action_allowed_from(name) ⇒ Object
States a custom action may be invoked from.
- #action_for(name) ⇒ Object
- #action_names ⇒ Object
-
#action_target_states ⇒ Object
The distinct set of states reachable via custom actions.
-
#add_action(name, to:, from: nil) ⇒ Object
Register a named custom action: a guarded “side transition” that moves a record to
toand records an event whoseactionis the name verbatim. - #add_authorization(from, guard) ⇒ Object
-
#add_state(name) ⇒ Object
— builders (called by the DSL) ————————————.
- #add_transition(from, to) ⇒ Object
-
#approval_terminal_states ⇒ Object
Terminal states that represent successful completion: terminal via the forward flow and not reachable only as an off-ramp (reject or a custom action target).
-
#finalized_states ⇒ Object
Every state that ends the workflow: successful terminals, the reject state, and any custom-action target that has no forward transition out (e.g. a
cancelaction’scancelledstate). -
#forward_targets(state) ⇒ Object
Targets reachable from
statevia a declared forward transition. - #guard_for(state) ⇒ Object
-
#initialize(state_column: :approval_state) ⇒ Definition
constructor
A new instance of Definition.
-
#next_state(from, to: nil) ⇒ Object
Resolve the next state when advancing forward from
from. -
#terminal_states ⇒ Object
States with no outgoing forward transition.
-
#validate! ⇒ Object
— validation ——————————————————.
Constructor Details
#initialize(state_column: :approval_state) ⇒ Definition
Returns a new instance of Definition.
17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/signoff/definition.rb', line 17 def initialize(state_column: :approval_state) @states = [] @transitions = {} # from(Symbol) => [to(Symbol), ...] @authorizations = {} # from(Symbol) => callable guard @actions = {} # name(Symbol) => { to: Symbol, from: [Symbol]|nil } @before_callbacks = [] @after_callbacks = [] @initial_state = nil @reject_state = nil @state_column = state_column.to_sym end |
Instance Attribute Details
#actions ⇒ Object (readonly)
Returns the value of attribute actions.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def actions @actions end |
#after_callbacks ⇒ Object (readonly)
Returns the value of attribute after_callbacks.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def after_callbacks @after_callbacks end |
#authorizations ⇒ Object (readonly)
Returns the value of attribute authorizations.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def @authorizations end |
#before_callbacks ⇒ Object (readonly)
Returns the value of attribute before_callbacks.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def before_callbacks @before_callbacks end |
#initial_state ⇒ Object
— queries ———————————————————
77 78 79 |
# File 'lib/signoff/definition.rb', line 77 def initial_state @initial_state || @states.first end |
#reject_state ⇒ Object
Returns the value of attribute reject_state.
73 74 75 |
# File 'lib/signoff/definition.rb', line 73 def reject_state @reject_state end |
#state_column ⇒ Object (readonly)
Returns the value of attribute state_column.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def state_column @state_column end |
#states ⇒ Object (readonly)
Returns the value of attribute states.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def states @states end |
#transitions ⇒ Object (readonly)
Returns the value of attribute transitions.
13 14 15 |
# File 'lib/signoff/definition.rb', line 13 def transitions @transitions end |
Instance Method Details
#action_allowed_from(name) ⇒ Object
States a custom action may be invoked from. An explicit from: wins; otherwise any non-finalized state except the action’s own target.
122 123 124 125 126 127 |
# File 'lib/signoff/definition.rb', line 122 def action_allowed_from(name) spec = @actions.fetch(name.to_sym) return spec[:from] if spec[:from] states - finalized_states - [spec[:to]] end |
#action_for(name) ⇒ Object
116 117 118 |
# File 'lib/signoff/definition.rb', line 116 def action_for(name) @actions[name.to_sym] end |
#action_names ⇒ Object
112 113 114 |
# File 'lib/signoff/definition.rb', line 112 def action_names @actions.keys end |
#action_target_states ⇒ Object
The distinct set of states reachable via custom actions.
108 109 110 |
# File 'lib/signoff/definition.rb', line 108 def action_target_states @actions.values.map { |spec| spec[:to] }.uniq end |
#add_action(name, to:, from: nil) ⇒ Object
Register a named custom action: a guarded “side transition” that moves a record to to and records an event whose action is the name verbatim. Like reject, the target is NOT a forward transition, so it never makes approve! ambiguous. from limits the source states (defaults to every non-finalized state except the target itself).
58 59 60 61 62 63 64 65 66 67 |
# File 'lib/signoff/definition.rb', line 58 def add_action(name, to:, from: nil) name = name.to_sym raise DefinitionError, "duplicate action #{name.inspect}" if @actions.key?(name) raise DefinitionError, "action #{name.inspect} requires a :to state" if to.nil? @actions[name] = { to: to.to_sym, from: from.nil? ? nil : Array(from).map(&:to_sym) } end |
#add_authorization(from, guard) ⇒ Object
49 50 51 |
# File 'lib/signoff/definition.rb', line 49 def (from, guard) @authorizations[from.to_sym] = guard end |
#add_state(name) ⇒ Object
— builders (called by the DSL) ————————————
31 32 33 34 35 36 |
# File 'lib/signoff/definition.rb', line 31 def add_state(name) name = name.to_sym raise DefinitionError, "duplicate state #{name.inspect}" if @states.include?(name) @states << name end |
#add_transition(from, to) ⇒ Object
38 39 40 41 42 43 44 45 46 47 |
# File 'lib/signoff/definition.rb', line 38 def add_transition(from, to) Array(from).each do |source| source = source.to_sym Array(to).each do |target| target = target.to_sym @transitions[source] ||= [] @transitions[source] << target unless @transitions[source].include?(target) end end end |
#approval_terminal_states ⇒ Object
Terminal states that represent successful completion: terminal via the forward flow and not reachable only as an off-ramp (reject or a custom action target). This keeps approved? true for genuine end states only.
94 95 96 |
# File 'lib/signoff/definition.rb', line 94 def approval_terminal_states terminal_states - [reject_state].compact - action_target_states end |
#finalized_states ⇒ Object
Every state that ends the workflow: successful terminals, the reject state, and any custom-action target that has no forward transition out (e.g. a cancel action’s cancelled state). Off-ramp targets that loop back (e.g. changes_requested -> re-submit) stay in flight, not finalized.
102 103 104 105 |
# File 'lib/signoff/definition.rb', line 102 def finalized_states terminal_action_targets = action_target_states.select { |s| forward_targets(s).empty? } (approval_terminal_states + [reject_state].compact + terminal_action_targets).uniq end |
#forward_targets(state) ⇒ Object
Targets reachable from state via a declared forward transition.
82 83 84 |
# File 'lib/signoff/definition.rb', line 82 def forward_targets(state) @transitions[state.to_sym] || [] end |
#guard_for(state) ⇒ Object
129 130 131 |
# File 'lib/signoff/definition.rb', line 129 def guard_for(state) @authorizations[state.to_sym] end |
#next_state(from, to: nil) ⇒ Object
Resolve the next state when advancing forward from from. to may be supplied to disambiguate when several forward transitions exist.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/signoff/definition.rb', line 135 def next_state(from, to: nil) from = from.to_sym targets = forward_targets(from) if to to = to.to_sym return to if targets.include?(to) raise InvalidTransitionError, "no transition declared from #{from.inspect} to #{to.inspect}" end case targets.size when 0 raise InvalidTransitionError, "no forward transition declared from #{from.inspect}" when 1 targets.first else raise InvalidTransitionError, "ambiguous transition from #{from.inspect}; pass to: " \ "(one of #{targets.inspect})" end end |
#terminal_states ⇒ Object
States with no outgoing forward transition.
87 88 89 |
# File 'lib/signoff/definition.rb', line 87 def terminal_states @states.reject { |s| forward_targets(s).any? } end |
#validate! ⇒ Object
— validation ——————————————————
162 163 164 165 166 167 168 169 170 171 |
# File 'lib/signoff/definition.rb', line 162 def validate! raise DefinitionError, "no states have been defined" if @states.empty? validate_initial_state! validate_transitions! validate_reject_state! validate_actions! self end |