Project

submodel

0.0
No commit activity in last 3 years
No release in over 3 years
Submodel maps ActiveRecord columns to ActiveModel models.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.6
>= 0
>= 0
>= 0
>= 0
>= 3.0.0.beta2

Runtime

 Project Readme

Submodel

Submodel maps ActiveRecord columns to ActiveModel models, so that hstore or serialized hash columns can share validations and be augmented with some methods. This can greatly help cleanup your business logic.

Usage

# Gemfile
gem 'submodel'

Create a submodel with ActiveModel::Model. Here’s an example Address model using Carmen to provide country and state validations.

# app/submodels/address.rb

class Address
  include ActiveModel::Model

  COUNTRY_CODES  = Carmen::Country.all.map(&:code)
  CA_STATE_CODES = Carmen::Country.coded('CA').subregions.map(&:code)
  US_STATE_CODES = Carmen::Country.coded('US').subregions.map(&:code)

  attr_accessor :street_1, :street_2, :city, :state, :country, :postal_code

  validates_inclusion_of :country, in: COUNTRY_CODES
  validates_inclusion_of :state, in: CA_STATE_CODES, if: :canada?
  validates_inclusion_of :state, in: US_STATE_CODES, if: :united_states?

  def canada?
    country == 'CA'
  end

  def united_states?
    country == 'US'
  end
end

Use the submodel method to map your ActiveRecord columns to the submodel.

# app/models/order.rb

class Order < ActiveRecord::Base
  submodel :billing_address, Address
end

Then, accessing #billing_address will return an instance created with Address.new. Similarly, passing a hash to #billing_address= will create a new instance with the hash as argument.

order = Order.new
order.attributes # => { "id" => nil, "billing_address" => nil }

order.billing_address # => #<Address>
order.billing_address.blank? # => true

order.billing_address.street_1 = '123 Fake Street'
order.billing_address # => #<Address street_1="123 Fake Street">
order.billing_address.blank? # => false

order.billing_address = { country: 'CA', state: 'QC' }
order.billing_address # => #<Address state="QC" country="CA">

Note: While the getter creates an instance on demand, blank submodels are persisted as NULL.

Comparison

When using ==, your submodel columns will be compared based on the stringified hash of their instance variables. Blank variables are ignored.

order = Order.new
order.billing_address # => #<Address>

order.billing_address == Address.new # => true
order.billing_address == {} # => true
order.billing_address == { street_1: '', street_2: '  ' } # => true
order.billing_address == { street_1: 'foo', street_2: 'bar' } # => false

order.billing_address.country = 'CA'
order.billing_address.state = 'QC'
order.billing_address == { 'country' => 'CA', :state => 'QC' } # => true
order.billing_address == Address.new(country: 'CA') # => false

Extending submodels per-column

You can pass the submodel method a block to be executed at the class level. For instance, this adds an (unfortunate) validation to shipping_address, leaving billing_address as is.

class Order < ActiveRecord::Base
  submodel :billing_address, Address
  submodel :shipping_address, Address do
    validates :country, inclusion: { in: %w[US CA] }
  end
end

This gem seems overkill.

You might think “Why not just override the getter and setter?” In my experience, getting this right is always more complex. If you want proper behavior (validation, comparison, FormBuilder support, persistence) you basically have to repeat this code for every column.


© 2014 Rafaël Blais Masson. Submodel is released under the MIT license.