Class: CircuitBreaker::Token
- Inherits:
-
Object
- Object
- CircuitBreaker::Token
- Includes:
- History
- Defined in:
- lib/circuit_breaker/token.rb
Overview
Base class for all workflow tokens
Defined Under Namespace
Classes: StateConfigDSL, StateError, TransitionError, ValidationError
Instance Attribute Summary collapse
-
#created_at ⇒ Object
readonly
Returns the value of attribute created_at.
-
#history ⇒ Object
Returns the value of attribute history.
-
#id ⇒ Object
readonly
Returns the value of attribute id.
-
#state ⇒ Object
Returns the value of attribute state.
-
#updated_at ⇒ Object
readonly
Returns the value of attribute updated_at.
Class Method Summary collapse
- .after_transition(&block) ⇒ Object
- .after_transition_hooks ⇒ Object
- .attribute(name, type = nil, **options) ⇒ Object
- .attribute_validations ⇒ Object
- .attributes ⇒ Object
-
.before_transition(&block) ⇒ Object
DSL methods for defining hooks and validations.
- .before_transition_hooks ⇒ Object
-
.default_state_message ⇒ Object
Default state message if none specified.
- .default_state_message=(block) ⇒ Object
-
.state_config(state, timestamps: nil, message: nil, &block) ⇒ Object
Combined state configuration.
-
.state_configs(&block) ⇒ Object
Multiple state configuration.
- .state_message(for_state:, &block) ⇒ Object
- .state_messages ⇒ Object
- .state_timestamps ⇒ Object
- .state_transitions ⇒ Object
- .state_validations ⇒ Object
-
.states(*state_list) ⇒ Object
Enhanced DSL methods.
- .track_timestamp(*fields, on_state: nil, on_states: nil) ⇒ Object
- .transition_rule(from:, to:, &block) ⇒ Object
- .transition_rules ⇒ Object
- .transitions_from(from, to:) ⇒ Object
- .validate_attribute(name, &block) ⇒ Object
- .validate_state(state, &block) ⇒ Object
-
.visualize(format = :mermaid) ⇒ Object
Visualization methods.
Instance Method Summary collapse
-
#initialize(attributes = {}) ⇒ Token
constructor
A new instance of Token.
-
#inspect ⇒ Object
Show all state in inspect for debugging.
- #notify(event, data = {}) ⇒ Object
-
#on(event, async: false, &block) ⇒ Object
Event handling.
-
#pretty_print(include_private = false) ⇒ Object
Pretty print the object's state.
- #record_transition(transition_name, old_state, new_state) ⇒ Object
-
#to_h(include_private = false) ⇒ Object
Serialization methods.
- #to_json(include_private = false) ⇒ Object
- #to_xml(include_private = false) ⇒ Object
- #to_yaml(include_private = false) ⇒ Object
- #trigger(event, **data) ⇒ Object
- #trigger_async(event, **data) ⇒ Object
- #update_state(new_state, actor_id: nil) ⇒ Object
Methods included from History
#export_history, #history_by_actor, #history_by_type, #history_since, included, #record_event
Constructor Details
#initialize(attributes = {}) ⇒ Token
Returns a new instance of Token.
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/circuit_breaker/token.rb', line 201 def initialize(attributes = {}) @id = SecureRandom.uuid @state = self.class::VALID_STATES.first # Initialize all attributes to nil self.class.attributes.each do |attr| instance_variable_set("@#{attr}", nil) end # Set provided attributes attributes.each do |key, value| send("#{key}=", value) if respond_to?("#{key}=") end # Add default transition hook for timestamps and history self.class.before_transition do |from, to| # Set timestamps for the target state if ( = self.class.[to.to_sym]) .each do |field| send("#{field}=", Time.now) end end # Record the transition in history with details record_event( :state_transition, { from: from, to: to, timestamp: Time.now, details: state_change_details(from, to) } ) end @created_at = Time.now @updated_at = @created_at @event_handlers = Hash.new { |h, k| h[k] = [] } @async_handlers = Hash.new { |h, k| h[k] = [] } @observers = Hash.new { |h, k| h[k] = [] } @async_observers = Hash.new { |h, k| h[k] = [] } @history = [] end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method_name, *args) ⇒ Object (protected)
451 452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'lib/circuit_breaker/token.rb', line 451 def method_missing(method_name, *args) # Check if it's a setter method (ends with =) if method_name.to_s.end_with?('=') attr_name = method_name.to_s.chomp('=') if self.class.attributes.include?(attr_name.to_sym) self.class.send(:attr_accessor, attr_name.to_sym) send(method_name, *args) else super end else super end end |
Instance Attribute Details
#created_at ⇒ Object (readonly)
Returns the value of attribute created_at.
18 19 20 |
# File 'lib/circuit_breaker/token.rb', line 18 def created_at @created_at end |
#history ⇒ Object
Returns the value of attribute history.
19 20 21 |
# File 'lib/circuit_breaker/token.rb', line 19 def history @history end |
#id ⇒ Object (readonly)
Returns the value of attribute id.
18 19 20 |
# File 'lib/circuit_breaker/token.rb', line 18 def id @id end |
#state ⇒ Object
Returns the value of attribute state.
19 20 21 |
# File 'lib/circuit_breaker/token.rb', line 19 def state @state end |
#updated_at ⇒ Object (readonly)
Returns the value of attribute updated_at.
18 19 20 |
# File 'lib/circuit_breaker/token.rb', line 18 def updated_at @updated_at end |
Class Method Details
.after_transition(&block) ⇒ Object
52 53 54 |
# File 'lib/circuit_breaker/token.rb', line 52 def after_transition(&block) after_transition_hooks << block end |
.after_transition_hooks ⇒ Object
27 28 29 |
# File 'lib/circuit_breaker/token.rb', line 27 def after_transition_hooks @after_transition_hooks ||= [] end |
.attribute(name, type = nil, **options) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/circuit_breaker/token.rb', line 88 def attribute(name, type = nil, **) # Add to list of attributes attributes << name # Define the attribute accessor attr_accessor name # Define the validator if type is specified if type validate_attribute name do |value| next true if value.nil? next false unless value.is_a?(type) if [:allowed] [:allowed].include?(value) else true end end end end |
.attribute_validations ⇒ Object
31 32 33 |
# File 'lib/circuit_breaker/token.rb', line 31 def attribute_validations @attribute_validations ||= {} end |
.attributes ⇒ Object
110 111 112 |
# File 'lib/circuit_breaker/token.rb', line 110 def attributes @attributes ||= [] end |
.before_transition(&block) ⇒ Object
DSL methods for defining hooks and validations
48 49 50 |
# File 'lib/circuit_breaker/token.rb', line 48 def before_transition(&block) before_transition_hooks << block end |
.before_transition_hooks ⇒ Object
23 24 25 |
# File 'lib/circuit_breaker/token.rb', line 23 def before_transition_hooks @before_transition_hooks ||= [] end |
.default_state_message ⇒ Object
Default state message if none specified
132 133 134 |
# File 'lib/circuit_breaker/token.rb', line 132 def @default_state_message ||= ->(token, from, to) { "State changed from #{from} to #{to}" } end |
.default_state_message=(block) ⇒ Object
136 137 138 |
# File 'lib/circuit_breaker/token.rb', line 136 def (block) @default_state_message = block end |
.state_config(state, timestamps: nil, message: nil, &block) ⇒ Object
Combined state configuration
145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/circuit_breaker/token.rb', line 145 def state_config(state, timestamps: nil, message: nil, &block) # Handle timestamps if (*Array(), on_state: state) end # Handle message if block_given? (for_state: state, &block) elsif (for_state: state) { |token| } end end |
.state_configs(&block) ⇒ Object
Multiple state configuration
160 161 162 163 |
# File 'lib/circuit_breaker/token.rb', line 160 def state_configs(&block) config_dsl = StateConfigDSL.new(self) config_dsl.instance_eval(&block) end |
.state_message(for_state:, &block) ⇒ Object
140 141 142 |
# File 'lib/circuit_breaker/token.rb', line 140 def (for_state:, &block) [for_state] = block end |
.state_messages ⇒ Object
127 128 129 |
# File 'lib/circuit_breaker/token.rb', line 127 def @state_messages ||= {} end |
.state_timestamps ⇒ Object
123 124 125 |
# File 'lib/circuit_breaker/token.rb', line 123 def @state_timestamps ||= {} end |
.state_transitions ⇒ Object
39 40 41 |
# File 'lib/circuit_breaker/token.rb', line 39 def state_transitions @state_transitions ||= {} end |
.state_validations ⇒ Object
35 36 37 |
# File 'lib/circuit_breaker/token.rb', line 35 def state_validations @state_validations ||= {} end |
.states(*state_list) ⇒ Object
Enhanced DSL methods
69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/circuit_breaker/token.rb', line 69 def states(*state_list) state_list.each do |state| state_transitions[state] ||= [] end # Define predicate methods for states state_list.each do |state| define_method("#{state}?") do @state == state end end const_set(:VALID_STATES, state_list.freeze) end |
.track_timestamp(*fields, on_state: nil, on_states: nil) ⇒ Object
114 115 116 117 118 119 120 121 |
# File 'lib/circuit_breaker/token.rb', line 114 def (*fields, on_state: nil, on_states: nil) states = on_states || [on_state] states.compact.each do |state| [state] ||= [] [state].concat(fields) end attr_accessor(*fields) end |
.transition_rule(from:, to:, &block) ⇒ Object
64 65 66 |
# File 'lib/circuit_breaker/token.rb', line 64 def transition_rule(from:, to:, &block) transition_rules[[from, to]] = block end |
.transition_rules ⇒ Object
43 44 45 |
# File 'lib/circuit_breaker/token.rb', line 43 def transition_rules @transition_rules ||= {} end |
.transitions_from(from, to:) ⇒ Object
60 61 62 |
# File 'lib/circuit_breaker/token.rb', line 60 def transitions_from(from, to:) state_transitions[from] = Array(to) end |
.validate_attribute(name, &block) ⇒ Object
84 85 86 |
# File 'lib/circuit_breaker/token.rb', line 84 def validate_attribute(name, &block) attribute_validations[name] = block end |
.validate_state(state, &block) ⇒ Object
56 57 58 |
# File 'lib/circuit_breaker/token.rb', line 56 def validate_state(state, &block) state_validations[state] = block end |
.visualize(format = :mermaid) ⇒ Object
Visualization methods
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/circuit_breaker/token.rb', line 183 def visualize(format = :mermaid) case format when :mermaid Visualizer.to_mermaid(self) when :dot Visualizer.to_dot(self) when :plantuml Visualizer.to_plantuml(self) when :html Visualizer.to_html(self) when :markdown Visualizer.to_markdown(self) else raise ArgumentError, "Unsupported format: #{format}" end end |
Instance Method Details
#inspect ⇒ Object
Show all state in inspect for debugging
420 421 422 |
# File 'lib/circuit_breaker/token.rb', line 420 def inspect pretty_print(true) end |
#notify(event, data = {}) ⇒ Object
342 343 344 345 346 347 348 349 350 |
# File 'lib/circuit_breaker/token.rb', line 342 def notify(event, data = {}) @observers[event].each { |observer| observer.call(data) } @async_observers[event].each do |observer| Thread.new do observer.call(data) end end end |
#on(event, async: false, &block) ⇒ Object
Event handling
298 299 300 301 302 303 304 305 |
# File 'lib/circuit_breaker/token.rb', line 298 def on(event, async: false, &handler) if async @async_handlers[event] << handler else @event_handlers[event] << handler end self end |
#pretty_print(include_private = false) ⇒ Object
Pretty print the object's state
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 |
# File 'lib/circuit_breaker/token.rb', line 395 def pretty_print(include_private = false) fields = to_h(include_private) # Find the longest key for padding max_key_length = fields.keys.map(&:to_s).map(&:length).max output = ["#<#{self.class}"] fields.each do |key, value| value_str = case value when nil then "nil" when String then "\"#{value}\"" when Time then "\"#{value.iso8601}\"" else value.to_s end # Pad the key with spaces for alignment padded_key = key.to_s.ljust(max_key_length) output << " #{padded_key}: #{value_str}" end output << ">" output.join("\n") end |
#record_transition(transition_name, old_state, new_state) ⇒ Object
424 425 426 427 428 429 430 431 432 |
# File 'lib/circuit_breaker/token.rb', line 424 def record_transition(transition_name, old_state, new_state) @history << { transition: transition_name, from: old_state, to: new_state, timestamp: Time.now } @updated_at = Time.now end |
#to_h(include_private = false) ⇒ Object
Serialization methods
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/circuit_breaker/token.rb', line 353 def to_h(include_private = false) if include_private # Get all instance variables including private state instance_variables.each_with_object({}) do |var, hash| next if [:@event_handlers, :@async_handlers, :@observers, :@async_observers].include?(var) key = var.to_s.delete_prefix('@').to_sym hash[key] = instance_variable_get(var) end else # Get only publicly accessible attributes self.class.instance_methods(false) .select { |method| method.to_s !~ /[=!?]$/ } .reject { |method| [:inspect, :pretty_print, :to_h, :to_json, :to_yaml, :to_xml].include?(method) } .each_with_object({}) do |method, hash| hash[method] = send(method) end end end |
#to_json(include_private = false) ⇒ Object
372 373 374 |
# File 'lib/circuit_breaker/token.rb', line 372 def to_json(include_private = false) JSON.pretty_generate(to_h(include_private)) end |
#to_xml(include_private = false) ⇒ Object
380 381 382 383 384 385 386 387 388 389 390 391 392 |
# File 'lib/circuit_breaker/token.rb', line 380 def to_xml(include_private = false) require 'nokogiri' builder = Nokogiri::XML::Builder.new do |xml| xml.token(class: self.class.name) { to_h(include_private).each do |key, value| xml.send(key, value) end } end builder.to_xml rescue LoadError raise "Nokogiri is required for XML serialization. Add it to your Gemfile." end |
#to_yaml(include_private = false) ⇒ Object
376 377 378 |
# File 'lib/circuit_breaker/token.rb', line 376 def to_yaml(include_private = false) to_h(include_private).to_yaml end |
#trigger(event, **data) ⇒ Object
307 308 309 |
# File 'lib/circuit_breaker/token.rb', line 307 def trigger(event, **data) @event_handlers[event].each { |handler| handler.call(data) } end |
#trigger_async(event, **data) ⇒ Object
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/circuit_breaker/token.rb', line 311 def trigger_async(event, **data) return if @async_handlers[event].empty? Async do |task| @async_handlers[event].each do |handler| task.async do begin handler.call(data) rescue => e # Log async handler errors and record in history error_msg = "[ERROR] Async handler failed: #{e.}" puts error_msg record_event(:async_handler_error, { event: event, error: error_msg, data: data }) end end end end end |
#update_state(new_state, actor_id: nil) ⇒ Object
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/circuit_breaker/token.rb', line 245 def update_state(new_state, actor_id: nil) old_state = @state begin # Run before transition hooks self.class.before_transition_hooks.each { |hook| instance_exec(old_state, new_state, &hook) } # Run state-specific validations validate_transition(from: old_state, to: new_state) validate_current_state(new_state) # Run custom transition rules if rule = self.class.transition_rules[[old_state, new_state]] result = instance_exec(&rule) raise TransitionError, result if result.is_a?(String) end @state = new_state @updated_at = Time.now # Record the transition in history record_event(:state_transition, { from: old_state, to: new_state, timestamp: @updated_at }, actor_id: actor_id) # Run after transition hooks self.class.after_transition_hooks.each { |hook| instance_exec(old_state, new_state, &hook) } # Trigger state change events trigger(:state_changed, old_state: old_state, new_state: new_state) trigger_async(:state_changed, old_state: old_state, new_state: new_state) notify(:state_changed, old_state: old_state, new_state: new_state) true rescue StandardError => e # Record the failed transition record_event(:transition_failed, { from: old_state, to: new_state, error: e. }, actor_id: actor_id) trigger(:transition_failed, error: e, from: old_state, to: new_state) trigger_async(:transition_failed, error: e, from: old_state, to: new_state) notify(:transition_failed, error: e, from: old_state, to: new_state) raise end end |