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

Pagination using Range header #234

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/pliny.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative "pliny/helpers/encode"
require_relative "pliny/helpers/params"
require_relative "pliny/log"
require_relative "pliny/range_parser"
require_relative "pliny/request_store"
require_relative "pliny/router"
require_relative "pliny/utils"
Expand Down
55 changes: 55 additions & 0 deletions lib/pliny/range_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module Pliny
class RangeParser
attr_reader :range_header
attr_reader :start, :end, :parameters

RANGE_FORMAT_ERROR = 'Invalid `Range` header. Please use format like `objects 0-99; sort=name, order=desc`.'.freeze

def initialize(range_header)
@range_header = range_header

set_defaults
return if range_header.nil?
parse
end

private

def parse
parts = range_header.split(';')
raise_range_format_error if parts.size > 2
bounds_str, parameters_str = parts
parse_range_bounds(bounds_str)
parse_range_parameters(parameters_str)
end

def parse_range_bounds(bounds_str)
return if bounds_str.nil?
unit, bounds = bounds_str.split(/\s+/, 2)
raise_range_format_error unless unit.downcase == 'objects'
/(?<start_bound>\d*)-(?<end_bound>\d*)/ =~ bounds
@start = start_bound.to_i unless start_bound.empty?
@end = end_bound.to_i unless end_bound.empty?
end

def parse_range_parameters(parameters_str)
return if parameters_str.nil?
@parameters = Hash[
parameters_str.split(',')
.map { |option| option.split('=') }
.select { |k, v| k && v }
.map { |k, v| [k.strip.to_sym, v.strip] }
]
end

def raise_range_format_error
fail Pliny::Errors::BadRequest, RANGE_FORMAT_ERROR
end

def set_defaults
@start = nil
@end = nil
@parameters = {}
end
end
end
75 changes: 75 additions & 0 deletions spec/range_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
require "spec_helper"

describe Pliny::RangeParser do
subject(:parser) { described_class.new(range_header) }

context 'with an empty header' do
let(:range_header) { nil }

it 'parses' do
assert_nil parser.start
assert_nil parser.end
assert_equal({}, parser.parameters)
end
end

context 'with a bound range' do
let(:range_header) { 'objects 0-99' }

it 'parses a start and an end' do
assert_equal 0, parser.start
assert_equal 99, parser.end
assert_equal({}, parser.parameters)
end
end

context 'with an unbound start range' do
let(:range_header) { 'objects -99' }

it 'parses a start' do
assert_nil parser.start
assert_equal 99, parser.end
assert_equal({}, parser.parameters)
end
end

context 'with an unbound end range' do
let(:range_header) { 'objects 0-' }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geemus Do you think unbound ranges should be allowed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gudmundur I think they are in the API already!

$ curl -H "Range: id ]006e2c53-e3a9-4152-851c-abf6e9991c63..; max=1" -H "Accept: application/vnd.heroku+json; version=3" -n https://api.heroku.com/apps
[
  {
    "archived_at":null,
...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not absolutely required, but certainly nice to have.


it 'parses a start' do
assert_equal 0, parser.start
assert_nil parser.end
assert_equal({}, parser.parameters)
end
end

context 'with parameters' do
let(:range_header) { 'objects 0-99; a=b, c=d' }

it 'parses parameters' do
assert_equal({ a: 'b', c: 'd' }, parser.parameters)
end
end

context 'with multiple semicolons' do
let(:range_header) { 'objects 0-99; a=b; c=d' }
let(:message) { Pliny::RangeParser::RANGE_FORMAT_ERROR }

it 'raises a bad request' do
assert_raises Pliny::Errors::BadRequest, message do
parser
end
end
end

context 'with a non objects unit' do
let(:range_header) { 'ids 0-99' }
let(:message) { Pliny::RangeParser::RANGE_FORMAT_ERROR }

it 'raises a bad request' do
assert_raises Pliny::Errors::BadRequest, message do
parser
end
end
end
end