Skip to content
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