Module: StateMachines::Integrations::ActiveRecord::MachineMethods
- Included in:
- StateMachines::Integrations::ActiveRecord
- Defined in:
- lib/state_machines/integrations/active_record.rb
Overview
Machine-specific methods for enum integration
Instance Attribute Summary collapse
-
#enum_integration ⇒ Object
Enum integration metadata storage.
Instance Method Summary collapse
-
#after_initialize ⇒ Object
Hook called after machine initialization.
-
#after_transition(*args, **options, &block) ⇒ Object
Creates a callback that will be invoked after a transition is performed, so long as the given requirements match the transition.
-
#check_conflicting_attribute_default ⇒ Object
Warns at most once when the column default conflicts with the machine’s initial state.
-
#detect_enum_integration ⇒ Object
Check if enum integration should be enabled for this machine.
-
#enum_integrated? ⇒ Boolean
Check if this machine has enum integration enabled.
-
#enum_mapping ⇒ Object
Get the enum mapping for this attribute.
-
#initialize_enum_integration ⇒ Object
Initialize enum integration if enum is detected.
- #integer_type_registered? ⇒ Boolean
-
#original_enum_methods ⇒ Object
Get list of original enum methods that were preserved.
-
#owner_class=(klass) ⇒ Class
Hook called when a machine is assigned to a class, including when an inherited machine is cloned for an STI subclass.
-
#read(object, attr_sym, ivar = false) ⇒ Object
Machine internals (state matching, validations) call read() to get the current state value and compare it against state.value.
-
#register_integer_type? ⇒ Boolean
Returns true when this machine should use the custom integer attribute type to convert between Ruby state names and integer database values.
-
#state ⇒ Object
Override state method to trigger method generation after states are defined.
-
#state_machine_methods ⇒ Object
Get list of state machine methods that were generated.
Instance Attribute Details
#enum_integration ⇒ Object
Enum integration metadata storage
386 387 388 |
# File 'lib/state_machines/integrations/active_record.rb', line 386 def enum_integration @enum_integration end |
Instance Method Details
#after_initialize ⇒ Object
Hook called after machine initialization
389 390 391 392 393 394 |
# File 'lib/state_machines/integrations/active_record.rb', line 389 def after_initialize super initialize_enum_integration register_integer_type if register_integer_type? end |
#after_transition(*args, **options, &block) ⇒ Object
Creates a callback that will be invoked after a transition is performed, so long as the given requirements match the transition.
In addition to the configuration options supported by the core after_transition (see StateMachines::Machine#after_transition), the ActiveRecord integration supports:
-
:after_commit- Defer execution of the callback until the database transaction wrapping the transition has been committed. When no transaction is open at that point, the callback runs immediately. When the transaction (or an outer transaction wrapping it) is rolled back, the callback is discarded.
This is the safe place to enqueue background jobs that reference the record (e.g. via GlobalID), since a regular after_transition runs inside the transaction, before the record’s changes are visible to other connections:
class Vehicle < ApplicationRecord
state_machine do
after_transition on: :ignite, after_commit: true do |vehicle|
EngineWarmupJob.perform_later(vehicle)
end
...
end
end
Note that a deferred callback cannot halt the callback chain or affect the result of the transition: by the time it runs, the transition has already been committed. For the same reason, an exception raised by a deferred callback is not propagated (doing so would revert the record’s in-memory state even though the database was already updated); it is reported to ActiveSupport.error_reporter (Rails.error) instead. Conditions (:if/:unless) and state requirements are evaluated when the transition is performed, not at commit time.
Like ActiveRecord’s own after_commit, a surrounding transaction(joinable: false) wrapper is transparent: the callback fires at the inner commit. This is what makes it fire under transactional test fixtures.
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 |
# File 'lib/state_machines/integrations/active_record.rb', line 549 def after_transition(*args, **, &block) # The flag may hide in a legacy trailing positional options hash = args.last.is_a?(Hash) ? args.pop.dup : {} = .merge() # Only a boolean is the flag — a non-boolean value is the implicit # state-requirement form for a state named :after_commit flag = [:after_commit] return super unless flag == true || flag == false .delete(:after_commit) return super unless flag # Method handling goes to a real Callback (reusing core's binding, # arity and :do semantics); branch matching stays on the wrapper so # conditions are evaluated at transition time parsed = parse_callback_arguments(args, ) = parsed.slice(:do, :bind_to_object) [:terminator] = callback_terminator = parsed.except(:do, :bind_to_object, :terminator) deferred = Callback.new(:after, , &block) super(**, bind_to_object: false) do |object, transition| object.class.current_transaction.after_commit do # The transition's catch(:halt) is gone at commit time catch(:halt) { deferred.call(object, {}, transition) } rescue StandardError => e # Raising would roll back in-memory state already committed; report instead ActiveSupport.error_reporter.report(e, handled: false, source: 'state_machines-activerecord') end end end |
#check_conflicting_attribute_default ⇒ Object
Warns at most once when the column default conflicts with the machine’s initial state. The conflict is re-evaluated as states are defined, so remember when the warning has been issued.
456 457 458 459 460 461 462 463 464 465 466 467 |
# File 'lib/state_machines/integrations/active_record.rb', line 456 def check_conflicting_attribute_default return if @attribute_default_conflict_warned initial_state = states.detect(&:initial) conflict = !owner_class_attribute_default.nil? && ( dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state) ) return unless conflict @attribute_default_conflict_warned = true super end |
#detect_enum_integration ⇒ Object
Check if enum integration should be enabled for this machine
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/state_machines/integrations/active_record.rb', line 413 def detect_enum_integration return nil unless owner_class.defined_enums.key?(attribute.to_s) # For now, auto-detect enum and enable basic integration # Later we can add explicit configuration options { enabled: true, prefix: true, suffix: false, scopes: true, enum_values: owner_class.defined_enums[attribute.to_s] || {}, original_enum_methods: detect_existing_enum_methods, state_machine_methods: [] } end |
#enum_integrated? ⇒ Boolean
Check if this machine has enum integration enabled
479 480 481 |
# File 'lib/state_machines/integrations/active_record.rb', line 479 def enum_integrated? enum_integration && enum_integration[:enabled] end |
#enum_mapping ⇒ Object
Get the enum mapping for this attribute
484 485 486 487 488 |
# File 'lib/state_machines/integrations/active_record.rb', line 484 def enum_mapping return {} unless enum_integrated? enum_integration[:enum_values] || {} end |
#initialize_enum_integration ⇒ Object
Initialize enum integration if enum is detected
430 431 432 433 434 435 436 |
# File 'lib/state_machines/integrations/active_record.rb', line 430 def initialize_enum_integration detected_config = detect_enum_integration return unless detected_config # Store enum integration metadata self.enum_integration = detected_config end |
#integer_type_registered? ⇒ Boolean
504 505 506 |
# File 'lib/state_machines/integrations/active_record.rb', line 504 def integer_type_registered? !!@integer_type_registered end |
#original_enum_methods ⇒ Object
Get list of original enum methods that were preserved
491 492 493 494 495 |
# File 'lib/state_machines/integrations/active_record.rb', line 491 def original_enum_methods return [] unless enum_integrated? enum_integration[:original_enum_methods] || [] end |
#owner_class=(klass) ⇒ Class
Hook called when a machine is assigned to a class, including when an inherited machine is cloned for an STI subclass. The cloned machine carries the parent’s @integer_type_registered flag while the subclass still inherits the parent’s attribute type, which references the parent machine’s state collection. Re-register so the subclass type sees this machine’s (cloned) states, picking up subclass-added states as they are defined.
406 407 408 409 410 |
# File 'lib/state_machines/integrations/active_record.rb', line 406 def owner_class=(klass) super.tap do register_integer_type if integer_type_registered? end end |
#read(object, attr_sym, ivar = false) ⇒ Object
Machine internals (state matching, validations) call read() to get the current state value and compare it against state.value. The custom integer type already returns the canonical value when state values are uniform: raw integers when every named state has an explicit integer value (passthrough), name strings when none do (state.value is the name). Only machines mixing explicit and auto-indexed values need an override, because the type returns name strings while the explicit states match on integers; map the stored value back to the matched state’s canonical state.value.
597 598 599 600 601 602 603 604 605 606 607 608 609 610 |
# File 'lib/state_machines/integrations/active_record.rb', line 597 def read(object, attr_sym, ivar = false) return super unless integer_type_registered? && attr_sym == :state return super unless mixed_integer_state_values? raw = object.read_attribute_before_type_cast(attribute.to_s) if raw.is_a?(::String) || raw.is_a?(::Symbol) matched = states.detect { |s| s.name && s.name.to_s == raw.to_s } return matched.value if matched end name = owner_class.type_for_attribute(attribute.to_s).deserialize(raw) matched = states.detect { |s| s.name && s.name.to_s == name.to_s } matched ? matched.value : raw end |
#register_integer_type? ⇒ Boolean
Returns true when this machine should use the custom integer attribute type to convert between Ruby state names and integer database values. This only applies to non-enum integer columns when automatic conversion is enabled.
472 473 474 475 476 |
# File 'lib/state_machines/integrations/active_record.rb', line 472 def register_integer_type? StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes && integer_column? && !enum_integrated? end |
#state ⇒ Object
Override state method to trigger method generation after states are defined
439 440 441 442 443 444 445 446 447 448 449 450 451 |
# File 'lib/state_machines/integrations/active_record.rb', line 439 def state(*, &) result = super # Generate methods after each state addition if enum integration is enabled generate_state_machine_methods if enum_integrated? # States defined after initialization (e.g. adding an explicit value # to the initial state) can change how the column default maps to # states, so re-evaluate the conflicting-default warning recheck_conflicting_attribute_default if integer_type_registered? result end |
#state_machine_methods ⇒ Object
Get list of state machine methods that were generated
498 499 500 501 502 |
# File 'lib/state_machines/integrations/active_record.rb', line 498 def state_machine_methods return [] unless enum_integrated? enum_integration[:state_machine_methods] || [] end |