diff --git a/spec/negotiator_spec.cr b/spec/negotiator_spec.cr index 296a59a..3c5e59b 100644 --- a/spec/negotiator_spec.cr +++ b/spec/negotiator_spec.cr @@ -7,7 +7,18 @@ struct NegotiatorTest < ASPEC::TestCase @negotiator = ANG::Negotiator.new end - def test_exception_handling : Nil + def test_best_respects_quality : Nil + accept = @negotiator.best "text/html,text/*;q=0.7", {"text/html;q=0.5", "text/plain;q=0.9"} + accept = accept.should_not be_nil + accept.should be_a ANG::Accept + accept.type.should eq "text/plain" + end + + def test_best_invalid_unstrict + @negotiator.best("/qwer", {"foo/bar"}, false).should be_nil + end + + def test_best_exception_handling : Nil ex = expect_raises ANG::Exceptions::InvalidMediaType, "Invalid media type: '/qwer'." do @negotiator.best "foo/bar", {"/qwer"} end @@ -90,7 +101,7 @@ struct NegotiatorTest < ASPEC::TestCase # IE8 {"image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, */*", {"text/html", "application/xhtml+xml"}, {"text/html", nil}}, - # wildcards with '+' + # wildcards with `+` {"application/vnd.api+json", {"application/json", "application/*+json"}, {"application/*+json", nil}}, {"application/json;q=0.7, application/*+json;q=0.7", {"application/hal+json", "application/problem+json"}, {"application/hal+json", nil}}, {"application/json;q=0.7, application/problem+*;q=0.7", {"application/hal+xml", "application/problem+xml"}, {"application/problem+xml", nil}}, @@ -98,4 +109,29 @@ struct NegotiatorTest < ASPEC::TestCase {"application/hal+json", {"application/ld+json", "application/hal+json", "application/xml", "text/xml", "application/json", "text/html"}, {"application/hal+json", nil}}, } end + + def test_ordered_elements_exception_handling : Nil + expect_raises ArgumentError, "The header string should not be empty." do + @negotiator.ordered_elements "" + end + end + + @[DataProvider("test_ordered_elements_data_provider")] + def test_ordered_elements(header : String, expected : Indexable(String)) : Nil + elements = @negotiator.ordered_elements header + + expected.each_with_index do |element, idx| + elements[idx].should be_a ANG::Accept + element.should eq elements[idx].value + end + end + + def test_ordered_elements_data_provider : Tuple + { + {"/qwer", [] of String}, # Invalid + {"text/html, text/xml", {"text/html", "text/xml"}}, # Ordered as given if no quality modifier + {"text/html;q=0.3, text/html;q=0.7", {"text/html;q=0.7", "text/html;q=0.3"}}, # Ordered by quality modifier + {"text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5", {"text/html;level=1", "text/html;q=0.7", "*/*;q=0.5", "text/html;level=2;q=0.4", "text/*;q=0.3"}}, # Ordered by quality modifier; one without wins + } + end end diff --git a/src/abstract_negotiator.cr b/src/abstract_negotiator.cr index d8b66e8..66d4296 100644 --- a/src/abstract_negotiator.cr +++ b/src/abstract_negotiator.cr @@ -1,4 +1,13 @@ abstract class Athena::Negotiation::AbstractNegotiator + private record OrderKey, quality : Float32, index : Int32, value : String do + include Comparable(self) + + def <=>(other : self) : Int32 + return @index <=> other.index if @quality == other.quality + @quality > other.quality ? -1 : 1 + end + end + private abstract def create_header(header : String) : ANG::BaseAccept def best(header : String, priorities : Indexable(String), strict : Bool = false) : ANG::BaseAccept? @@ -28,6 +37,28 @@ abstract class Athena::Negotiation::AbstractNegotiator match.nil? ? nil : accepted_priorties[match.index] end + def ordered_elements(header : String) : Array(ANG::BaseAccept) + raise ArgumentError.new "The header string should not be empty." if header.blank? + + elements = Array(ANG::BaseAccept).new + order_keys = Array(OrderKey).new + + idx = 0 + self.parse_header(header) do |h| + element = self.create_header h + elements << element + order_keys << OrderKey.new element.quality, idx, element.value + rescue ex + # skip + ensure + idx += 1 + end + + order_keys.sort!.map do |ok| + elements[ok.index] + end + end + protected def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch? accept_type = header.type priority_type = priority.type @@ -43,7 +74,7 @@ abstract class Athena::Negotiation::AbstractNegotiator private def parse_header(header : String, & : String ->) : Nil header.scan /(?:[^,\"]*+(?:"[^"]*+\")?)+[^,\"]*+/ do |match| - yield match[0] unless match[0].blank? + yield match[0].strip unless match[0].blank? end end diff --git a/src/athena-negotiation.cr b/src/athena-negotiation.cr index 76348e3..a39aba5 100644 --- a/src/athena-negotiation.cr +++ b/src/athena-negotiation.cr @@ -11,8 +11,5 @@ require "./exceptions/*" alias ANG = Athena::Negotiation module Athena::Negotiation + class_getter(negotiator) { ANG::Negotiator.new } end - -# n = ANG::Negotiator.new - -# pp n.best "text/html; charset=UTF-8, application/pdf", ["text/html"]