diff --git a/README.md b/README.md index c29ddfbd..f71862c0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/pliny.rb b/lib/pliny.rb index 0eff726e..e5390bc2 100644 --- a/lib/pliny.rb +++ b/lib/pliny.rb @@ -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" diff --git a/lib/pliny/commands/generator/base.rb b/lib/pliny/commands/generator/base.rb index 90b24729..47c3fb20 100644 --- a/lib/pliny/commands/generator/base.rb +++ b/lib/pliny/commands/generator/base.rb @@ -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 diff --git a/lib/pliny/commands/generator/endpoint.rb b/lib/pliny/commands/generator/endpoint.rb index 26aa92ce..16d2dff6 100644 --- a/lib/pliny/commands/generator/endpoint.rb +++ b/lib/pliny/commands/generator/endpoint.rb @@ -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:' diff --git a/lib/pliny/helpers/paginator.rb b/lib/pliny/helpers/paginator.rb new file mode 100644 index 00000000..9069abfc --- /dev/null +++ b/lib/pliny/helpers/paginator.rb @@ -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 diff --git a/lib/pliny/helpers/paginator/integer_paginator.rb b/lib/pliny/helpers/paginator/integer_paginator.rb new file mode 100644 index 00000000..dbae8784 --- /dev/null +++ b/lib/pliny/helpers/paginator/integer_paginator.rb @@ -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 diff --git a/lib/pliny/helpers/paginator/paginator.rb b/lib/pliny/helpers/paginator/paginator.rb new file mode 100644 index 00000000..f9525085 --- /dev/null +++ b/lib/pliny/helpers/paginator/paginator.rb @@ -0,0 +1,150 @@ +module Pliny::Helpers + module Paginator + class Paginator + SORT_BY = /(?\w+)/ + VALUE = /[^\.\s;\/]+/ + FIRST = /(?#{VALUE})/ + LAST = /(?#{VALUE})/ + COUNT = /(?:\/\d+)/ + 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] :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 diff --git a/lib/pliny/templates/endpoint_scaffold.erb b/lib/pliny/templates/endpoint_scaffold.erb index 8700ab49..66da8d43 100644 --- a/lib/pliny/templates/endpoint_scaffold.erb +++ b/lib/pliny/templates/endpoint_scaffold.erb @@ -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 diff --git a/lib/template/lib/endpoints/base.rb b/lib/template/lib/endpoints/base.rb index e745816c..80f76601 100644 --- a/lib/template/lib/endpoints/base.rb +++ b/lib/template/lib/endpoints/base.rb @@ -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 diff --git a/spec/helpers/paginator/integer_paginator_spec.rb b/spec/helpers/paginator/integer_paginator_spec.rb new file mode 100644 index 00000000..c53853b6 --- /dev/null +++ b/spec/helpers/paginator/integer_paginator_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Pliny::Helpers::Paginator::IntegerPaginator do + subject { described_class.new(sinatra, count, opts) } + let(:dummy_class) { Class.new { include Pliny::Helpers::Paginator } } + let(:sinatra) { dummy_class.new } + let(:count) { 4 } + let(:opts) { {} } + + before :each do + any_instance_of(Pliny::Helpers::Paginator::Paginator) do |klass| + stub(klass).request_options { {} } + stub(klass).set_headers + end + end + + describe '#run' do + it 'returns Hash' do + result = subject.run + + assert_kind_of Hash, result + + exp = + { + order_by: :id, + offset: nil, + limit: 200 + } + + assert_equal exp, result + end + end + + describe '#calculate_pages' do + let(:opts) { { first: 100, args: { max: 300 } } } + + describe 'when count < max in current range' do + let(:count) { 200 } + + it 'calculates :last' do + assert_equal 199, subject.calculate_pages[:last] + end + + it 'calculates :next_first' do + assert_equal nil, subject.calculate_pages[:next_first] + end + + it 'calculates :next_last' do + assert_equal nil, subject.calculate_pages[:next_last] + end + end + + describe 'when count > max in current range' do + let(:count) { 3000 } + + it 'calculates :last' do + assert_equal 399, subject.calculate_pages[:last] + end + + it 'calculates :next_first' do + assert_equal 400, subject.calculate_pages[:next_first] + end + + describe 'when count < max in next range' do + let(:count) { 600 } + + it 'calculates :next_last' do + assert_equal 599, subject.calculate_pages[:next_last] + end + end + + describe 'when count > max in next range' do + let(:count) { 3000 } + + it 'calculates :next_last' do + assert_equal 699, subject.calculate_pages[:next_last] + end + end + end + end +end diff --git a/spec/helpers/paginator/paginator_spec.rb b/spec/helpers/paginator/paginator_spec.rb new file mode 100644 index 00000000..241daebc --- /dev/null +++ b/spec/helpers/paginator/paginator_spec.rb @@ -0,0 +1,439 @@ +require 'spec_helper' + +describe Pliny::Helpers::Paginator::Paginator do + subject { described_class.new(sinatra, count, opts) } + let(:dummy_class) { Class.new { include Pliny::Helpers::Paginator } } + let(:sinatra) { dummy_class.new } + let(:count) { 4 } + let(:opts) { {} } + + describe '#run' do + it 'evaluates block' do + mock(subject).request_options { {} } + mock(subject).valid_options? { true } + mock(subject).set_headers + subject.instance_variable_set(:@options, args: { max: 200 }) + + result = + subject.run do |paginator| + paginator[:first] = 42 + end + + assert_equal 42, result + end + end + + describe '#request_options' do + it 'returns Hash' do + stub(sinatra).request do |klass| + stub(klass).env { { 'Range' => 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef; max=1000' } } + end + assert_kind_of Hash, subject.request_options + end + + describe 'when Range is nil' do + it 'returns empty Hash' do + stub(sinatra).request do |klass| + stub(klass).env { {} } + end + assert_kind_of Hash, subject.request_options + assert_empty subject.request_options + end + end + + describe 'when Range is an empty string' do + it 'returns empty Hash' do + stub(sinatra).request do |klass| + stub(klass).env { { 'Range' => '' } } + end + assert_kind_of Hash, subject.request_options + assert_empty subject.request_options + end + end + + describe 'when Range is valid' do + before :each do + stub(sinatra).request do |klass| + stub(klass).env do + { 'Range' => range } + end + end + end + + describe 'UUID' do + describe 'only sort_by' do + let(:range) { 'id' } + + it 'returns Hash' do + result = + { + sort_by: 'id' + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, first' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef' + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, args' do + let(:range) { 'id; max=1000' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + args: { max: '1000' } + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, count' do + let(:range) { 'id/400' } + + it 'returns Hash' do + result = + { + sort_by: 'id' + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, first, last' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + last: '01234567-89ab-cdef-0123-456789abcdef' + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, first, args' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef; max=1000' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + args: { max: '1000' } + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, first, count' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef/400' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef' + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, first, last, args' do + describe 'one arg' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef; max=1000' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + last: '01234567-89ab-cdef-0123-456789abcdef', + args: { max: '1000' } + } + assert_equal result, subject.request_options + end + end + + describe 'more args' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef; max=1000,order=desc' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + last: '01234567-89ab-cdef-0123-456789abcdef', + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + end + + describe 'only sort_by, first, last, args, count' do + describe 'one arg' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef/400; max=1000' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + last: '01234567-89ab-cdef-0123-456789abcdef', + args: { max: '1000' } + } + assert_equal result, subject.request_options + end + end + + describe 'more args' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef/400; max=1000,order=desc' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + last: '01234567-89ab-cdef-0123-456789abcdef', + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + end + + describe 'only sort_by, first, last, count' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef..01234567-89ab-cdef-0123-456789abcdef/400' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + last: '01234567-89ab-cdef-0123-456789abcdef' + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, first, args, count' do + let(:range) { 'id 01234567-89ab-cdef-0123-456789abcdef/400; max=1000,order=desc' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + first: '01234567-89ab-cdef-0123-456789abcdef', + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, args' do + let(:range) { 'id; max=1000,order=desc' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, args, count' do + let(:range) { 'id/400; max=1000,order=desc' } + + it 'returns Hash' do + result = + { + sort_by: 'id', + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + + describe 'only sort_by, count' do + let(:range) { 'id/400' } + + it 'returns Hash' do + result = + { + sort_by: 'id' + } + assert_equal result, subject.request_options + end + end + end + + describe 'timestamp in iso8601' do + let(:range) { "#{sort_by} #{first}..#{last}/400; max=1000,order=desc" } + let(:sort_by) { 'created_at' } + let(:first) { '1985-09-24T00:00:00+00:00' } + let(:last) { '2014-07-01T15:54:32+02:00' } + + it 'returns Hash' do + result = + { + sort_by: sort_by, + first: first, + last: last, + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + + describe 'fruits' do + let(:range) { "#{sort_by} #{first}..#{last}/400; max=1000,order=desc" } + let(:sort_by) { 'fruit' } + let(:first) { 'Apple' } + let(:last) { 'Banana' } + + it 'returns Hash' do + result = + { + sort_by: sort_by, + first: first, + last: last, + args: { max: '1000', order: 'desc' } + } + assert_equal result, subject.request_options + end + end + end + end + + describe '#build_range' do + it 'only sort_by' do + assert_equal 'id', + subject.build_range(:id, nil, nil, nil) + end + + it 'only sort_by, first' do + assert_equal 'id 100', + subject.build_range(:id, 100, nil, nil) + end + + it 'only sort_by, last' do + assert_equal 'id', + subject.build_range(:id, nil, 200, nil) + end + + it 'only sort_by, args' do + assert_equal 'id; max=300,order=desc', + subject.build_range(:id, nil, nil, max: 300, order: 'desc') + end + + it 'only sort_by, count' do + assert_equal 'id/1200', + subject.build_range(:id, nil, nil, nil, 1200) + end + + it 'only sort_by, first, last' do + assert_equal 'id 100..200', + subject.build_range(:id, 100, 200, nil) + end + + it 'only sort_by, first, args' do + assert_equal 'id 100; max=300,order=desc', + subject.build_range(:id, 100, nil, max: 300, order: 'desc') + end + + it 'only sort_by, first, count' do + assert_equal 'id 100/1200', + subject.build_range(:id, 100, nil, nil, 1200) + end + + it 'only sort_by, first, last, args' do + assert_equal 'id 100..200; max=300,order=desc', + subject.build_range(:id, 100, 200, max: 300, order: 'desc') + end + + it 'only sort_by, first, last, count' do + assert_equal 'id 100..200/1200', + subject.build_range(:id, 100, 200, nil, 1200) + end + + it 'only sort_by, first, args, count' do + assert_equal 'id 100/1200; max=300,order=desc', + subject.build_range(:id, 100, nil, { max: 300, order: 'desc' }, 1200) + end + + it 'only sort_by, args' do + assert_equal 'id; max=300,order=desc', + subject.build_range(:id, nil, nil, max: 300, order: 'desc') + end + + it 'only sort_by, args, count' do + assert_equal 'id/1200; max=300,order=desc', + subject.build_range(:id, nil, nil, { max: 300, order: 'desc' }, 1200) + end + + it 'only sort_by, count' do + assert_equal 'id/1200', + subject.build_range(:id, nil, nil, nil, 1200) + end + end + + describe '#will_paginate?' do + it 'converts max to integer' do + subject.instance_variable_set(:@options, args: { max: '1000' }) + stub(subject).count { 2000 } + assert subject.will_paginate? + end + end + + describe '#[]' do + describe 'allows to read #options with a convenience method' do + before :each do + mock(subject).options { { first: 1 } } + end + + it 'with symbol key' do + assert_equal subject[:first], 1 + end + + it 'with string key' do + assert_equal subject['first'], 1 + end + end + end + + describe '#[]=' do + describe 'allows to read #options with a convenience method' do + before :each do + subject.instance_variable_set(:@options, {}) + end + + it 'with symbol key' do + assert_equal nil, subject.options[:first] + subject[:first] = 1 + assert_equal 1, subject.options[:first] + end + + it 'with string key' do + assert_equal nil, subject.options[:first] + subject['first'] = 1 + assert_equal 1, subject.options[:first] + end + end + end +end diff --git a/spec/helpers/paginator_spec.rb b/spec/helpers/paginator_spec.rb new file mode 100644 index 00000000..06d6064c --- /dev/null +++ b/spec/helpers/paginator_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Pliny::Helpers::Paginator do + let(:dummy_class) { Class.new { include Pliny::Helpers::Paginator } } + let(:sinatra) { dummy_class.new } + + describe '#paginator' do + it 'calls Pagiantor.run' do + mock(Pliny::Helpers::Paginator::Paginator).run(sinatra, 12, sort_by: :foo) + sinatra.paginator(12, sort_by: :foo) + end + end + + describe '#uuid_paginator' do + it 'calls #paginator' do + resource = Class.new + stub(resource).count { 12 } + mock(sinatra).paginator(12, sort_by: :foo) + sinatra.uuid_paginator(resource, sort_by: :foo) + end + end + + describe '#integer_paginator' do + it 'calls IntegerPaginator.run' do + mock(Pliny::Helpers::Paginator::IntegerPaginator).run(sinatra, 12, sort_by: :foo) + sinatra.integer_paginator(12, sort_by: :foo) + end + end +end