Skip to content

Commit

Permalink
represent the concept of page number with the PageNumber class
Browse files Browse the repository at this point in the history
It handles validation of numbers and calculating the offset value.
  • Loading branch information
mislav committed Aug 7, 2011
1 parent 9ede214 commit 89cf147
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 53 deletions.
10 changes: 6 additions & 4 deletions lib/will_paginate/active_record.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'will_paginate/per_page'
require 'will_paginate/page_number'
require 'will_paginate/collection'
require 'active_record'

Expand Down Expand Up @@ -30,7 +31,7 @@ def per_page(value = nil)
def limit(num)
rel = super
if rel.current_page
rel.offset ::WillPaginate.calculate_offset(rel.current_page, rel.limit_value)
rel.offset rel.current_page.to_offset(rel.limit_value).to_i
else
rel
end
Expand Down Expand Up @@ -117,7 +118,7 @@ def paginate(options)
count_options = options.delete(:count)
options.delete(:page)

rel = limit(per_page).page(pagenum)
rel = limit(per_page.to_i).page(pagenum)
rel = rel.apply_finder_options(options) if options.any?
rel.wp_count_options = count_options if count_options
rel.total_entries = total.to_i unless total.blank?
Expand All @@ -126,8 +127,9 @@ def paginate(options)

def page(num)
rel = scoped.extending(RelationMethods)
pagenum, per_page, offset = ::WillPaginate.process_values(num, rel.limit_value || self.per_page)
rel = rel.offset(offset)
pagenum = ::WillPaginate::PageNumber(num.nil? ? 1 : num)
per_page = rel.limit_value || self.per_page
rel = rel.offset(pagenum.to_offset(per_page).to_i)
rel = rel.limit(per_page) unless rel.limit_value
rel.current_page = pagenum
rel
Expand Down
5 changes: 3 additions & 2 deletions lib/will_paginate/collection.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'will_paginate/per_page'
require 'will_paginate/page_number'

module WillPaginate
# = The key to pagination
Expand All @@ -24,7 +25,7 @@ class Collection < Array
# is best to do lazy counting; in other words, count *conditionally* after
# populating the collection using the +replace+ method.
def initialize(page, per_page = WillPaginate.per_page, total = nil)
@current_page = InvalidPage.validate(page, 'page')
@current_page = WillPaginate::PageNumber(page)
@per_page = per_page.to_i
self.total_entries = total if total
end
Expand Down Expand Up @@ -74,7 +75,7 @@ def out_of_bounds?
# the offset is 30. This property is useful if you want to render ordinals
# side by side with records in the view: simply start with offset + 1.
def offset
(current_page - 1) * per_page
@current_page.to_offset(per_page).to_i
end

# current_page - 1 or nil if there is no previous page
Expand Down
8 changes: 5 additions & 3 deletions lib/will_paginate/data_mapper.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
require 'dm-core'
require 'dm-aggregates'
require 'will_paginate/per_page'
require 'will_paginate/page_number'
require 'will_paginate/collection'

module WillPaginate
module DataMapper
module Pagination
def page(num)
pagenum, per_page, offset = ::WillPaginate.process_values(num, query.limit || self.per_page)
options = {:offset => offset}
pagenum = ::WillPaginate::PageNumber(num.nil? ? 1 : num)
per_page = query.limit || self.per_page
options = {:offset => pagenum.to_offset(per_page).to_i}
options[:limit] = per_page unless query.limit
col = new_collection(query.merge(options))
col.current_page = pagenum
Expand All @@ -21,7 +23,7 @@ def paginate(options)
per_page = options.delete(:per_page) || self.per_page

options.delete(:page)
options[:limit] = per_page
options[:limit] = per_page.to_i

all(options).page(pagenum)
end
Expand Down
57 changes: 57 additions & 0 deletions lib/will_paginate/page_number.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require 'delegate'
require 'forwardable'

module WillPaginate
# a module that page number exceptions are tagged with
module InvalidPage; end

# integer representing a page number
class PageNumber < DelegateClass(Integer)
# a value larger than this is not supported in SQL queries
BIGINT = 9223372036854775807

extend Forwardable

def initialize(value, name)
value = Integer(value)
if 'offset' == name ? (value < 0 or value > BIGINT) : value < 1
raise RangeError, "invalid #{name}: #{value.inspect}"
end
@name = name
super(value)
rescue ArgumentError, TypeError, RangeError => error
error.extend InvalidPage
raise error
end

alias_method :to_i, :__getobj__

def inspect
"#{@name} #{to_i}"
end

def to_offset(per_page)
PageNumber.new((to_i - 1) * per_page.to_i, 'offset')
end

def kind_of?(klass)
super || to_i.kind_of?(klass)
end
alias is_a? kind_of?
end

# Ultrahax: makes `Fixnum === current_page` checks pass
Numeric.extend Module.new {
def ===(obj)
obj.instance_of? PageNumber or super
end
}

# An idemptotent coercion method
def self.PageNumber(value, name = 'page')
case value
when PageNumber then value
else PageNumber.new(value, name)
end
end
end
36 changes: 0 additions & 36 deletions lib/will_paginate/per_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,4 @@ def inherited(subclass)

# default number of items per page
self.per_page = 30

# these methods are used internally and are subject to change
module Calculation
def process_values(page, per_page)
page = page.nil? ? 1 : InvalidPage.validate(page, 'page')
per_page = per_page.to_i
offset = calculate_offset(page, per_page)
[page, per_page, offset]
end

def calculate_offset(page, per_page)
InvalidPage.validate((page - 1) * per_page, 'offset')
end
end

extend Calculation

# Raised by paginating methods in case `page` parameter is an invalid number.
#
# In Rails this error is automatically handled as 404 Not Found.
class InvalidPage < ArgumentError
# the maximum value for SQL BIGINT
BIGINT = 9223372036854775807

# Returns value cast to integer, raising self if invalid
def self.validate(value, name)
num = value.to_i
rescue NoMethodError
raise self, "#{name} cannot be converted to integer: #{value.inspect}"
else
if 'offset' == name ? (num < 0 or num > BIGINT) : num < 1
raise self, "invalid #{name}: #{value.inspect}"
end
return num
end
end
end
8 changes: 0 additions & 8 deletions spec/collection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,6 @@
end
end

it "should raise WillPaginate::InvalidPage on invalid input" do
[0, -1, nil, '', 'Schnitzel'].each do |bad_input|
lambda {
create bad_input
}.should raise_error(WillPaginate::InvalidPage, "invalid page: #{bad_input.inspect}")
end
end

it "should not respond to page_count anymore" do
Proc.new { create.page_count }.should raise_error(NoMethodError)
end
Expand Down
65 changes: 65 additions & 0 deletions spec/page_number_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require 'spec_helper'
require 'will_paginate/page_number'

describe WillPaginate::PageNumber do
describe "valid" do
subject { described_class.new('12', 'page') }

it { should eq(12) }
its(:inspect) { should eq('page 12') }
it { should be_a(WillPaginate::PageNumber) }
it { should be_instance_of(WillPaginate::PageNumber) }
it { should be_a(Numeric) }
it { should be_a(Fixnum) }
it { should_not be_instance_of(Fixnum) }

it "passes the PageNumber=== type check" do |variable|
(WillPaginate::PageNumber === subject).should be
end

it "passes the Numeric=== type check" do |variable|
(Numeric === subject).should be
(Fixnum === subject).should be
end
end

describe "invalid" do
def create(value, name = 'page')
described_class.new(value, name)
end

it "errors out on non-int values" do
lambda { create(nil) }.should raise_error(WillPaginate::InvalidPage)
lambda { create('') }.should raise_error(WillPaginate::InvalidPage)
lambda { create('Schnitzel') }.should raise_error(WillPaginate::InvalidPage)
end

it "errors out on zero or less" do
lambda { create(0) }.should raise_error(WillPaginate::InvalidPage)
lambda { create(-1) }.should raise_error(WillPaginate::InvalidPage)
end

it "doesn't error out on zero for 'offset'" do
lambda { create(0, 'offset') }.should_not raise_error
lambda { create(-1, 'offset') }.should raise_error(WillPaginate::InvalidPage)
end
end

describe "coercion method" do
it "defaults to 'page' name" do
num = WillPaginate::PageNumber(12)
num.inspect.should eq('page 12')
end

it "accepts a custom name" do
num = WillPaginate::PageNumber(12, 'monkeys')
num.inspect.should eq('monkeys 12')
end

it "doesn't affect PageNumber instances" do
num = WillPaginate::PageNumber(12)
num2 = WillPaginate::PageNumber(num)
num2.object_id.should eq(num.object_id)
end
end
end
9 changes: 9 additions & 0 deletions spec/per_page_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ class MyModel
end
end

it "casts values to int" do
WillPaginate.per_page = '10'
begin
MyModel.per_page.should == 10
ensure
WillPaginate.per_page = 30
end
end

it "has an explicit value" do
MyModel.per_page = 12
begin
Expand Down

0 comments on commit 89cf147

Please sign in to comment.