ActiveHash is a simple base class that allows you to use a ruby hash as a readonly datasource for an ActiveRecord-like model.
ActiveHash assumes that every hash has an :id key, which is what you would probably store in a database. This allows you to seemlessly upgrade from ActiveHash objects to full ActiveRecord objects without having to change any code in your app, or any foreign keys in your database.
It also allows you to use #belongs_to in your AR objects.
ActiveHash can also be useful to create simple test classes that run without a database – ideal for testing plugins or gems that rely on simple AR behavior, but don’t want to deal with databases or migrations for the spec suite.
ActiveHash also ships with:
- ActiveFile: a base class that will reload data from a flat file every time the flat file is changed
- ActiveYaml: a base class that will turn YAML into a hash and load the data into an ActiveHash object
Installation
sudo gem install zilkey-active_hash
Or go to http://github.com/zilkey/active_hash/tree/master for more information.
Usage
To use ActiveHash, you need to:
- Inherit from ActiveHash::Base
- Define your fields
- Define your data
A quick example would be:
class Country < ActiveHash::Base
field :name
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
Defining Fields
You can define fields in 2 ways, using the :fields method, or using the :field method, which allows you to specify a default value for the field:
class Country < ActiveHash::Base
fields :name, :population
field :is_axis_of_evil, :default => false
end
Defining Data
You can define data inside your class or outside. For example, you might have a class like this:
# app/models/country.rb
class Country < ActiveHash::Base
fields :name, :population
end
# config/initializers/data.rb
Country.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
If you prefer to store your data in YAML, see below.
Class Methods
ActiveHash gives you ActiveRecord-esque methods like:
Country.all # => returns all Country objects
Country.count # => returns the length of the .data array
Country.first # => returns the first country object
Country.last # => returns the last country object
Country.find 1 # => returns the first country object with that id
Country.find [1,2] # => returns all Country objects with ids in the array
Country.find :all # => same as .all
Country.find :all, args # => the second argument is totally ignored, but allows it to play nicely with AR
Country.find_by_id 1 # => find the first object that matches the id
It also gives you a few dynamic finder methods. For example, if you defined :name as a field, you’d get:
Country.find_by_name "foo" # => returns the first object matching that name
Country.find_all_by_name "foo" # => returns an array of the objects with matching names
Instance Methods
ActiveHash objects implement enough of the ActiveRecord api to satisfy most common needs. For example:
Country#id # => returns the numeric id or nil
Country#quoted_id # => returns the numeric id
Country#to_param # => returns the id as a string
Country#new_record? # => false
Country#readonly? # => true
Country#hash # => the hash of the id (or the hash of nil)
Country#eql? # => compares type and id, returns false if id is nil
ActiveHash also gives you methods related to the fields you defined. For example, if you defined :name as a field, you’d get:
Country#name # => returns the passed in name
Country#name? # => returns true if the name is not blank
Integration with Rails
You can create .belongs_to associations from rails objects, like so:
class Country < ActiveHash::Base
fields :name, :population
end
class Person < ActiveRecord::Base
belongs_to :country
end
You can also use standard rails view helpers, like #collection_select:
<%= collection_select :person, :country_id, Country.all, :id, :name %>
ActiveYaml
If you want to store your data in YAML files, just inherit from ActiveYaml and specify your path information:
class Country < ActiveYaml::Base
field :name
end
By default, this class will look for a yml file named “countries.yml” in the same directory as the file. You can either change the directory it looks in, the filename it looks for, or both:
class Country < ActiveYaml::Base
set_root_path "/u/data"
set_filename "sample"
field :name
end
The above example will look for the file “/u/data/sample.yml”.
ActiveYaml, as well as ActiveFile, check the mtime of the file you specified, and reloads the data if the mtime has changed. So you can replace the data in the files even if your app is running in production mode in rails.
ActiveFile
If you store encrypted data, or you’d like to store your flat files as CSV or XML or any other format, you can easily extend ActiveHash to parse and load your file. Just add a custom ::load_file method, and define the extension you want the file to use:
class Country < ActiveFile::Base
set_root_path "/u/data"
set_filename "sample"
field :name
class << self
def extension
".super_secret"
end
def load_file
MyAwesomeDecoder.load_file(full_path)
end
end
end
The two methods you need to implement are load_file, which needs to return an array of hashes, and .extension, which returns the file extension you are using. You have full_path available to you if you wish, or you can provide your own path.
Authors
Written by Mike Dalessio, Ben Woosley and Jeff Dean
Enjoy!
This looks awesome, and might replace enumeration_mixin (and virtual enumerations) as my go-to enum implementation.
July 22, 2009 at 11:40 pm
Looks like a great idea. I plan to begin using it this week.
Should load_file return an array of hashes, rather than a hash?
Thanks,
Dan
July 27, 2009 at 11:24 pm
Yes – it load_file should return an array of hashes. I’ll update that now – thanks!
August 2, 2009 at 12:11 pm
Just started using this. It’s exactly what I needed.
I wonder what your thoughts are on providing class methods to access instances by name, like this:
Country.US # => the country whose name is ‘US’
August 3, 2009 at 4:24 pm
@jb – Robby Russell posted something similar to that in http://www.robbyonrails.com/articles/2009/06/23/using-model-constants-for-project-sanity, but I wasn’t planning on adding that feature.
If you have simple enumerations, with just an id and a name, then these class methods might make sense, but once you go beyond that it’s not immediately clear what those methods would do. Given the fact that ActiveHash only needs an id, there would have to be a declarative way to define which field creates the constants, and check for uniqueness.
That being said, I could see an ActiveEnum base class that only recognizes id and name, and adds these fields. Patches are always welcome!
August 3, 2009 at 6:46 pm