Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paginator helper #40

Closed
wants to merge 69 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
85063b2
Add paginator helper
tmaier Jun 17, 2014
4fb3df5
Set upper boundary to count in Next-Range
tmaier Jun 17, 2014
919e898
accepted_ranges must be an array
tmaier Jun 17, 2014
3818243
Add Paginator::Paginator class
tmaier Jun 29, 2014
04eb327
Remove semicolon
tmaier Jun 29, 2014
345d2cd
Don't convert start and end to integer
tmaier Jun 29, 2014
68cd04e
Halt with HTTP status code 416
tmaier Jun 29, 2014
95218ba
Add specs
tmaier Jun 29, 2014
d116a2c
Run accepts block
tmaier Jun 30, 2014
e8973df
Fix limit in #run hash
tmaier Jun 30, 2014
6453cd6
Add #will_paginate?
tmaier Jun 30, 2014
f095338
Rename options
tmaier Jun 30, 2014
10d8756
Fix regex
tmaier Jun 30, 2014
8488d97
Fix specs
tmaier Jun 30, 2014
ff8a729
Add #[] and #[]=
tmaier Jun 30, 2014
feaff92
Support for count in regex
tmaier Jun 30, 2014
303f600
Add test for #run block
tmaier Jun 30, 2014
40c1930
Improve test
tmaier Jun 30, 2014
1796fd8
Convert max to integer in #will_paginate?
tmaier Jun 30, 2014
7a0cbd0
Use assert
tmaier Jun 30, 2014
e6fec8c
Fix spelling error
tmaier Jul 1, 2014
218b812
Fix code style
tmaier Jul 1, 2014
1874a7b
Add #build_range
tmaier Jul 1, 2014
2b66b2b
Add block to #paginator
tmaier Jul 1, 2014
3e44ee5
Halt if :sort_by is nil
tmaier Jul 1, 2014
5b9358f
Move default options to initializer
tmaier Jul 1, 2014
c66fbd0
Rename res to options
tmaier Jul 1, 2014
6f969bc
Fix #set_headers
tmaier Jul 1, 2014
6043cb6
Remove optional argument
tmaier Jul 1, 2014
55baa68
Add tests for #calculate_pages
tmaier Jul 1, 2014
d8ffa92
Only add Next-Range header if there is a next range
tmaier Jul 1, 2014
4b6795f
Support more than just UUID and Integer in Range
tmaier Jul 1, 2014
7d9e023
Refactor "when Range is valid" tests
tmaier Jul 1, 2014
8333a2e
#run shall return result of block
tmaier Jul 1, 2014
0c1b87c
Rename keys in hash of #run
tmaier Jul 1, 2014
13da84f
Add pagination to endpoint scaffold
tmaier Jul 1, 2014
38c8b29
Fix test
tmaier Jul 1, 2014
1543e31
Fix next_last
tmaier Jul 1, 2014
c6c9b54
Add pagination to README
tmaier Jul 1, 2014
3a6e338
Fix syntax error
tmaier Jul 1, 2014
eaf896e
Require paginator
tmaier Jul 1, 2014
c59a853
Update default options
tmaier Jul 1, 2014
cfd461c
Skip calculate_pages when first is nil
tmaier Jul 1, 2014
733fb74
Revert "Update default options"
tmaier Jul 1, 2014
f84c32c
Convert public facing "id" to "uuid"
tmaier Jul 1, 2014
011303c
first shall default to nil
tmaier Jul 1, 2014
5f53eec
Introduce #uuid_paginator
tmaier Jul 1, 2014
888f6e0
Improve regex
tmaier Jul 1, 2014
e9b9069
Reorganize, add more tests
tmaier Jul 1, 2014
5a7a88a
Add more tests
tmaier Jul 1, 2014
a5e3004
Improve queries in #uuid_paginator
tmaier Jul 2, 2014
90295a6
Use options.merge
tmaier Jul 2, 2014
115367b
Improve code style
tmaier Jul 2, 2014
fc992eb
Use options.merge!
tmaier Jul 2, 2014
11098ef
Add all options to default hash
tmaier Jul 2, 2014
b622848
Add description group to test
tmaier Jul 2, 2014
6aa012e
Add #integer_paginator
tmaier Jul 2, 2014
dddb88a
Split paginator.rb file
tmaier Jul 2, 2014
3508c6c
Always apply limit
tmaier Jul 2, 2014
91c5425
Add test for #uuid_paginator
tmaier Jul 2, 2014
0a10645
Rename #validate_options to #valid_options?
tmaier Jul 6, 2014
8e503c4
Remove all the "magic" from #options
tmaier Jul 6, 2014
3fe462b
Improve endpoint scaffold
tmaier Jul 6, 2014
ef21f37
Add basic documentation to paginator methods
tmaier Aug 8, 2014
ee7e3e3
Fix indentation
tmaier Sep 4, 2014
a69f552
Add paginator to Endpoints::Base
tmaier Dec 1, 2014
40d232f
Fix call to fields_name in endpoint scaffold template
tmaier Dec 1, 2014
ec557b4
Move tests to spec directory
tmaier Dec 29, 2014
60d3bb3
Use described_class method in specs
tmaier Dec 29, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ And gems/helpers to tie these together and support operations:
- [Rollbar](https://www.rollbar.com/) for tracking exceptions
- [Log helper](spec/log_spec.rb) that logs in [data format](https://www.youtube.com/watch?v=rpmc-wHFUBs) [to stdout](https://adam.heroku.com/past/2011/4/1/logs_are_streams_not_files)
- [Mediators](http://brandur.org/mediator) to help encapsulate more complex interactions
- [Pagination with Ranges](lib/pliny/helpers/paginator.rb) to paginate large amount of data
- [Rspec](https://github.com/rspec/rspec) for lean and fast testing
- [Puma](http://puma.io/) as the web server, [configured for optimal performance on Heroku](lib/template/config/puma.rb)
- [Rack-test](https://github.com/brynary/rack-test) to test the API endpoints
Expand Down
3 changes: 3 additions & 0 deletions lib/pliny.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
require_relative "pliny/errors"
require_relative "pliny/extensions/instruments"
require_relative "pliny/helpers/encode"
require_relative "pliny/helpers/paginator"
require_relative "pliny/helpers/paginator/paginator"
require_relative "pliny/helpers/paginator/integer_paginator"
require_relative "pliny/helpers/params"
require_relative "pliny/log"
require_relative "pliny/request_store"
Expand Down
4 changes: 4 additions & 0 deletions lib/pliny/commands/generator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def field_name
name.tableize.singularize
end

def fields_name
name.tableize.pluralize
end

def pluralized_file_name
name.tableize
end
Expand Down
1 change: 1 addition & 0 deletions lib/pliny/commands/generator/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def create
plural_class_name: plural_class_name,
singular_class_name: singular_class_name,
field_name: field_name,
fields_name: fields_name,
url_path: url_path)
display "created endpoint file #{endpoint}"
display 'add the following to lib/routes.rb:'
Expand Down
68 changes: 68 additions & 0 deletions lib/pliny/helpers/paginator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module Pliny::Helpers
module Paginator

# Sets the HTTP Range header for pagination if necessary
#
# @see uuid_paginator
# @see integer_paginator
# @see Pliny::Helpers::Paginator::Paginator
def paginator(count, options = {}, &block)
Paginator.run(self, count, options, &block)
end

# paginator for UUID columns
#
# @example call in the Endpoint
# articles = uuid_paginator(Article, args: { max: 10 })
#
# @example HTTP header returned
# Content-Range: id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef/400; max=10
# Next-Range: id 76543210-89ab-cdef-0123-456789abcdef..76543210-89ab-cdef-0123-456789abcdef/400; max=10
#
# @param [Object] resource the resource to paginate
# @param [Hash] options
# @return [Object] modified resource (by order, limit and offset)
# @see paginator
def uuid_paginator(resource, options = {})
paginator(resource.count, options) do |paginator|
sort_by_conversion = { id: :uuid }
max = paginator[:args][:max].to_i
resources =
resource
.order(sort_by_conversion[paginator[:sort_by].to_sym])
.limit(max)

if paginator.will_paginate?
resources = resources.where { uuid >= Sequel.cast(paginator[:first], :uuid) } if paginator[:first]

paginator.options.merge! \
first: resources.get(:uuid),
last: resources.offset(max - 1).get(:uuid),
next_first: resources.offset(max).get(:uuid),
next_last: resources.offset(2 * max - 1).get(:uuid) || resources.select(:uuid).last.uuid
end

resources
end
end

# paginator for integer columns
#
# @example call in the Endpoint
# paginator = integer_paginator(User.count)
# users = User.order(paginator[:order_by]).limit(paginator[:limit]).offset(paginator[:offset])
#
# @example HTTP header returned
# Content-Range: id 0..199/400; max=200
# Next-Range: id 200..399/400; max=200
#
# @param [Integer] count the count of resources
# @param [Hash] options
# @return [Hash] with :order_by and calculated :offset and :limit
# @see paginator
# @see Pliny::Helpers::Paginator::IntegerPaginator
def integer_paginator(count, options = {})
IntegerPaginator.run(self, count, options)
end
end
end
56 changes: 56 additions & 0 deletions lib/pliny/helpers/paginator/integer_paginator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module Pliny::Helpers
module Paginator
class IntegerPaginator
attr_reader :sinatra, :count
attr_accessor :options

class << self
def run(*args, &block)
new(*args).run(&block)
end
end

def initialize(sinatra, count, options = {})
@sinatra = sinatra
@count = count
@options = options
end

def run
options = calculate_pages

{
order_by: options[:sort_by],
offset: options[:first],
limit: options[:args][:max]
}
end

def calculate_pages
Paginator.run(self, count, options) do |paginator|
max = paginator[:args][:max].to_i
paginator[:last] =
paginator[:first].to_i + max - 1

if paginator[:last] >= count - 1
paginator.options.merge! \
last: count - 1,
next_first: nil,
next_last: nil
else
paginator[:next_first] =
paginator[:last] + 1
paginator[:next_last] =
[
paginator[:next_first] + max - 1,
count - 1
]
.min
end

paginator.options
end
end
end
end
end
150 changes: 150 additions & 0 deletions lib/pliny/helpers/paginator/paginator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
module Pliny::Helpers
module Paginator
class Paginator
SORT_BY = /(?<sort_by>\w+)/
VALUE = /[^\.\s;\/]+/
FIRST = /(?<first>#{VALUE})/
LAST = /(?<last>#{VALUE})/
COUNT = /(?:\/\d+)/
ARGS = /(?<args>.*)/
RANGE = /\A#{SORT_BY}(?:\s+#{FIRST})?(\.{2}#{LAST})?#{COUNT}?(;\s*#{ARGS})?\z/

attr_accessor :options
attr_reader :sinatra, :count

class << self
def run(*args, &block)
new(*args).run(&block)
end
end

# Initializes an instance of Paginator
#
# @param [Sinatra::Base] the controller calling the paginator
# @param [Integer] count the count of resources
# @param [Hash] options for the paginator
# @option options [Array<Symbol>] :accepted_ranges ([:id]) fields allowed to sort the listing
# @option options [Symbol] :sort_by (:id) field to sort the listing
# @option options [String] :first ID or name of the first element of the current page
# @option options [String] :last ID or name of the last element of the current page
# @option options [String] :next_first ID or name of the first element of the next page
# @option options [String] :next_last ID or name of the last element of the next page
# @option options [Hash] :args ({max: 200}) arguments for the HTTP Range header
def initialize(sinatra, count, options = {})
@sinatra = sinatra
@count = count
@options =
{
accepted_ranges: [:id],
sort_by: :id,
first: nil,
last: nil,
next_first: nil,
next_last: nil,
args: { max: 200 }
}
.merge(options)
end

# executes the paginator and sets the HTTP headers if necessary
#
# @yieldparam paginator [Paginator]
# @yieldreturn [Object]
# @return [Object] the result of the block yielded
def run
options.merge!(request_options)

result = yield(self) if block_given?

halt unless valid_options?
set_headers

result
end

def request_options
range = sinatra.request.env['Range']
return {} if range.nil? || range.empty?

match =
RANGE.match(range)

match ? parse_request_options(match) : halt
end

def parse_request_options(match)
request_options = {}

[:sort_by, :first, :last].each do |key|
request_options[key] = match[key] if match[key]
end

if match[:args]
args =
match[:args]
.split(/\s*,\s*/)
.map do |value|
k, v = value.split('=', 2)
[k.to_sym, v]
end

request_options[:args] = Hash[args]
end

request_options
end

def valid_options?
options[:sort_by] && options[:accepted_ranges].include?(options[:sort_by].to_sym)
end

def halt
sinatra.halt(416)
end

def set_headers
sinatra.headers 'Accept-Ranges' => options[:accepted_ranges].join(',')

if will_paginate?
sinatra.status 206

cnt = build_range(options[:sort_by], options[:first], options[:last], options[:args], count)
sinatra.headers 'Content-Range' => cnt

if options[:next_first]
nxt = build_range(options[:sort_by], options[:next_first], options[:next_last], options[:args])
sinatra.headers 'Next-Range' => nxt
end
else
sinatra.status 200
end
end

def build_range(sort_by, first, last, args, count = nil)
range = sort_by.to_s
range << " #{[first, last].compact.join('..')}" if first
range << "/#{count}" if count
range << "; #{encode_args(args)}" if args
range
end

def encode_args(args)
args
.map { |key, value| "#{key}=#{value}" }
.join(',')
end

def will_paginate?
count > options[:args][:max].to_i
end

def [](key)
options[key.to_sym]
end

def []=(key, value)
options[key.to_sym] = value
end
end
end
end
3 changes: 2 additions & 1 deletion lib/pliny/templates/endpoint_scaffold.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ module Endpoints
end

get do
encode serialize(<%= singular_class_name %>.all)
<%= fields_name %> = uuid_paginator(<%= singular_class_name %>, args: { max: 10 })
encode serialize(<%= fields_name %>)
end

post do
Expand Down
1 change: 1 addition & 0 deletions lib/template/lib/endpoints/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Base < Sinatra::Base

helpers Pliny::Helpers::Encode
helpers Pliny::Helpers::Params
helpers Pliny::Helpers::Paginator

set :dump_errors, false
set :raise_errors, true
Expand Down
Loading