Class: CircuitBreaker::Token

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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 (timestamp_fields = self.class.state_timestamps[to.to_sym])
      timestamp_fields.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_atObject (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

#historyObject

Returns the value of attribute history.



19
20
21
# File 'lib/circuit_breaker/token.rb', line 19

def history
  @history
end

#idObject (readonly)

Returns the value of attribute id.



18
19
20
# File 'lib/circuit_breaker/token.rb', line 18

def id
  @id
end

#stateObject

Returns the value of attribute state.



19
20
21
# File 'lib/circuit_breaker/token.rb', line 19

def state
  @state
end

#updated_atObject (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_hooksObject



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, **options)
  # 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 options[:allowed]
        options[:allowed].include?(value)
      else
        true
      end
    end
  end
end

.attribute_validationsObject



31
32
33
# File 'lib/circuit_breaker/token.rb', line 31

def attribute_validations
  @attribute_validations ||= {}
end

.attributesObject



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_hooksObject



23
24
25
# File 'lib/circuit_breaker/token.rb', line 23

def before_transition_hooks
  @before_transition_hooks ||= []
end

.default_state_messageObject

Default state message if none specified



132
133
134
# File 'lib/circuit_breaker/token.rb', line 132

def default_state_message
  @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 default_state_message=(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 timestamps
    track_timestamp(*Array(timestamps), on_state: state)
  end

  # Handle message
  if block_given?
    state_message(for_state: state, &block)
  elsif message
    state_message(for_state: state) { |token| message }
  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 state_message(for_state:, &block)
  state_messages[for_state] = block
end

.state_messagesObject



127
128
129
# File 'lib/circuit_breaker/token.rb', line 127

def state_messages
  @state_messages ||= {}
end

.state_timestampsObject



123
124
125
# File 'lib/circuit_breaker/token.rb', line 123

def state_timestamps
  @state_timestamps ||= {}
end

.state_transitionsObject



39
40
41
# File 'lib/circuit_breaker/token.rb', line 39

def state_transitions
  @state_transitions ||= {}
end

.state_validationsObject



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 track_timestamp(*fields, on_state: nil, on_states: nil)
  states = on_states || [on_state]
  states.compact.each do |state|
    state_timestamps[state] ||= []
    state_timestamps[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_rulesObject



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

#inspectObject

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.message}"
          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.message
    }, 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