mumboe / has_easy forked from swalterd/has_easy

Easy access and creation of "has many" relationships for ActiveRecord models. Use this plugin to add preferences, options, flags, etc to your models.

This URL has Read+Write access

has_easy / README.rdoc
100644 306 lines (243 sloc) 10.308 kb

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

 git clone git://github.com/cjbottaro/has_easy.git vendor/plugins/has_easy
 script/generate has_easy_migration create_has_easy_things
 rake db:migrate
 rake db:test:prepare
 cd vendor/plugins/has_easy
 rake test

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 <cjbottaro@alumni.cs.utexas.edu>, released under the MIT license