A readonly ActiveRecord-esque base class that lets you use a hash, a Yaml file or a custom file as the datasource
A readonly ActiveRecord-esque base class that lets you use a hash, a Yaml file or a custom file as the datasource
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 seamlessly 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 #has_many and #belongs_to (via belongs_to_active_hash) 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:
We have changed returned value to chainable by v3.0.0. It's not just an Array instance anymore.
If it breaks your application, please report us on issues, and use v2.x.x as following..
gem 'active_hash', '~> 2.3.0'Bundler:
gem 'active_hash'Other:
gem install active_hashRequirements:
gem 'active_hash', '~> 1.5.3'We wrote ActiveHash so that we could use simple, in-memory, ActiveRecord-like data structures that play well with Rails forms, like:
# in app/models/country.rb
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
# in some view
<%= collection_select :person, :country_id, Country.all, :id, :name %>Before ActiveHash, we did things like:
# in app/models/person.rb
class Person < ActiveRecord::Base
COUNTRIES = ["US", "Canada"]
end
# in some view
<%= collection_select :person, :country_id, Person::COUNTRIES, :to_s, :to_s %>The majority of ActiveHash uses involve setting up some data at boot time, and never modifying that data at runtime.
To use ActiveHash, you need to:
A quick example would be:
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
country = Country.new(:name => "Mexico")
country.name # => "Mexico"
country.name? # => trueYou can also use create:
class Country < ActiveHash::Base
field :name
create :id => 1, :name => "US"
create :id => 2, :name => "Canada"
endYou can also use add:
class Country < ActiveHash::Base
field :name
add :id => 1, :name => "US"
add :id => 2, :name => "Canada"
endActiveHash will auto-define all fields for you when you load the hash. For example, if you have the following class:
class CustomField < ActiveHash::Base
self.data = [
{:custom_field_1 => "foo"},
{:custom_field_2 => "foo"},
{:custom_field_3 => "foo"}
]
endOnce you call CustomField.all it will define methods for :custom_field_1, :custom_field_2 etc...
If you need the fields at load time, as opposed to after .all is called, you can also define them manually, like so:
class CustomField < ActiveHash::Base
fields :custom_field_1, :custom_field_2, :custom_field_3
endNOTE: auto-defined fields will not override fields you've defined, either on the class or on the instance.
If some of your hash values contain nil, and you want to provide a default, you can specify defaults with the :field method:
class Country < ActiveHash::Base
field :is_axis_of_evil, :default => false
endYou 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
end
# config/initializers/data.rb
Rails.application.config.to_prepare do
Country.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
endIf you prefer to store your data in YAML, see below.
ActiveHash gives you ActiveRecord-esque methods like:
…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
Country.find_by_id_and_name 1, "Germany" # => returns the first object matching that id and name
Country.find_all_by_id_and_name 1, "Germany" # => returns an array of objects matching that name and idFurthermore, it allows to create custom scope query methods, similar to how it's possible with ActiveRecord:
Country.scope :english, -> { where(language: 'English') } # Creates a class method Country.english performing the given query
Country.scope :with_language, ->(language) { where(language: language) } # Creates a class method Country.with_language(language) performing the given queryActiveHash objects implement enough of the ActiveRecord api to satisfy most common needs. For example:
Country#id # => returns the id or nil
Country#id= # => sets the id attribute
Country#quoted_id # => returns the numeric id
Country#to_param # => returns the id as a string
Country#new_record? # => returns true if is not part of Country.all, false otherwise
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 nilActiveHash 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
Country#name= # => sets the nameThe ActiveHash::Base.all method functions like an in-memory data store. You can save your records as ActiveHash::Relation object by using standard ActiveRecord create and save methods:
…Notice that when adding records to the collection, it will auto-increment the id for you by default. If you use string ids, it will not auto-increment the id. Available methods are:
Country.insert( record )
Country#save
Country#save!
Country.create
Country.create!As such, ActiveHash::Base and its descendants should work with Fixjour or FactoryBot, so you can treat ActiveHash records the same way you would any other ActiveRecord model in tests.
To clear all records from the in-memory array, call delete_all:
Country.delete_all # => does not affect the yaml files in any way - just clears the in-memory array which can be useful for testingOne common use case for ActiveHash is to have top-level objects in memory that ActiveRecord objects belong to.
class Country < ActiveHash::Base
end
class Person < ActiveRecord::Base
extend ActiveHash::Associations::ActiveRecordExtensions
belongs_to :country
endNOTE: this needs to be called on a subclass of ActiveRecord::Base. If you extend ActiveRecord::Base, it will not work.
If you want to extend ActiveRecord::Base so all your AR models can belong to ActiveHash::Base objects, you can use the
belongs_to_active_hash method:
ActiveRecord::Base.extend ActiveHash::Associations::ActiveRecordExtensions
class Country < ActiveHash::Base
end
class Person < ActiveRecord::Base
belongs_to_active_hash :country
endSince ActiveHashes usually are static, we can use shortcuts to assign via an easy to remember string instead of an obscure ID number.
…You can have multiple shortcuts, so settings :shortcuts => [:name, :friendly_name] will enable you to use #country_name= and #country_friendly_name=.
If you include the ActiveHash::Associations module, you can also create associations from your ActiveHash classes, like so:
class Country < ActiveHash::Base
include ActiveHash::Associations
has_many :people
end
class Person < ActiveHash::Base
include ActiveHash::Associations
belongs_to :country
has_many :pets
end
class Pet < ActiveRecord::Base
endOnce you define a belongs to, you also get the setter method:
class City < ActiveHash::Base
include ActiveHash::Associations
belongs_to :state
end
city = City.new
city.state = State.first
city.state_id # is State.first.idNOTE: You cannot use ActiveHash objects as children of ActiveRecord and I don't plan on adding support for that. It doesn't really make any sense, since you'd have to hard-code your database ids in your class or yaml files, which is a dependency inversion.
Thanks to baldwindavid for the ideas and code on that one.
If you want to store your data in YAML files, just inherit from ActiveYaml and specify your path information:
class Country < ActiveYaml::Base
endBy 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"
endThe above example will look for the file "/u/data/sample.yml".
Since ActiveYaml just creates a hash from the YAML file, you will have all fields specified in YAML auto-defined for you. You can format your YAML as an array, or as a hash:
# array style
- id: 1
name: US
- id: 2
name: Canada
- id: 3
name: Mexico
# hash style
us:
id: 1
name: US
canada:
id: 2
name: Canada
mexico:
id: 3
name: MexicoWhen using the hash format for your YAML file, ActiveYaml will automatically add a key attribute with the name of the object. You can overwrite this by setting the key attribute in the YAML file.
For example:
au:
id: 1
name: AustraliaWhen you access the object you can do Country.find(1).key => 'au'. Or Country.find_by_key('au')
If you want a different key on only some objects you can mix and match:
au:
id: 1
key: aus
name: Australia
nz:
id: 2
name: New ZealandCountry.find(1).key => 'aus'
Country.find(2).key => 'nz'
You can use multiple files to store your data. You will have to choose between hash or array style as you cannot use both for one model.
class Country < ActiveYaml::Base
use_multiple_files
set_filenames "europe", "america", "asia", "africa"
endAliases can be used in ActiveYaml using either array or hash style by including ActiveYaml::Aliases.
With that module included, keys beginning with a '/' character can be safely added, and will be ignored, allowing you to add aliases anywhere in your code:
# Array Style
- /aliases:
soda_flavor: &soda_flavor
sweet
soda_price: &soda_price
1.0
- id: 1
name: Coke
flavor: *soda_flavor
price: *soda_price
# Key style
/aliases:
soda_flavor: &soda_flavor
sweet
soda_price: &soda_price
1.0
coke:
id: 1
name: Coke
flavor: *soda_flavor
price: *soda_priceclass Soda < ActiveYaml::Base
include ActiveYaml::Aliases
end
Soda.length # => 1
Soda.first.flavor # => sweet
Soda.first.price # => 1.0Embedded ruby can be used in ActiveYaml using erb brackets `<%
No open issues yet, or sync has not completed.