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 = 'red' user.preferences # => '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=. 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. 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 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 (c) 2008 Christopher J. Bottaro [email protected], released under the MIT license