Skip to content

Commit

Permalink
Added support for automatic initialization using :initialize_with.
Browse files Browse the repository at this point in the history
Changed the public method name to simply be #find_param.
Refactored to remove dependence on class inheritable attributes.
Improved the documentation.
  • Loading branch information
Tyler Hunt committed May 29, 2008
1 parent 5d01367 commit 085b4bc
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 69 deletions.
60 changes: 49 additions & 11 deletions README
Original file line number Diff line number Diff line change
@@ -1,16 +1,54 @@
This is a silly plugin to let you take a shortcut. Here's the blog post example:
Find-Param
==========

class Post < ActiveRecord::Base
define_find_param :slug
end
This is a simple plugin to let you easily define a parameter to use for
#to_param, and also define a finder to access that same parameter.

This will set the to_param to be post.slug and add the following method to the Post model:
This plugin is most useful for situations where you want to override the usage
of ID as the default parameter value. Using a slug creates URLs that can be
friendlier for humans and bots alike.

def self.find_by_param(*args)
find_by_slug(*args)
end

You can also pass :raise_on_not_found => true to have it raise ActiveRecord::RecordNotFound
if the result set is empty.
Getting Started
---------------

Like I said, it's silly, but I find myself using this pattern a lot.
Here we define the find param to be a slug:

class Post < ActiveRecord::Base
find_param :slug
end

This will set the #to_param to be #slug and add a finder to the model called
#find_by_param that can be used in your controllers to fetch records using
params[:id].

class PostController
before_filter :find_post, :only => %(show edit update destroy)

def find_post
find_by_param(params[:id])
end
end


Options
-------

To have your find param automatically populated on record creation, use the
:initialize_with option.

class Post < ActiveRecord::Base
find_param :slug, :initialize_with => :title
end

This will use the title property to populate the slug. By default this value is
lowercased and all whitespace and special characters are replaced by hyphens.
To specify your own formatted for the find param, there a :using option that
accepts a proc.

class Post < ActiveRecord::Base
find_param :slug, :initialize_with => :title, :using => Proc.new { |value| value.upcase }
end

You can also pass :raise_on_not_found => true to have it raise an
ActiveRecord::RecordNotFound erorr when the result set is empty.
3 changes: 2 additions & 1 deletion init.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
require 'find_by_param'
ActiveRecord::Base.send(:extend, FindByParam::ClassMethods)

ActiveRecord::Base.send(:extend, FindByParam::ClassMethods)
73 changes: 41 additions & 32 deletions lib/find_by_param.rb
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
module FindByParam

##
# Catch-all error for any issue arrising within the FindByParam plugin.
#
class Error < RuntimeError; end

##
# Raised when the param requested is not in the model's table definition.
# For example:
#
# class WillRaiseError < ActiveRecord::Base
# define_find_param :undefined_column
# end

# Raised when the param requested is not in the model's table definition:
#
# class WillRaiseError < ActiveRecord::Base
# define_find_param :undefined_column
# end
class ColumnNotFoundError < Error; end

module ClassMethods
def define_find_param(param, options={})
param = param.to_s
options[:raise_on_not_found] ||= false
if column_names.include?(param)
write_inheritable_attribute :find_parameter, param
write_inheritable_attribute :find_param_options, options
bl = lambda do |args|
results = send("find_by_#{read_inheritable_attribute(:find_parameter)}", *args)
raise ActiveRecord::RecordNotFound if options[:raise_on_not_found] && (results.nil? or (results.is_a?(Array) && results.size == 0))
return results
# Defines a finder (#find_by_param) using the specified parameter name.
# Also, defines #to_param to use the same parameter.
#
# Options:
# * <tt>:raise_on_not_found</tt>: cases ActiveRecord::RecordNotFound to be raised when no record is found
# * <tt>:initialize_with</tt>: the name of parameter to use to initialize the find parameter when a new record is created
# * <tt>:using</tt>: a proc used to manipulated the initialization parameter before setting the find parameter
def find_param(param, options={})
raise_on_not_found = options.delete(:raise_on_not_found)
initialize_with = options.delete(:initialize_with)
raise ColumnNotFoundError unless column_names.include?(param.to_s)

self.class.send(:define_method, :find_by_param) do |args|
returning send("find_by_#{param}", *args) do |results|
raise ActiveRecord::RecordNotFound if raise_on_not_found && (results.nil? || (results.is_a?(Array) && results.size == 0))
end
self.class.send(:define_method, 'find_by_param', &bl)
else
raise ColumnNotFoundError
end
self.send(:include, FindByParam::InstanceMethods)

self.send(:alias_method, :to_param, param)

initialize_parameter(param, initialize_with, options) if initialize_with
end
end

module InstanceMethods
def to_param
self.send(self.class.read_inheritable_attribute(:find_parameter))

def initialize_parameter(param, source, options={})
raise ColumnNotFoundError unless column_names.include?(source.to_s)
using = options.delete(:using)

self.send(:define_method, :set_param) do
value = self.send(source)
value = using.respond_to?(:call) ? using.call(value) : value.downcase.gsub(/[^\w]+/, '-')

self.send("#{param}=", value)
end
self.send(:private, :set_param)

self.send(:before_create, :set_param)
end
private :initialize_parameter
end
end
end
63 changes: 45 additions & 18 deletions test/find_by_param_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,75 @@

class FindByParamTest < Test::Unit::TestCase
def setup
BlogPost.create(:slug => 'adam-west', :title => 'Adam West')
BlogPost.create(:slug => 'burt-ward', :title => 'Burt Ward')
BlogPost.send(:define_find_param, 'slug')
Post.create(:slug => 'adam-west', :title => 'Adam West')
Post.create(:slug => 'burt-ward', :title => 'Burt Ward')
Post.find_param(:slug)
end

def teardown
BlogPost.delete_all
Post.delete_all
end

def test_plugin_loaded_correctly
assert_kind_of FindByParam::ClassMethods, BlogPost
assert BlogPost.respond_to?(:find_by_param)
assert_kind_of FindByParam::ClassMethods, Post
end

def test_find_by_param_was_defined
assert Post.respond_to?(:find_by_param)
end

def test_find_by_param_is_defined_in_subclasses
assert Blog.respond_to?(:find_by_param)
end

def test_returns_valid_data
bp = BlogPost.find(:first, :conditions => 'slug = "adam-west"')
assert_equal BlogPost.find_by_param('adam-west'), bp
post = Post.find_by_param('adam-west')
assert_equal Post.find_by_slug('adam-west'), post
end

def test_can_define_find_parameter
BlogPost.send('define_find_param', 'title')
bp = BlogPost.find(:first, :conditions => {:slug => 'adam-west'})
assert_equal BlogPost.find_by_param('Adam West'), bp
def test_can_define_find_parameter_with_symbol
Post.find_param(:title)
post = Post.find_by_param('Adam West')
assert_equal Post.find_by_title('Adam West'), post
end

def test_can_define_find_parameter_with_string
Post.find_param('title')
post = Post.find_by_param('Adam West')
assert_equal Post.find_by_title('Adam West'), post
end

def test_correctly_goes_to_param
bp = BlogPost.find(:first, :conditions => {:slug => 'adam-west'})
assert_equal bp.to_param, 'adam-west'
post = Post.find(:first, :conditions => { :slug => 'adam-west' })
assert_equal 'adam-west', post.to_param
end

def test_raises_on_not_found_if_specified
BlogPost.send(:define_find_param, 'slug', :raise_on_not_found => true)
Post.find_param('slug', :raise_on_not_found => true)
assert_raises ActiveRecord::RecordNotFound do
BlogPost.find_by_param('in ur tests, failing')
Post.find_by_param('non-existent-slug')
end
end

def test_raises_column_not_found_error_when_given_undefined_column
assert_raise(FindByParam::ColumnNotFoundError) do
BlogPost.send(:define_find_param, 'bad_column_name')
Post.find_param(:bad_column_name)
end
end

def test_column_not_found_error_is_a_find_by_param_error
assert_kind_of FindByParam::Error, FindByParam::ColumnNotFoundError.new
end

end
def test_initializes_param_on_create
Post.find_param(:slug, :initialize_with => :title)
blog_post = Post.create(:title => 'A Test Post')
assert_equal 'a-test-post', blog_post.slug
end

def test_initializes_param_on_create_using_a_custom_initializer
Post.find_param(:slug, :initialize_with => :title, :using => Proc.new { |value| value.upcase.gsub(/\s+/, '_') })
blog_post = Post.create(:title => 'A Test Post')
assert_equal 'A_TEST_POST', blog_post.slug
end
end
13 changes: 6 additions & 7 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$LOAD_PATH.unshift 'lib/'
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')

require 'rubygems'
require 'multi_rails_init'
Expand All @@ -15,17 +15,16 @@
RAILS_ROOT = '.' unless defined? RAILS_ROOT
RAILS_ENV = 'test' unless defined? RAILS_ENV


ActiveRecord::Base.send(:extend, FindByParam::ClassMethods)
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :dbfile => ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define(:version => 1) do
create_table :blog_posts do |t|
t.column :slug, :string
create_table :posts do |t|
t.column :title, :string
t.column :slug, :string
end
end

class BlogPost < ActiveRecord::Base
end
class Post < ActiveRecord::Base ; end
class Blog < Post ; end

0 comments on commit 085b4bc

Please sign in to comment.