Plugin to add or chain filters to several ActiveRecord attributes setters at a time.
Ruby
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
lib
test
MIT-LICENSE
README.md
init.rb

README.md

ar_setter_filter

Plugin to add or chain filters to several ActiveRecord attributes setters at a time.

Usage

If no selectors are specified, all of the attributes will apply the given filter/s on their setters:

setter_filter [:first_filter, :second_filter] # applied to all attributes

You can select some attributes with :only or :except

setter_filter [:first_filter, :second_filter], :only => [:first_name, :last_name]
setter_filter [:other, :different, :filters], :except => [:bio]

You can chain several filters to the same attribute/s with different setter_filter calls:

setter_filter [:a, :b], :only => [:name, :website]
setter_filter [:c, :d, :e], :only => [:website, :bio, :birthdate] 
# website's setter will apply all filters following their declaration order: a,b,c,d,e

You can also select the attributes by their column types:

setter_filter [:a, :b], :only_types => [:string, :text]
setter_filter [:c, :d, :e], :except_types => [:integer, :boolean, :datetime] 

A filter can be any instance method which follows this convention:

def my_gorgeous_filter(attrib_name, new_value)
  # do whatever you want
  # Just remember to return the desired new_value, 
  # as it will be the incoming value for the next filter.
  # The value returned by the last filter will be stored on the database.
end

It's important to return a value which will be passed in turn to the next filter. The value returned by the last filter will be the actual value stored on the database.

Let's see a simple example:

setter_filter [:downcase, :remove_vowels], :only => [:example]

def downcase(attrib_name, new_value)   # if new_value = "FOO"
  new_value.downcase                   # will return "foo"
end

def remove_vowels(attrib_name, new_value)  # will receive "foo"
  new_value.gsub(/[aeiou]/,'')             # will return "f", to be stored on the db
end

Please note that setter_filter never will be applied to non-content columns (eg: id, xxx_id, xxx_count or STI-involved attributes, see content_columns).

Motivation

I usually need to filter some attributes values before storing them, specially when it comes to string or text fields: cleaning HTML, applying Markdown/bbcode/whatever, adding http:// to URLs if necessary, updating related fields at the same time, etc.

The best way to get it done is overloading the attribute setter, but we can't use alias_method_chain here. Fortunately, there seems to be a common, easy pattern to apply one filter to one attribute's setter:

def name=(new_name)
  # your filter here! (For example you can modify new_name before storing it)
  write_attribute :name, new_name # the value is actually stored on db here
end

This is really nice if you just need to apply one filter to one attribute at a time. There are some plugins which make use of this pattern on several useful ways. For example, I like to sanitize all my text fields before storing them, so instead of writing the previous code to each attribute I use a plugin to do something like:

class Project < ActiveRecord::Base

  sanitize_html :only => [:name, :client, :client_url, :company, :company_url]

end

The plugin declares a sanitize_html class method which applies the pattern to the selected attributes. Pretty cool!

But today I need to apply one more filter to several attributes. It's a very simple filter, I just need to format the URLs to follow some conventions (i.e: add the 'http://' if the user missed it, etc).

I could have it working with:

sanitize_html :only => [:name, :client, :company]

def company_url=
  # look for the plugin's sanitizing method to invoke it,
  # and now apply my own format_url filter
  write_attribute :company_url
end

def client_url=
  # look for the plugin's sanitizing method to invoke it,
  # and now apply my own format_url filter
  write_attribute :client_url
end

...though it's not very DRY. I could try to improve it:

[:company_url, :client_url].each do |attribute|
  # define a setter following the last examples
end

...but I actually don't like it.

I could also create a new plugin to customize the behaviour and call it sanitize_html_and_another_filter...hmpf.

I've realized that what I really would like to write is something like:

setter_filter [:sanitize_html]
setter_filter [:another_filter], :only => [client_url, :company_url]

and be sure that the given filter/s will be applied in the same order as they are declared.

...so that's why I wrote this plugin. But hey, I don't want to reinvent the wheel: if you know a better solution please contact me!

Questions, suggestions, bug reports...

Feedback, questions and comments will be always welcome at raul@murciano.net

Credits

License

Copyright (c) 2009 Released under the MIT license (see MIT-LICENSE)
Raul Murciano <http://raul.murciano.net>  
Domestika INTERNET S.L. <http://domestika.org>