New Gem: HT (Hash Templating)
Note: This was originally posted on my previous blog. It is archived here for sentimental, educational (to me) and, in the unlikely case, actual value
While writing some code the other day, I needed to build up a hash based on various inputs and pass it to some API call. Although the inputs were different for each case the final hash passed in always had some base objects in common but the set of in common key-value pairs varied too. To build a hash for some given inputs I wrote a block which called other blocks that built other parts of the hash. These sub-blocks or building blocks were shared between input sets to build the in-common pieces. This became cubersome & ugly. Instead, I wrote & have recently released, HT, a gem for templating hashes in layers.
HT allows you to define a set of layers, or a cascade. Each layer sets the values for keys in the resulting hash, operating on a raw data hash passed in at build-time. You can, also, decide up to what layer to build to. The build sequence is defined by creating a base layer then layering others on top of it and each other. The sequence is determined at build-time, and moves up through the determined list layer by layer. The build always starts with the base layer.
Installation
Before we get started lets install HT.
gem install ht
HT is only supported on Ruby 1.9.
A Quick Example
With HT installed lets quickly show how it is used. After, I will show the code to make it work. In the example below, we have an instance of HT::Cascade and use it to build hashes up to various layers in the cascade.
# cascade = instance of HT::Cascade
>> cascade.build(:path_1_all, {:a => 1, :b => 2})
{:a => 2, :b => 1, :c => 3, :d => "abc"}
>> cascade.build(:path_1_mid, {:a => 1, :b => 2})
{:a => :2, :b => 2, :d => "abc"}
>> cascade.build(:path_2_all, {:a => 1, :b => 2})
{1 => :a, 2 => :b, :a => 1, :b => 2, :c => 2, :d => "abc"}
>> cascade.build(:path_2_mid, {:a => 1, :b => 2})
{1 => :a, 2 => :b, , :a => 1, :b => 2, :d => "abc"}
>> cascade.build(:path_share, {:a => 1, :b => 2})
{:a => 1, :b => 2, :d => "abc"}
>> cascade.build(:base, {:a => 1, :b => 2})
{:d => "abc"}
Let’s talk a bit about what happened above. In this example we have several layers. I will call a layer that no other layers depend on a “top layer”. The layers ending in _all are the top layers in this example. As you can see you don’t have to build up to a top layer, but let’s talk about what happens when you do since it will cover most of the functionality. To build :path_1_all we start with the :base which gives use the hash {:d => "abc"}. We then move up to :path_share which merges our data into the result: {:a => 1, :b => 2, :d => "abc"}. Next, :path_1_mid stores the value of :b from the data in :a in the result overwriting its previous value: {:a => 2, :b => 2, :d => "abc"}. Finally, :path_one_all uses the last layer to replace :b with the value from :a from the data and creates :c which is :a + :b.
Digging In
To make the above example work we must create an instance of HT::Cascade. Here’s how this is done.
cascade = HT::Cascade.new(:my_cascade) do |t|
t.base do |t, data|
t.set_value :d, "abc"
end
t.layer :path_share do |t, data|
t.set_value :a, data[:a]
t.set_value :b, data[:b]
end
t.layer :path_1_mid, :path_share do |t, data|
t.set_value :a, data[:b]
end
t.layer :path_2_mid, :path_share do |t, data|
t.set_value t.get_value(:a), :a
t.set_value t.get_value(:b), :b
end
t.layer :path_1_all, :path_1_mid do |t, data|
t.set_value :b, data[:a]
t.set_value :c, t.get_value(:a) + t.get_value(:b)
end
t.layer :path_2_all, :path_2_mid do |t, data|
t.set_value :c, data[:a] * data[:b]
end
end
To get a better feel of what this is doing I suggest reading the explanation of how :path_1_all is built while looking at the code. In short, we create our instance, giving the cascade a name. We then define our base, this is required. After, we define our first layer. It depends on :base so we do not pass in a second argument. We then go on to define other layers, creating two distinct paths through the cascade, by passing in the layer to depend on as the second argument. Each block passed to these methods takes two arguments: the instance of the cascade and the data hash passed in at build-time. The blocks are called at build-time facilitating the hash creation. These are simple examples but you are free to create complex logic here.
The API to define cascades and use them may change in the future. Read on for the how & why.