Attribute Filters for Rails

attribute-filters version 2.0 (Beard)

Summary

Attribute Filters extension adds couple of DSL keywords and some syntactic sugar to Rails, thereby allowing you to express filtering and grouping model attributes in a concise and clean way.

If you are a fan of declarative style, this gem is for you. It lets you program your models in a way that it's clear to see what's going on with attributes just by looking at the top of their classes.

When?

You may want to try it when your Rails application often modifies attribute values that changed recently and uses callbacks to do that.

When the number of attributes that are altered in such a way increases, you can observe that the same thing is happening with your filtering methods. That's because each one is tied to some attribute.

To refine that process you may write more generic methods for altering attributes. They should be designed to handle common operations and not be tied to certain attributes. Attribute Filters helps you do that.

Let's see that in action.

Before

class User < ActiveRecord::Base

  before_validation :strip_and_downcase_username
  before_validation :strip_and_downcase_email
  before_validation :strip_and_capitalize_real_name

  def strip_and_downcase_username
    if username.present?
      self.username = self.username.strip.mb_chars.downcase.to_s
    end
  end

  def strip_and_downcase_email
    if email.present?
      self.email.strip!
      self.email.downcase!
    end
  end

  def strip_and_capitalize_real_name
    if real_name.present?
      self.real_name = self.real_name.strip.mb_chars.split(' ').
                        map { |n| n.capitalize }.join(' ')
    end
  end  
end

The more attributes there is the more messy it becomes. The filtering code is not reusable since it operates on specific attributes.

After

class User < ActiveRecord::Base
  include ActiveModel::AttributeFilters::Common

  strips_attributes    :username, :email, :real_name
  downcases_attributes :username, :email
  titleizes_attribute  :real_name

  before_validation :filter_attributes
end

or if you want more control:

class User < ActiveRecord::Base
  include ActiveModel::AttributeFilters::Common::Strip
  include ActiveModel::AttributeFilters::Common::Downcase
  include ActiveModel::AttributeFilters::Common::Titleize  

  has_attributes_that should_be_stripped:   [ :username, :email, :real_name ]
  has_attributes_that should_be_downcased:  [ :username, :email ]
  has_attributes_that should_be_titleized:  [ :real_name ]

  before_validation :strip_attributes
  before_validation :downcase_attributes
  before_validation :titleize_attributes
end

or if you would like to create filtering methods on your own:

class User < ActiveRecord::Base
  has_attributes_that should_be_stripped:     [ :username, :email, :real_name ]
  has_attributes_that should_be_downcased:    [ :username, :email ]
  has_attributes_that should_be_capitalized:  [ :real_name ]

  before_validation :filter_attributes

  has_filtering_method :downcase_names, :should_be_downcased
  has_filtering_method :capitalize,     :should_be_capitalized
  has_filtering_method :strip_names,    :should_be_stripped

  def downcase_names
    filter_attributes_that :should_be_downcased do |atr|
      atr.mb_chars.downcase.to_s
    end
  end

  def capitalize_names
    filter_attributes_that :should_be_capitalized do |atr|
      atr.mb_chars.split(' ').map { |n| n.capitalize }.join(' ')
    end
  end

  def strip_names
    for_attributes_that(:should_be_stripped) { |atr| atr.strip! }
  end
end

Attributes that should be altered may be simply added to the attribute sets that you define and then filtered with generic methods that operate on that sets. You can share these methods across all of your models by putting them into your own handy module that will be included in models that need it.

Alternatively (as presented at the beginning) you can use predefined filtering methods from ActiveModel::AttributeFilters::Common module and its submodules. They contain filters for changing the case, stripping, squishng, squeezing, joining, splitting and more.

If you would rather like to group filters by attribute names while registering them then the alternative syntax may be helpful:

class User < ActiveRecord::Base
  its_attribute email:        [ :should_be_stripped, :should_be_downcased   ]
  its_attribute username:     [ :should_be_stripped, :should_be_downcased   ]
  its_attribute real_name:    [ :should_be_stripped, :should_be_capitalized ]
end

Tracking changes and filtering virtual attributes is also easy:

class User < ActiveRecord::Base
  include ActiveModel::AttributeFilters::Common::Split

  split_attribute   :real_name => [ :first_name, :last_name ]
  before_validation :filter_attributes

  attr_virtual  :real_name
  attr_writer   :real_name

  def real_name
    @real_name ||= "#{first_name} #{last_name}"
  end
end

Usage and more examples

You can use Attribute Filters to filter attributes (as presented above) but you can also use it to express some logic on your own.

  • See USAGE for examples and detailed information about the usage.
  • See COMMON-FILTERS for examples and detailed description of common filters.
  • See whole documentation to browse all documents.

Sneak peeks

  @user.is_the_attribute.username.required_to_make_deals?
  # => true

  @user.the_attribute(:username).should_be_stripped?
  # => true

  @user.is_the_attribute(:username).accessible?
  # => true

  @user.is_the_attribute.username.protected?
  # => false

  @user.has_the_attribute.username.changed?
  # => false

  @user.is_the_attribute.username.valid?
  # => true

  @user.are_attributes_that.should_be_stripped.all.present?
  # => false

  @user.from_attributes_that.should_be_stripped.list.blank?
  # => #<ActiveModel::AttributeSet: {"unconfirmed_email"}> 

  @user.the_attribute(:username).list.sets
  # => {:should_be_downcased=>true, :should_be_stripped=>true}

  @user.attributes_that.should_be_stripped.list.present?
  # => #<ActiveModel::AttributeSet: {"username", "email"}>

  @user.all_attributes.list.valid?
  # => #<ActiveModel::AttributeSet: {"username", "email"}>

  @user.all_attributes.list.changed?
  # => #<ActiveModel::AttributeSet: {"username"}>

  @user.all_attributes.any.changed?
  # => true

How it works?

It creates a new Active Model submodule called AttributeFilters. That module contains the needed DSL that goes into your models. It also creates ActiveModel::AttributeSet and ActiveModel::MetaSet classes which are just new kinds of hash used for storing attribute names and attribute set names.

Then it forces Rails to include the ActiveModel::AttributeFilters in any model that will at any time include ActiveModel::AttributeMethods. The last one is included quite often; many popular ORM-s use it. (I'm calling that thechnique "the accompanying module".)

It all should work out of the box with ActiveModel. However, if your application is somehow unusua (for example you don't need to include ActiveModel::AttributeMethods) you can always include the AttributeFilters module manually in any model or entity:

class User
  include ActiveModel::AttributeFilters
end

Requirements

Download

Source code

Gem

Installation

gem install attribute-filters

Specs

You can run RSpec examples both with

  • bundle exec rake spec or just bundle exec rake
  • run a test file directly, e.g. ruby -S rspec spec/attribute-filters_spec.rb -Ispec:lib

Common rake tasks

  • bundle exec rake bundler:gemfile – regenerate the Gemfile
  • bundle exec rake docs – render the documentation (output in the subdirectory directory doc)
  • bundle exec rake gem:spec – builds static gemspec file (attribute-filters.gemspec)
  • bundle exec rake gem – builds package (output in the subdirectory pkg)
  • bundle exec rake spec – performs spec. tests
  • bundle exec rake Manifest.txt – regenerates the Manifest.txt file
  • bundle exec rake ChangeLog – regenerates the ChangeLog file

Credits

  • iConsulting supports Free Software and has contributed to this library by paying for my food during the coding.
  • MrZYX (Jonne Haß) contributed by giving me some hints and answering basic questions on IRC – THX!
  • Robert Pankowecki contributed by suggesting selective inclusion of filtering helpers.

Like my work?

You can send me some bitcoins if you would like to support me:

  • 13wZbBjs6yQQuAb3zjfHubQSyer2cLAYzH

Or you can endorse my skills on LinkedIn or Coderwall:

License

Copyright (c) 2012 by Paweł Wilk.

attribute-filters is copyrighted software owned by Paweł Wilk ([email protected]). You may redistribute and/or modify this software as long as you comply with either the terms of the LGPL (see LGPL-LICENSE), or Ruby's license (see COPYING).

THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.