Blueprint

Overview

Blueprint is a general purpose Ruby meta-language that you can use to describe hierarchical data structures, similar to XML, but directly in Ruby. This gives you the ability to define a dynamic data structure that can be based on queries to other sources, like say, a database.

The point of Blueprint, the reason it was created, was to be able to define the information architecture of an application that a UI can use to procedurally build itself. It allows you to pull out the data from views and the structural relationship management of that data from controllers and deal with it as a separate concern.

So it's sort of like using XML and XSLT, but without actually using them. With Blueprint, you define a data structure similar to XML that's code, not data, meaning it can be dynamic (like code that generates XML). Then you can query that data structure within Ruby with a chain of method calls that feels similar to something like XPath, and use that data in your views. It's especially good for defining navigational structure.

This gives you a layer between your views and models that transcends controllers... if that makes sense.

Example

Okay, so let's get concrete. Here is Blueprint in action, defining the navigational architecture of an application that lets you manage Services, Accounts (as in bookkeeping), and Users. We put the definition in a class that extends Blueprint::Countainer, which gives us a define class method to pass the definition block to. I put this class in the model directory in Rails applications.

class IA < Blueprint::Container
  define do
    node 'tabs' do
      tab 'Home', :link_to => {:action => :home}
  
      tab 'Services', :link_to => {:action => :services} do
        node 'Services' do
          items Service.find(:all) do |service|
            items Package.find_all_by_service_id(service.id) do |package|
              items Subscription.find_all_by_package_id(package.id)
            end
          end
        end
      end
  
      tab 'Accounting' do
        ['asset', 'liability', 'capital'].each do |t|
          node "#{t.titleize} Accounts" do
            items Account.find_all_by_type(t) 
          end
        end
      end

      tab 'Search', :align => :right
  
      tab 'Users', :align => :right do
        node 'Users' do
          items User.find(:all)
        end
      end

    end

  end
end

This could then be used to generate a basic UI as pictured to the right.

Notice the use of ActiveRecord because that makes a point of it being dynamic. Sometimes your navigation is based on data that needs to be retrieved dynamically.

Also notice the use of arbitrary attributes, such as :align and :link_to. You can define whatever attributes you like and use them however you need in your UI. There are a few "special" attributes that are reserved for Blueprint functionality, but we're not using any here.

And lastly notice the use of "tab" as what seems to be a primitive. Actually, only "node" and "item" are primitive node types. You're free to define whatever type of node you wish by simply using it. This makes your datastructure readable and comes in handy when querying.

Actually I want you to notice one more thing. Declaring a node with a plural type is used to make many nodes, and each node will have the same children as defined by that block. It's a macro of the sort, but applies to any node type you want, be it simply "node", or "item", or "tab", or "zebra".

Using your Blueprint

This is how you'd use this blueprint to render the tab-like menu at the top (assuming some CSS styles applied):

<ul>
  <% for tab in IA.tabs %>
  <li style="float: <%= tab[:align] == :right ? 'right' : 'left' %>;">
    <%= link_to_remote tab.to_s, :url => {:action => 'menu_select', :tab => tab.name, :content => url_for(tab[:link_to])} %>
  </li>
  <% end %>
</ul>

IA is the name of the container class that has our blueprint definition. When I call IA.tabs, I'm getting the node named 'tabs' that we first see in the blueprint, and then we iterate on it to get its children, which are the tab nodes. You can see how we make use of the attributes of the tab nodes. If it's not obvious, the :link_to attribute is used to define the content area of the page (where you see "this is services", that's a template that was located by the :link_to attribute of the 'Services' tab node.)

So that was a fairly simple example as far as querying the blueprint, but let's say we wanted to make a menu (or even just list!) of all the nodes representing asset accounts, which are deeper in the structure. We would just do something like this:

<ul>
  <% for account in IA.tabs.accounting.asset_accounts %>
  <li><%= account[:item].name %> has a balance of $<%= account[:item].balance %></li>
  <% end %>
</ul>

What we're iterating on is a query made to our blueprint. You just query by calling the names of the nodes. The names are defined by the first argument, which can be a display title such as "Asset Accounts", but are queried as a Ruby-style name, like "asset_accounts".

You see here one of the first reserved attributes used by Blueprint, which is specific to the item node type. The :item attribute is the payload for an item node, in this case, an ActiveRecord object that has at least a 'name' and 'balance' attribute.

I'm not going to get into the navigation tree on the left. It's a little complex, and I borrowed most of the code from Radiant, but Blueprint is excellent for building these. You can use the attributes to specify icons, etc.

But Why?

Why use this instead of doing it by hand? Well, this was made with large-scale applications in mind, better yet, a sort of standard, configurable administration interface that can be used for any data model. Something along the lines of Streamlined, but with this and lots of other goodies. (hint, hint)

Other uses?

Obviously, you could use this for a lot of things because it's really, I guess, concern neutral and very flexible. In a way, it's almost like a meta-language starter kit because it's not specific to any domain and already does a lot of work for you... as long as you need a dynamic hierarchical data structure.

Cut short

Well that's all I have for now. I'll write more later. This hasn't been packaged up or anything yet, but it's part of the prototype application you'll find the in the repository. (http://svn.devjavu.com/blueprint)

UPDATE: It's now at least packaged as a Rails plugin in the trunk: http://svn.devjavu.com/blueprint/trunk/

--Jeff Lindsay (progrium AT gmail)

Attachments