diff --git a/lib/pliny.rb b/lib/pliny.rb index 9ee4652c..6fd6d11c 100644 --- a/lib/pliny.rb +++ b/lib/pliny.rb @@ -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" diff --git a/lib/pliny/range_parser.rb b/lib/pliny/range_parser.rb new file mode 100644 index 00000000..da4f7c4c --- /dev/null +++ b/lib/pliny/range_parser.rb @@ -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' + /(?\d*)-(?\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 diff --git a/spec/range_parser_spec.rb b/spec/range_parser_spec.rb new file mode 100644 index 00000000..d606b12e --- /dev/null +++ b/spec/range_parser_spec.rb @@ -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-' } + + 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