Tagged With   gem:name=hirb , lib:name=irb , gem:name=activerecord , gem:tags=rails , lib:plugin=acts_as_tree , gem:topic=tree , post:lang=ruby , post:type=tutorial

Ruby Class and Rails Plugin Trees With Hirb

In my last post, I demonstrated how Hirb can render puurty tables with automated views. In this post, I’ll show Hirb’s new tree view using its console methods and these three examples- class inheritance trees, nested class trees and Rails’ ActiveRecord trees.

First, let’s view a basic tree with Hirb.

  bash> irb -rubygems -rhirb
  
  # Import a view() method we'll use.
  irb>> extend Hirb::Console
  => main
  
  # Create a tree structure Hirb understands.
  irb>> tree = [[0,0], [1,1], [2,2], [2,3], [1,4]]
  => [[0,0], [1,1], [2,2], [2,3], [1,4]]
  
  # Render a basic tree.
  irb>> view tree, :class=>:tree
  0
      1
          2
          3
      4
  => true
  
  # Render a directory tree.
  irb>> view tree, :class=>:tree, :type=>:directory
  0
  |-- 1
  |   |-- 2
  |   `-- 3
  `-- 4
  => true
  
  # Render a tree with each level numbered.
  irb>> view tree, :class=>:tree, :type=>:number
  0. 0
      1. 1
          1. 2
          2. 3
      1. 4

A Hirb tree consists of an array of nodes. Each node is an array consisting of it’s depth/level in the tree and its value.

Class Inheritance Trees

Although that basic tree was easy to build, more complex trees require more setup. Thankfully Hirb’s ParentChildTree class makes tree building easy. It’s based on one simple assumption: each node has a method to retrieve its immediate children. Given a node, ParentChildTree uses that method to build out all the children under it. To put this class into exercise let’s build a class inheritance tree for any Ruby class. Does a Ruby class have a method to retrieve it’s immediate subclasses? No, but no worries. Ruby’s got your back:

  module InheritanceTree
    # Retrieves objects of the current class.
    def objects
      class_objects = []
      ObjectSpace.each_object(self) {|e| class_objects << e }
      class_objects
    end
    
    # Retrives immediate subclasses of the current class.
    def class_children
      (@class_objects ||= Class.objects).select {|e| e.superclass == self }
    end
  end
  
  Module.send :include, InheritanceTree
  # Note that in modifying a class as ubiquitous as Module that I extend its behavior through 
  # a traceable module. I think it's good manners to let others know you're modifying core
  # classes: http://tagaholic.me/2009/01/31/share-extensions-without-monkey-patching.html

That wasn’t too hard. class_children() is the method we seek. It iterates over all existing classes and only picks those that are subclasses of the current class. Having extended Module, let’s see some inheritance trees:

  # test drive :class_children
  irb>> Numeric.class_children
  => [Float, Integer, Date::Infinity, Rational]
  
  # Must explicitly pass :class_children since ParentChildTree assumes a :children method by default.
  irb>> view Numeric, :class=>:parent_child_tree, :children_method=>:class_children, :type=>:directory
  Numeric
  |-- Float
  |-- Integer
  |   |-- Bignum
  |   `-- Fixnum
  |-- Date::Infinity
  `-- Rational
  => true
  
  irb>> view StandardError, :class=>:parent_child_tree, :children_method=>:class_children, :type=>:directory
  # Just kidding. Don't want to double the length of this post.
  
  # We can however do a subset of that tree:
  irb>> view RuntimeError, :class=>:parent_child_tree, :children_method=>:class_children, :type=>:directory
  RuntimeError
  `-- Gem::Exception
      |-- Gem::VerificationError
      |-- Gem::RemoteSourceException
      |-- Gem::RemoteInstallationSkipped
      |-- Gem::RemoteInstallationCancelled
      |-- Gem::RemoteError
      |-- Gem::OperationNotSupportedError
      |-- Gem::InvalidSpecificationException
      |-- Gem::InstallError
      |-- Gem::GemNotFoundException
      |-- Gem::FormatException
      |-- Gem::FilePermissionError
      |-- Gem::EndOfYAMLException
      |-- Gem::DocumentError
      |-- Gem::GemNotInHomeException
      |-- Gem::DependencyRemovalException
      |-- Gem::DependencyError
      `-- Gem::CommandLineError
  => true


Nested Class Trees

Seeing how easily Hirb creates trees for us, let’s build another one. When I start using a new gem, it helps to get an overview of the classes and modules it defines. All we need is a method that can retrieve a class’ immediate nested class (ie Gem::Error is a nested class of Gem):

  module NestedTree
    # Since nested classes are just constants:
    def nested_children
      constants.map {|e| const_get(e) }.select {|e| e.is_a?(Module) } - [self]
    end
    
    def nested_name
      self.to_s.split(":")[-1]
    end
  end
  
  Module.send :include, NestedTree

Update: nested_children() has been tweaked to avoid recursion in some cases as pointed out in the comments.

With nested_children(), we’re ready to rock. Let’s view nested classes under Hirb:

  # test drive nested_children
  irb>> Hirb.nested_children
  => [Hirb::Util, Hirb::View, Hirb::Console, Hirb::Helpers, Hirb::Views, Hirb::HashStruct]

  irb>> view Hirb, :class=>:parent_child_tree, :children_method=>:nested_children, :type=>:directory
  Hirb
  |-- Hirb::Util
  |-- Hirb::View
  |-- Hirb::Console
  |-- Hirb::Helpers
  |   |-- Hirb::Helpers::ObjectTable
  |   |   `-- Hirb::Helpers::Table::TooManyFieldsForWidthError
  |   |-- Hirb::Helpers::AutoTable
  |   |-- Hirb::Helpers::ParentChildTree
  |   |   |-- Hirb::Helpers::Tree::ParentlessNodeError
  |   |   `-- Hirb::Helpers::Tree::Node
  |   |       |-- Hirb::Helpers::Tree::Node::MissingValueError
  |   |       `-- Hirb::Helpers::Tree::Node::MissingLevelError
  |   |-- Hirb::Helpers::Table
  |   |   `-- Hirb::Helpers::Table::TooManyFieldsForWidthError
  |   |-- Hirb::Helpers::ActiveRecordTable
  |   |   `-- Hirb::Helpers::Table::TooManyFieldsForWidthError
  |   `-- Hirb::Helpers::Tree
  |       |-- Hirb::Helpers::Tree::ParentlessNodeError
  |       `-- Hirb::Helpers::Tree::Node
  |           |-- Hirb::Helpers::Tree::Node::MissingValueError
  |           `-- Hirb::Helpers::Tree::Node::MissingLevelError
  |-- Hirb::Views
  |   `-- Hirb::Views::ActiveRecord_Base
  `-- Hirb::HashStruct
  => true

Sweet! A nice visualization of Hirb’s classes. But wouldn’t it be nice if a nested class only showed its nested name? That’s what we defined nested_name() for:

>> view Hirb, :class=>:parent_child_tree, :children_method=>:nested_children, :type=>:directory,
  :value_method=>:nested_name
Hirb
|-- Util
|-- View
|-- Console
|-- Helpers
|   |-- ObjectTable
|   |   `-- TooManyFieldsForWidthError
|   |-- AutoTable
|   |-- ParentChildTree
|   |   |-- ParentlessNodeError
|   |   `-- Node
|   |       |-- MissingValueError
|   |       `-- MissingLevelError
|   |-- Table
|   |   `-- TooManyFieldsForWidthError
|   |-- ActiveRecordTable
|   |   `-- TooManyFieldsForWidthError
|   `-- Tree
|       |-- ParentlessNodeError
|       `-- Node
|           |-- MissingValueError
|           `-- MissingLevelError
|-- Views
|   `-- ActiveRecord_Base
`-- HashStruct
=> true

Better! Note that we passed a :value_method option to tell ParentChildTree what method to call to represent a node. We didn’t have to specify this in the inheritance trees because it defaults to name() which Ruby classes have defined.

ActiveRecord Trees

Having had to build our children methods for our last two tree type examples, this example should be easy. Let’s setup a tree-enabled Rails app to use acts_as_tree :

  # generate rails app
  bash> rails tree
  bash> cd tree

  # generate model
  bash> script/generate model category name:string parent_id:integer
  bash> rake db:migrate

  # install plugin
  bash> script/plugin install git://github.com/rails/acts_as_tree.git

  # modify app/models/category.rb to look like:
  # class Category < ActiveRecord::Base
  #   acts_as_tree
  # end

With this app setup, let’s fire up script/console to create some records:

  bash> script/console
  Loading development environment (Rails 2.2.2)

  ## Create some categories
  irb>> conf.echo = false
  irb>> Category.create(:name=>'root')
  irb>> _.children.create(:name=>'gems')
  irb>> _.children.create(:name=>'hirb')
  irb>> _.parent.children.create(:name=>'utility_belt')
  irb>> Category.root.children.create(:name=>'plugins')
  irb>> _.children.create(:name=>'acts_as_tree')
  irb>> conf.echo = true

  # Enable Hirb so we get table views for ActiveRecord objects.
  irb>> require 'hirb'; Hirb::View.enable
  => nil

  # Let's see what we've created.
  irb>> Category.all
  +----+-------------------------+--------------+-----------+-------------------------+
  | id | created_at              | name         | parent_id | updated_at              |
  +----+-------------------------+--------------+-----------+-------------------------+
  | 5  | 2009-03-18 20:41:43 UTC | root         |           | 2009-03-18 20:46:52 UTC |
  | 6  | 2009-03-18 20:46:27 UTC | gems         | 5         | 2009-03-18 20:46:27 UTC |
  | 7  | 2009-03-18 20:48:06 UTC | hirb         | 6         | 2009-03-18 20:48:06 UTC |
  | 8  | 2009-03-18 20:48:28 UTC | utility_belt | 6         | 2009-03-18 20:48:28 UTC |
  | 9  | 2009-03-18 20:54:24 UTC | plugins      | 5         | 2009-03-18 20:54:24 UTC |
  | 10 | 2009-03-18 20:54:38 UTC | acts_as_tree | 9         | 2009-03-18 20:54:38 UTC |
  +----+-------------------------+--------------+-----------+-------------------------+
  => true

With our app and records setup, it’s time to see the trees:

  irb>> extend Hirb::Console
  => main

  # View tree from root node.
  irb>> view Category.root, :class=>:parent_child_tree, :type=>:directory
  root
  |-- gems
  |   |-- hirb
  |   `-- utility_belt
  `-- plugins
      `-- acts_as_tree
  => true

  # We can view a tree from any node:
  irb>> view Category.find_by_name('gems'), :class=>:parent_child_tree, :type=>:directory
  gems
  |-- hirb
  `-- utility_belt
  => true

Sweet! Hirb’s trees should work fine with other tree plugins that have a children method ie rails’ nested set plugin and the awesome nested_set plugin. Don’t forget to pass a :children_method option as we did in previous examples when nodes don’t retrieve their children with children().

Conclusion

In this post, we focused on using Hirb with its console methods. If you didn’t read the previous post, know that any of these views can be automatically echoed based on the output’s class. For the last example, we could trigger an automated directory tree for any Category nodes with:

  irb>> Hirb::View.enable {|c| c.output = {"Category"=>{:class=>"Hirb::Helpers::ParentChildTree", :options=>{:type=>:directory}}} }

As always, I encourage you to fork and share your custom views for irb. Some food for thought for more tree visualizations. If you haven’t already:


gem install hirb

Enjoyed this post? Tell others! hacker newsHacker News | twitterTwitter | DeliciousDelicious | redditReddit
blog comments powered by Disqus