Easy access and creation of “has many” relationships.

What’s the difference between flags, preferences and options? Nothing really, they are just “has many” relationships. So why should I install a separate plugin for each one? This plugin can be used to add preferences, flags, options, etc to any model.

Installation

In your Gemfile:

gem "has_easy"

At the command prompt:

rails g has_easy_migration
rake db:migrate

Example

class User < ActiveRecord::Base
  has_easy :preferences do |p|
    p.define :color
    p.define :theme
  end
  has_easy :flags do |f|
    f.define :is_admin
    f.define :is_spammer
  end
end

user = User.new

# hash like access
user.preferences[:color] = 'red'
user.preferences[:color] # => 'red'

# object like access
user.preferences.theme? # => false, shorthand for !!user.preferences.theme
user.preferences.theme = "savage thunder"
user.preferences.theme # => "savage thunder"
user.preferences.theme? # => true

# easy access for form inputs
user.flags_is_admin? # => false, shorthand for !!user.flags_is_admin
user.flags_is_admin = true
user.flags_is_admin # => true
user.flags_is_admin? # => true

# save user's preferences
user.preferences.save # will trickle down validation errors to user
user.errors.empty? # hopefully true

# save user's flags
user.flags.save! # will raise exception on validation errors

Advanced Usage

There are a lot of options that you can use with has_easy:

  • aliasing

  • default values

  • inheriting default values from parent associations

  • calculated default values

  • type checking values

  • validating values

  • preprocessing values

In this section, we’ll go over how to use each option and explain why it’s useful.

:alias and :aliases

These options go on the has_easy method call and specify alternate ways of invoking the association.

class User < ActiveRecord::Base
  has_easy :preferences, :aliases => [:prefs, :options] do |p|
    p.define :likes_cheese
  end
  has_easy :flags, :alias => :status do |p|
    p.define :is_admin
  end
end

user.preferences.likes_cheese = 'yes'
user.prefs.likes_cheese => 'yes'
user.options_likes_cheese => 'yes'
user.prefs[:likes_cheese] => 'yes'
user.options.likes_cheese? => true
...etc...

:default

Very simple. It does what you think it does.

class User < ActiveRecord::Base
  has_easy :options do |p|
    p.define :gender, :default => 'female'
  end
end

User.new.options.gender # => 'female'

:default_through

Allows the model to inherit it’s default value from an association.

class Client < ActiveRecord::Base
  has_many :users
  has_easy :options do |p|
    p.define :gender, :default => 'male'
  end
end
class User < ActiveRecord::Base
  belongs_to :client
  has_easy :options do |p|
    p.define :gender, :default_through => :client, :default => 'female'
  end
end

client = Client.create
user = client.users.create
user.options.gender # => 'male'

client.options.gender = 'asexual'
client.options.save
user.client(true) # reload association
user.options.gender # => 'asexual'

User.new.options.gender => 'female'

:default_dynamic

Allows for calculated default values.

class User < ActiveRecord::Base
  has_easy 'prefs' do |t|
    t.define :likes_cheese, :default_dynamic => :defaults_to_like_cheese
    t.define :is_dumb, :default_dynamic => Proc.new{ |user| user.dumb_post_count > 10 }
  end

  def defaults_to_like_cheese
    cheesy_post_count > 10
  end
end

user = User.new :cheesy_post_count => 5
user.prefs.likes_cheese? => false

user = User.new :cheesy_post_count => 11
user.prefs.likes_cheese? => true

user = User.new :dumb_post_count => 5
user.prefs.is_dumb? => false

user = User.new :dumb_post_count => 11
user.prefs.is_dumb? => true

:type_check

Allows type checking of values (for people who are into that).

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :theme, :type_check => String
    p.define :dollars, :type_check => [Fixnum, Bignum]
  end
end

user.prefs.theme = 123
user.prefs.save! # ActiveRecord::InvalidRecord exception raised with message like:
                 # 'theme' for has_easy('prefs') failed type check

user.prefs.dollars = "hello world"
user.prefs.save
user.errors.empty? # => false
user.errors.on(:prefs) # => 'dollars' for has_easy('prefs') failed type check

:validate

Make sure that values fit some kind of criteria. If you use a Proc or name a method with a Symbol to do validation, there are three ways to specify failure:

  1. return false

  2. raise a HasEasy::ValidationError

  3. return an array of custom validation error messages

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :foreground, :validate => ['red', 'blue', 'green']
    p.define :background, :validate => Proc.new{ |value| %w[black white grey].include?(value) }
    p.define :midground,  :validate => :midground_validator
  end
  def midground_validator(value)
    return ["msg1", msg2] unless %w[yellow brown purple].include?(value)
  end
end

user.prefs.foreground = 'yellow'
user.prefs.save! # ActiveRecord::InvalidRecord exception raised with message like:
                 # 'theme' for has_easy('prefs') failed validation

user.prefs.background = "pink"
user.prefs.save
user.errors.empty? => false
user.errors.on(:prefs) => 'background' for has_easy('prefs') failed validation

user.prefs.midground = "black"
user.prefs.save
user.errors.on(:prefs)[0] => "msg1"
user.errors.on(:prefs)[1] => "msg2"

:preprocess

Alter the value before it goes through type checking and/or validation. This is useful when working with forms and boolean values. CAREFUL!! This option only applies to the underscore accessors, i.e. prefs_likes_cheese=, not prefs.likes_cheese= or prefs[:likes_cheese]=.

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :likes_cheese, :validate => [true, false],
                            :preprocess => Proc.new{ |value| ['true', 'yes'].include?(value) ? true : false }
  end
end

user.prefs.likes_cheese = 'yes' # :preprocess NOT invoked; it only applies to underscore accessors!!
user.prefs.likes_cheese
=> 'yes'
user.prefs.save! # exception, validation failed

user.prefs_likes_cheese = 'yes' # :preprocess invoked
user.prefs.likes_cheese
=> true
user.prefs.save! # no exception

:postprocess

Alter the value when it is read. This is useful when working with forms and boolean values. CAREFUL!! This option only applies to the underscore accessors, i.e. prefs_likes_cheese, not prefs.likes_cheese or prefs[:likes_cheese].

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :likes_cheese, :validate => [true, false],
                            :postprocess => Proc.new{ |value| value ? 'yes' : 'no' }
  end
end

user.prefs.likes_cheese = true
user.prefs.likes_cheese # :postprocess NOT invoked, it only applies to underscore accessors
=> true
user.prefs_likes_cheese # :postprocess invoked
=> 'yes'

Using with Forms

Suppose you have a has_easy field defined as a boolean and you want to use it with a checkbox in form_for.

(model)

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :likes_cheese, :type_check => [TrueClass, FalseClass],
                            :preprocess => Proc.new{ |value| value == 'yes' },
                            :postprocess => Proc.new{ |value| value ? 'yes' : 'no' }
  end
end

(view)

<% form_for(@user) do |f| %>
  <%= f.check_box 'user', 'prefs_likes_cheese', {}, 'yes', 'no' %> # invokes @user.prefs_likes_cheese which does the :postprocess
<% end %>

(controller)

@user.update_attributes(params[:user]) # invokes @user.prefs_likes_cheese= which does the :preprocess
@user.prefs.save
@user.prefs.likes_cheese
=> true or false
@user.prefs_likes_cheese # remember, only underscore accessors invoke the :preprocess and :postprocess options
=> 'yes' or 'no'

The general idea is that we make the form use prefs_likes_cheese= and prefs_likes_cheese accessors which in turn use the :preprocess and :postprocess options. Then in our normal code, we use prefs.likes_cheese or prefs[:likes_cheese] accessors to get our expected boolean values.

Missing Features

Autovivification

For when we want to use fields without having to define them first.

class User < ActiveRecord::Base
  has_easy :prefs, :autovivify => true do |p|
    p.define :likes_cheese, :default => 'yes'
  end
end

user.prefs.likes_cheese => 'yes'
user.prefs.likes_pizza => nil
user.prefs.likes_pizza = true
user.prefs.likes_pizza => true

Scoping to other models

Ehh, can’t think of a way to describe this other than example. Also, the syntax is completely up in the air, there are so many different ways to do it, I have no idea which way to go with. Please tell me your ideas.

class User < ActiveRecord::Base
  has_easy :prefs do |p|
    p.define :subscribed, :scoped => Post
    p.define :color, :scoped => [Car, Motorcycle] # polymorphic but must be Car or Motorcycle
    p.define :hair_color, :scoped => true # polymorphic no restrictions
    p.define :likes_cheese, :scoped => [Food, NilClass] # scoped and not scoped at the same time
  end
end

post = Post.find :first, :conditions => {:topic => 'rails'}
me.prefs.subscribed? :to => post
=> true

vette = Car.find :first, :conditions => {:model => 'corvette'}
me.prefs.color :for => vette
=> 'black'

gf = Girl.find :first, :conditions => {:name => 'aimee'}
me.prefs.hair_color :on => gf
=> 'brown'

watermelon = Food.find :first, :conditions => {:kind => 'watermelon'}
my.prefs.likes_cheese? # not scoped; do I like cheese in general?
=> true
my.prefs.likes_cheese? :on => watermelon # scoped; do I like cheese on watermelon?
=> false

Copyright © 2008 Christopher J. Bottaro <[email protected]>, released under the MIT license