StateMachines::Diagram - Extensible Core for State Machine Visualization
An extensible diagram building foundation for the state_machines ecosystem. This gem provides a structured intermediate representation (IR) and adapter framework that enables consistent visualization across multiple output formats.
Quick Start
# 1. Define a state machine using any supported library
require 'state_machines'
class Order
  state_machine :status, initial: :pending do
    state :pending, :processing, :shipped, :cancelled
    
    event :process do
      transition pending: :processing, if: :payment_cleared?
    end
    
    event :ship do
      transition processing: :shipped, action: :send_notification
    end
    
    event :cancel do
      transition [:pending, :processing] => :cancelled
    end
  end
  
  def payment_cleared?
    @payment_cleared ||= false
  end
  
  def send_notification
    puts "Order shipped!"
  end
end
# 2. Generate diagram representation
require 'state_machines-diagram'
# Text format (default)
Order.state_machine(:status).draw
# Output:
# Status: pending → processing [process] (if: payment_cleared?)
# Status: processing → shipped [ship] (action: send_notification)
# Status: pending → cancelled [cancel]
# Status: processing → cancelled [cancel]
# JSON structure  
Order.state_machine(:status).draw(format: :json)
# Output: {"states": [...], "transitions": [...]}
# With specific rendering gem
require 'state_machines-mermaid'
Order.state_machine(:status).draw(format: :mermaid)
# Output:
# stateDiagram-v2
#   pending --> processing : process (if: payment_cleared?)
#   processing --> shipped : ship (action: send_notification)
#   pending --> cancelled : cancel
#   processing --> cancelled : cancelArchitecture
This gem implements a structured data transformation pipeline:
StateMachine → Builder → Diagrams::StateDiagram → Renderer → Output
Core Components
1. Intermediate Representation (IR)
The Diagrams::StateDiagram IR uses immutable Dry::Struct objects to represent state machine structure:
# State representation
Diagrams::Elements::State = Dry::Struct do
  attribute :id, Types::Strict::String
  attribute :state_type, Types::StateType  # :initial, :final, :normal
  attribute :name, Types::Strict::String.optional
end
# Transition representation with semantic information
Diagrams::Elements::Transition = Dry::Struct do
  attribute :source_state_id, Types::Strict::String
  attribute :target_state_id, Types::Strict::String
  attribute :label, Types::Strict::String.optional
  attribute :guard, Types::Strict::String.optional    # if: condition
  attribute :action, Types::Strict::String.optional   # callback method
end
# Complete diagram structure
Diagrams::StateDiagram = Dry::Struct do
  attribute :states, Types::Array.of(Diagrams::Elements::State)
  attribute :transitions, Types::Array.of(Diagrams::Elements::Transition)
  attribute :title, Types::Strict::String.optional
end2. Builder Contract
The StateMachines::Diagram::Builder extracts semantic information from state machines:
# Core building method
diagram = StateMachines::Diagram::Builder.build_state_diagram(machine, options)
# Builder handles:
# - State extraction with type detection
# - Transition mapping with guard conditions  
# - Callback extraction (before/after/around)
# - Event-to-transition resolution
# - Filtering (state_filter, event_filter)3. Renderer Interface
Renderers implement a standardized contract:
module StateMachines::Diagram::Renderer
  # Required method for all renderers
  def self.draw_machine(machine, io: $stdout, **options)
    diagram = build_state_diagram(machine, options)
    output_diagram(diagram, io, options)
  end
  
  private
  
  # Override this method for custom output
  def self.output_diagram(diagram, io, options)
    # Your rendering logic here
  end
endError Handling
The gem provides robust error handling for common edge cases:
begin
  diagram = StateMachines::Diagram::Builder.build_state_diagram(machine)
rescue StateMachines::Diagram::InvalidStateError => e
  puts "Invalid state configuration: #{e.message}"
rescue StateMachines::Diagram::TransitionError => e
  puts "Transition error: #{e.message}"
end
# Validate diagram structure
if diagram.states.empty?
  raise StateMachines::Diagram::EmptyDiagramError, "No states found"
endAdvanced Usage
Custom Renderer Example
module MyPlantUMLRenderer
  extend StateMachines::Diagram::Renderer
  
  private
  
  def self.output_diagram(diagram, io, options)
    io.puts "@startuml"
    io.puts "title #{diagram.title}" if diagram.title
    
    diagram.states.each do |state|
      io.puts "state #{state.id}"
    end
    
    diagram.transitions.each do |transition|
      line = "#{transition.source_state_id} --> #{transition.target_state_id}"
      line += " : #{transition.label}" if transition.label
      
      annotations = []
      annotations << "#{transition.guard}" if transition.guard  
      annotations << "#{transition.action}" if transition.action
      line += " [#{annotations.join(', ')}]" unless annotations.empty?
      
      io.puts line
    end
    
    io.puts "@enduml"
  end
end
# Use the custom renderer
StateMachines::Machine.renderer = MyPlantUMLRenderer
Order.state_machine(:status).drawComplex State Machine Support
class Dragon
  # Multiple parallel state machines
  state_machine :mood, initial: :sleeping do
    state :sleeping, :hunting, :hoarding
    
    event :wake_up do
      transition sleeping: :hunting, if: :hungry?
      transition sleeping: :hoarding, unless: :hungry?
    end
    
    event :find_treasure do
      transition hoarding: :hoarding  # Self-transition
    end
  end
  
  state_machine :flight, initial: :grounded do
    state :grounded, :airborne
    
    event :take_off do
      transition grounded: :airborne, action: :spread_wings
    end
  end
  
  def hungry?
    @hunger_level > 5
  end
  
  def spread_wings
    puts "Dragon spreads mighty wings!"
  end
end
# Generate diagrams for each state machine
Dragon.state_machine(:mood).draw(show_conditions: true)
Dragon.state_machine(:flight).draw(show_callbacks: true)Filtering and Options
# Focus on specific state
Order.state_machine(:status).draw(state_filter: :processing)
# Focus on specific event  
Order.state_machine(:status).draw(event_filter: :ship)
# Show semantic information
Order.state_machine(:status).draw(show_conditions: true, show_callbacks: true)
# Human-readable names
Order.state_machine(:status).draw(human_names: true)
# Output to file
File.open('order_diagram.json', 'w') do |file|
  Order.state_machine(:status).draw(io: file, format: :json)
endTesting Strategy
The gem includes comprehensive test coverage with robust fixtures:
# Run all tests
rake test
# Test specific components
ruby -Itest test/unit/builder_test.rb
ruby -Itest test/unit/renderer_test.rb
# Lint code
bundle exec rubocopTest Coverage
- Builder Tests: State extraction, transition mapping, guard condition handling
- Renderer Tests: Output format validation, option handling, error cases
- Integration Tests: End-to-end workflows with complex state machines
- Edge Case Tests: Invalid states, circular transitions, missing callbacks
Ecosystem Integration
This gem serves as the foundation for rendering-specific gems:
- 
state_machines-diagram(this gem): Core diagram building and IR
- 
state_machines-mermaid: Mermaid syntax renderer
- 
state_machines-graphviz: GraphViz DOT format renderer (planned)
- Custom renderers: PlantUML, SVG, ASCII art, etc.
Supported State Machine Libraries
- 
state_machines(primary)
- state_machines-activerecord
- state_machines-activemodel
Ruby Version Support
- Ruby 3.3+: Required for pattern matching and modern syntax
- Rails 7.2+: Optional, for ActiveRecord/ActiveModel integration
Performance Considerations
- 
Immutable IR: Uses Dry::Structfor thread-safe, immutable diagram objects
- 
Lazy Evaluation: Diagrams are built only when drawis called
- Memory Efficient: No global state or caching by default
- Streaming Support: Large diagrams can be rendered incrementally
Contributing
- Fork the repository
- Create a feature branch (git checkout -b my-new-feature)
- Add tests for your changes
- Ensure all tests pass (rake test)
- Ensure code style compliance (bundle exec rubocop)
- Submit a pull request
Adding a New State Machine Library
To support a new state machine library, implement the builder pattern:
module StateMachines::Diagram::Adapters
  class MyLibraryAdapter
    def initialize(machine)
      @machine = machine
    end
    
    def build_states
      # Extract states from @machine
    end
    
    def build_transitions  
      # Extract transitions from @machine
    end
  end
endLicense
The gem is available as open source under the terms of the MIT License.
Changelog
See CHANGELOG.md for version history and breaking changes.