From 180ba3fae4f40c5824e53b530d608870018c44da Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Tue, 22 Dec 2020 12:52:33 -0500 Subject: [PATCH] Add exception types Add specs Fix some bugs --- shard.yml | 3 + spec/accept_language_spec.cr | 31 ++++++++ spec/accept_match_spec.cr | 43 ++++++++++ spec/accept_spec.cr | 54 +++++++++++++ spec/athena-negotiation_spec.cr | 7 -- spec/base_accept_spec.cr | 65 +++++++++++++++ spec/negotiator_spec.cr | 101 ++++++++++++++++++++++++ spec/spec_helper.cr | 5 ++ src/abstract_negotiator.cr | 42 +++++----- src/accept.cr | 2 +- src/accept_language.cr | 2 - src/accept_match.cr | 22 ++++++ src/athena-negotiation.cr | 16 +--- src/base_accept.cr | 11 +-- src/exceptions/invalid_media_type.cr | 9 +++ src/exceptions/negotiation_exception.cr | 2 + src/negotiator.cr | 63 +++++++++++++++ 17 files changed, 433 insertions(+), 45 deletions(-) create mode 100644 spec/accept_language_spec.cr create mode 100644 spec/accept_match_spec.cr create mode 100644 spec/accept_spec.cr delete mode 100644 spec/athena-negotiation_spec.cr create mode 100644 spec/base_accept_spec.cr create mode 100644 spec/negotiator_spec.cr create mode 100644 src/exceptions/invalid_media_type.cr create mode 100644 src/exceptions/negotiation_exception.cr diff --git a/shard.yml b/shard.yml index 9f229af..1f5a48e 100644 --- a/shard.yml +++ b/shard.yml @@ -20,3 +20,6 @@ development_dependencies: ameba: github: crystal-ameba/ameba version: ~> 0.13.0 + athena-spec: + github: athena-framework/spec + version: ~> 0.2.3 diff --git a/spec/accept_language_spec.cr b/spec/accept_language_spec.cr new file mode 100644 index 0000000..c45b096 --- /dev/null +++ b/spec/accept_language_spec.cr @@ -0,0 +1,31 @@ +require "./spec_helper" + +struct AcceptLanguageTest < ASPEC::TestCase + @[DataProvider("type_data_provider")] + def test_get_type(header : String?, expected : String?) : Nil + ANG::AcceptLanguage.new(header).type.should eq expected + end + + def type_data_provider : Tuple + { + {"en;q=0.7", "en"}, + {"en-GB;q=0.8", "en-gb"}, + {"da", "da"}, + {"en-gb;q=0.8", "en-gb"}, + {"es;q=0.7", "es"}, + {"fr ; q= 0.1", "fr"}, + } + end + + @[DataProvider("value_data_provider")] + def test_get_value(header : String?, expected : String?) : Nil + ANG::AcceptLanguage.new(header).value.should eq expected + end + + def value_data_provider : Tuple + { + {"en;q=0.7", "en;q=0.7"}, + {"en-GB;q=0.8", "en-GB;q=0.8"}, + } + end +end diff --git a/spec/accept_match_spec.cr b/spec/accept_match_spec.cr new file mode 100644 index 0000000..b78c513 --- /dev/null +++ b/spec/accept_match_spec.cr @@ -0,0 +1,43 @@ +require "./spec_helper" + +struct AcceptMatchTest < ASPEC::TestCase + @[DataProvider("compare_data_provider")] + def test_compare(match1 : ANG::AcceptMatch, match2 : ANG::AcceptMatch, expected : Int32) : Nil + (match1 <=> match2).should eq expected + end + + def compare_data_provider : Tuple + { + {ANG::AcceptMatch.new(1.0, 110, 1), ANG::AcceptMatch.new(1.0, 111, 1), 0}, + {ANG::AcceptMatch.new(0.1, 10, 1), ANG::AcceptMatch.new(0.1, 10, 2), -1}, + {ANG::AcceptMatch.new(0.5, 110, 5), ANG::AcceptMatch.new(0.5, 11, 4), 1}, + {ANG::AcceptMatch.new(0.4, 110, 1), ANG::AcceptMatch.new(0.6, 111, 3), 1}, + {ANG::AcceptMatch.new(0.6, 110, 1), ANG::AcceptMatch.new(0.4, 111, 3), -1}, + } + end + + @[DataProvider("reduce_data_provider")] + def test_reduce(matches : Hash(Int32, ANG::AcceptMatch), match : ANG::AcceptMatch, expected : Hash(Int32, ANG::AcceptMatch)) : Nil + ANG::AcceptMatch.reduce(matches, match).should eq expected + end + + def reduce_data_provider : Tuple + { + { + {1 => ANG::AcceptMatch.new(1.0, 10, 1)}, + ANG::AcceptMatch.new(0.5, 111, 1), + {1 => ANG::AcceptMatch.new(0.5, 111, 1)}, + }, + { + {1 => ANG::AcceptMatch.new(1.0, 110, 1)}, + ANG::AcceptMatch.new(0.5, 11, 1), + {1 => ANG::AcceptMatch.new(1.0, 110, 1)}, + }, + { + {0 => ANG::AcceptMatch.new(1.0, 10, 1)}, + ANG::AcceptMatch.new(0.5, 111, 1), + {0 => ANG::AcceptMatch.new(1.0, 10, 1), 1 => ANG::AcceptMatch.new(0.5, 111, 1)}, + }, + } + end +end diff --git a/spec/accept_spec.cr b/spec/accept_spec.cr new file mode 100644 index 0000000..d962354 --- /dev/null +++ b/spec/accept_spec.cr @@ -0,0 +1,54 @@ +require "./spec_helper" + +struct AcceptTest < ASPEC::TestCase + def test_parameters : Nil + ANG::Accept.new("foo/bar; q=1; hello=world").parameters["hello"]?.should eq "world" + end + + @[DataProvider("normalized_value_data_provider")] + def test_normalized_value(header : String, expected : String) : Nil + ANG::Accept.new(header).normalized_value.should eq expected + end + + def normalized_value_data_provider : Tuple + { + {"text/html; z=y; a=b; c=d", "text/html; z=y; a=b; c=d"}, + {"application/pdf; q=1; param=p", "application/pdf; param=p"}, + } + end + + @[DataProvider("type_data_provider")] + def test_type(header : String, expected : String) : Nil + ANG::Accept.new(header).type.should eq expected + end + + def type_data_provider : Tuple + { + {"text/html;hello=world", "text/html"}, + {"application/pdf", "application/pdf"}, + {"application/xhtml+xml;q=0.9", "application/xhtml+xml"}, + {"text/plain; q=0.5", "text/plain"}, + {"text/html;level=2;q=0.4", "text/html"}, + {"text/html ; level = 2 ; q = 0.4", "text/html"}, + {"text/*", "text/*"}, + {"text/* ;q=1 ;level=2", "text/*"}, + {"*/*", "*/*"}, + {"*", "*/*"}, + {"*/* ; param=555", "*/*"}, + {"* ; param=555", "*/*"}, + {"TEXT/hTmL;leVel=2; Q=0.4", "text/html"}, + } + end + + @[DataProvider("value_data_provider")] + def test_value(header : String, expected : String) : Nil + ANG::Accept.new(header).value.should eq expected + end + + def value_data_provider : Tuple + { + {"text/html;hello=world ;q=0.5", "text/html;hello=world ;q=0.5"}, + {"application/pdf", "application/pdf"}, + } + end +end diff --git a/spec/athena-negotiation_spec.cr b/spec/athena-negotiation_spec.cr deleted file mode 100644 index 8a342bc..0000000 --- a/spec/athena-negotiation_spec.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./spec_helper" - -describe Athena::Negotiation do - it "works" do - false.should eq(true) - end -end diff --git a/spec/base_accept_spec.cr b/spec/base_accept_spec.cr new file mode 100644 index 0000000..71305e8 --- /dev/null +++ b/spec/base_accept_spec.cr @@ -0,0 +1,65 @@ +require "./spec_helper" + +private struct MockAccept < ANG::BaseAccept; end + +struct BaseAcceptTest < ASPEC::TestCase + @[DataProvider("build_parameters_data_provider")] + def test_build_parameters_string(header : String, expected : String) : Nil + MockAccept.new(header).normalized_value.should eq expected + end + + def build_parameters_data_provider : Tuple + { + {"media/type; xxx = 1.0;level=2;foo=bar", "media/type; xxx=1.0; level=2; foo=bar"}, + } + end + + @[DataProvider("parameters_data_provider")] + def test_parse_parameters(header : String, expected_parameters : Hash(String, String)) : Nil + accept = MockAccept.new header + parameters = accept.parameters + + # TODO: Can this be improved? + if header.includes? 'q' + parameters["q"] = accept.quality.to_s + end + + expected_parameters.size.should eq parameters.size + + expected_parameters.each do |k, v| + parameters.has_key?(k).should be_true + parameters[k].should eq v + end + end + + def parameters_data_provider : Tuple + { + { + "application/json ;q=1.0; level=2;foo= bar", + { + "q" => "1.0", + "level" => "2", + "foo" => "bar", + }, + }, + { + "application/json ;q = 1.0; level = 2; FOO = bAr", + { + "q" => "1.0", + "level" => "2", + "foo" => "bAr", + }, + }, + { + "application/json;q=1.0", + { + "q" => "1.0", + }, + }, + { + "application/json;foo", + {} of String => String, + }, + } + end +end diff --git a/spec/negotiator_spec.cr b/spec/negotiator_spec.cr new file mode 100644 index 0000000..296a59a --- /dev/null +++ b/spec/negotiator_spec.cr @@ -0,0 +1,101 @@ +require "./spec_helper" + +struct NegotiatorTest < ASPEC::TestCase + @negotiator : ANG::Negotiator + + def initialize + @negotiator = ANG::Negotiator.new + end + + def test_exception_handling : Nil + ex = expect_raises ANG::Exceptions::InvalidMediaType, "Invalid media type: '/qwer'." do + @negotiator.best "foo/bar", {"/qwer"} + end + + ex.type.should eq "/qwer" + + expect_raises ArgumentError, "priorities should not be empty." do + @negotiator.best "foo/bar", [] of String + end + + expect_raises ArgumentError, "The header string should not be empty." do + @negotiator.best "", {"text/html"} + end + end + + @[DataProvider("best_data_provider")] + def test_best(header : String, priorities : Indexable(String), expected : Tuple(String, Hash(String, String) | Nil) | Nil) : Nil + begin + accept_header = @negotiator.best header, priorities + rescue ex + ex.should eq expected + + return + end + + if accept_header.nil? + expected.should be_nil + + return + end + + accept_header.should be_a ANG::Accept + + expected = expected.should_not be_nil + + accept_header.type.should eq expected[0] + accept_header.parameters.should eq (expected[1] || Hash(String, String).new) + end + + def best_data_provider : Tuple + rfc_header = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5" + php_pear_header = "text/html,application/xhtml+xml,application/xml;q=0.9,text/*;q=0.7,*/*,image/gif; q=0.8, image/jpeg; q=0.6, image/*" + + { + {"/qwer", {"f/g"}, nil}, + {"text/html", {"application/rss"}, nil}, + {rfc_header, {"text/html;q=0.4", "text/plain"}, {"text/plain", nil}}, + + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. + {rfc_header, {"text/html;level=1"}, {"text/html", {"level" => "1"}}}, + {rfc_header, {"text/html"}, {"text/html", nil}}, + {rfc_header, {"image/jpeg"}, {"image/jpeg", nil}}, + {rfc_header, {"text/html;level=2"}, {"text/html", {"level" => "2"}}}, + {rfc_header, {"text/html;level=3"}, {"text/html", {"level" => "3"}}}, + + {"text/*;q=0.7, text/html;q=0.3, */*;q=0.5, image/png;q=0.4", {"text/html", "image/png"}, {"image/png", nil}}, + {"image/png;q=0.1, text/plain, audio/ogg;q=0.9", {"image/png", "text/plain", "audio/ogg"}, {"text/plain", nil}}, + {"image/png, text/plain, audio/ogg", {"baz/asdf"}, nil}, + {"image/png, text/plain, audio/ogg", {"audio/ogg"}, {"audio/ogg", nil}}, + {"image/png, text/plain, audio/ogg", {"YO/SuP"}, nil}, + {"text/html; charset=UTF-8, application/pdf", {"text/html; charset=UTF-8"}, {"text/html", {"charset" => "UTF-8"}}}, + {"text/html; charset=UTF-8, application/pdf", {"text/html"}, nil}, + {"text/html, application/pdf", {"text/html; charset=UTF-8"}, {"text/html", {"charset" => "UTF-8"}}}, + + # PHP"s PEAR HTTP2 assertions I took from the other lib. + {php_pear_header, {"image/gif", "image/png", "application/xhtml+xml", "application/xml", "text/html", "image/jpeg", "text/plain"}, {"image/png", nil}}, + {php_pear_header, {"image/gif", "application/xhtml+xml", "application/xml", "image/jpeg", "text/plain"}, {"application/xhtml+xml", nil}}, + {php_pear_header, {"image/gif", "application/xml", "image/jpeg", "text/plain"}, {"application/xml", nil}}, + {php_pear_header, {"image/gif", "image/jpeg", "text/plain"}, {"image/gif", nil}}, + {php_pear_header, {"text/plain", "image/png", "image/jpeg"}, {"image/png", nil}}, + {php_pear_header, {"image/jpeg", "image/gif"}, {"image/gif", nil}}, + {php_pear_header, {"image/png"}, {"image/png", nil}}, + {php_pear_header, {"audio/midi"}, {"audio/midi", nil}}, + {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", {"application/rss+xml"}, {"application/rss+xml", nil}}, + + # Case sensitiviy + {"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=2"}, {"text/html", {"level" => "2"}}}, + {"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=3"}, {"text/html", {"level" => "3"}}}, + + # 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 '+' + {"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}}, + {php_pear_header, {"application/*+xml"}, {"application/*+xml", nil}}, + {"application/hal+json", {"application/ld+json", "application/hal+json", "application/xml", "text/xml", "application/json", "text/html"}, {"application/hal+json", nil}}, + } + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 48155e0..e891a3c 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,2 +1,7 @@ require "spec" +require "athena-spec" require "../src/athena-negotiation" + +include ASPEC::Methods + +ASPEC.run_all diff --git a/src/abstract_negotiator.cr b/src/abstract_negotiator.cr index 0a2d846..d8b66e8 100644 --- a/src/abstract_negotiator.cr +++ b/src/abstract_negotiator.cr @@ -1,9 +1,9 @@ abstract class Athena::Negotiation::AbstractNegotiator private abstract def create_header(header : String) : ANG::BaseAccept - def best(header : String, priorities : Array(String), strict : Bool = false) : ANG::BaseAccept? - raise ArgumentError.new "priorities should not be empty" if priorities.empty? - raise ArgumentError.new "The header string should not be empty" if header.blank? + def best(header : String, priorities : Indexable(String), strict : Bool = false) : ANG::BaseAccept? + raise ArgumentError.new "priorities should not be empty." if priorities.empty? + raise ArgumentError.new "The header string should not be empty." if header.blank? accepted_headers = Array(ANG::BaseAccept).new @@ -17,7 +17,26 @@ abstract class Athena::Negotiation::AbstractNegotiator matches = self.find_matches accepted_headers, accepted_priorties - pp matches + specific_matches = matches.reduce({} of Int32 => ANG::AcceptMatch) do |matches, match| + ANG::AcceptMatch.reduce matches, match + end.values + + specific_matches.sort! + + match = specific_matches.shift? + + match.nil? ? nil : accepted_priorties[match.index] + end + + protected def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch? + accept_type = header.type + priority_type = priority.type + + equal = accept_type.downcase == priority_type.downcase + + if equal || accept_type == "*" + return ANG::AcceptMatch.new header.quality * priority.quality, 1 * (equal ? 1 : 0), index + end nil end @@ -28,7 +47,7 @@ abstract class Athena::Negotiation::AbstractNegotiator end end - private def find_matches(headers : Array(ANG::BaseAccept), priorities : Array(ANG::BaseAccept)) : Array(ANG::AcceptMatch) + private def find_matches(headers : Array(ANG::BaseAccept), priorities : Indexable(ANG::BaseAccept)) : Array(ANG::AcceptMatch) matches = [] of ANG::AcceptMatch priorities.each_with_index do |priority, idx| @@ -41,17 +60,4 @@ abstract class Athena::Negotiation::AbstractNegotiator matches end - - private def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch? - accept_type = header.type - priority_type = priority.type - - equal = accept_type.downcase <=> priority_type.downcase - - if !equal.zero? || accept_type == "*" - return ANG::AcceptMatch.new header.quality * priority.quality, 1 * equal, index - end - - nil - end end diff --git a/src/accept.cr b/src/accept.cr index 6a6366a..fec57ae 100644 --- a/src/accept.cr +++ b/src/accept.cr @@ -12,7 +12,7 @@ struct Athena::Negotiation::Accept < Athena::Negotiation::BaseAccept parts = @type.split '/' # TODO: Use more specific exception - raise "Invalid media type: '#{@type}'." if parts.size != 2 || !parts[0].presence || !parts[1].presence + raise ANG::Exceptions::InvalidMediaType.new @type, "Invalid media type: '#{@type}'." if parts.size != 2 || !parts[0].presence || !parts[1].presence @base_part = parts[0] @sub_part = parts[1] diff --git a/src/accept_language.cr b/src/accept_language.cr index 3277585..251d175 100644 --- a/src/accept_language.cr +++ b/src/accept_language.cr @@ -10,8 +10,6 @@ struct Athena::Negotiation::AcceptLanguage < Athena::Negotiation::BaseAccept parts = @value.split '-' - pp parts - case parts.size when 2 @language = parts[0] diff --git a/src/accept_match.cr b/src/accept_match.cr index 43b9474..948419e 100644 --- a/src/accept_match.cr +++ b/src/accept_match.cr @@ -1,7 +1,29 @@ struct Athena::Negotiation::AcceptMatch + include Comparable(self) + getter quality : Float32 getter score : Int32 getter index : Int32 + def self.reduce(matches : Hash(Int32, self), match : self) : Hash(Int32, self) + if !matches.has_key?(match.index) || matches[match.index].score < match.score + matches[match.index] = match + end + + matches + end + def initialize(@quality : Float32, @score : Int32, @index : Int32); end + + def <=>(other : self) : Int32 + if @quality != other.quality + return @quality > other.quality ? -1 : 1 + end + + if @index != other.index + return @index > other.index ? 1 : -1 + end + + 0 + end end diff --git a/src/athena-negotiation.cr b/src/athena-negotiation.cr index cfd24fb..76348e3 100644 --- a/src/athena-negotiation.cr +++ b/src/athena-negotiation.cr @@ -5,22 +5,14 @@ require "./accept_encoding" require "./accept_language" require "./negotiator" +require "./exceptions/*" + # Convenience alias to make referencing `Athena::Negotiation` types easier. alias ANG = Athena::Negotiation module Athena::Negotiation end -# pp ANG::Accept.new "application/json;q=1.0" -# pp ANG::Accept.new "application/json ;q=1.0; level=2;foo= bar" -# pp ANG::Accept.new "text/html ; level = 2 ; q = 0.4" +# n = ANG::Negotiator.new -# puts -# puts - -# pp ANG::AcceptLanguage.new "en-gb;q=0.8" - -n = ANG::Negotiator.new - -pp n.best "text/html;level=1", ["text/html"] # text/html -pp n.best "text/*;q=0.7, text/html;q=0.3, */*;q=0.5, image/png;q=0.4", ["text/html", "image/png"] # image/png +# pp n.best "text/html; charset=UTF-8, application/pdf", ["text/html"] diff --git a/src/base_accept.cr b/src/base_accept.cr index 3066652..29896da 100644 --- a/src/base_accept.cr +++ b/src/base_accept.cr @@ -2,7 +2,7 @@ abstract struct Athena::Negotiation::BaseAccept getter quality : Float32 = 1.0 getter normalized_value : String getter value : String - getter parameters : Hash(String, String) + getter parameters : Hash(String, String) = Hash(String, String).new getter type : String def initialize(@value : String) @@ -10,13 +10,13 @@ abstract struct Athena::Negotiation::BaseAccept parts = @value.split ';' @type = parts.shift.strip.downcase - @parameters = parts.to_h do |part| + parts.each do |part| part = part.split '=' - # TODO: Use more specific exception - raise ArgumentError.new "Invalid header: '#{@value}'." unless part.size == 2 + # Skip invalid parameters + next unless part.size == 2 - {part[0].strip.downcase, part[1].strip(" \"")} + @parameters[part[0].strip.downcase] = part[1].strip(" \"") end if quality = @parameters.delete "q" @@ -28,6 +28,7 @@ abstract struct Athena::Negotiation::BaseAccept unless @parameters.empty? io << "; " + # TODO: Do we care the parameters aren't sorted? parameters.join(io, "; ") { |(k, v), io| io << "#{k}=#{v}" } end end diff --git a/src/exceptions/invalid_media_type.cr b/src/exceptions/invalid_media_type.cr new file mode 100644 index 0000000..66e725c --- /dev/null +++ b/src/exceptions/invalid_media_type.cr @@ -0,0 +1,9 @@ +require "./negotiation_exception" + +class Athena::Negotiation::Exceptions::InvalidMediaType < Athena::Negotiation::Exceptions::Exception + getter type : String + + def initialize(@type : String, message : String? = nil, cause : Exception? = nil) + super message, cause + end +end diff --git a/src/exceptions/negotiation_exception.cr b/src/exceptions/negotiation_exception.cr new file mode 100644 index 0000000..f91de18 --- /dev/null +++ b/src/exceptions/negotiation_exception.cr @@ -0,0 +1,2 @@ +abstract class Athena::Negotiation::Exceptions::Exception < ::Exception +end diff --git a/src/negotiator.cr b/src/negotiator.cr index dad27bb..352af49 100644 --- a/src/negotiator.cr +++ b/src/negotiator.cr @@ -1,6 +1,69 @@ require "./abstract_negotiator" class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator + protected def match(accept : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch? + return nil if !accept.is_a?(ANG::Accept) || !priority.is_a?(ANG::Accept) + + accept_base = accept.base_part + priority_base = priority.base_part + + accept_sub = accept.sub_part + priority_sub = priority.sub_part + + intercection = accept.parameters.each_with_object({} of String => String) do |(k, v), params| + priority.parameters.tap do |pp| + params[k] = v if pp.has_key?(k) && pp[k] == v + end + end + + base_equal = accept_base.downcase == priority_base.downcase + sub_equal = accept_sub.downcase == priority_sub.downcase + + if ( + (accept_base == "*" || base_equal) && + (accept_sub == "*" || sub_equal) && + intercection.size == accept.parameters.size + ) + score = 100 * (base_equal ? 1 : 0) + 10 * (sub_equal ? 1 : 0) + intercection.size + + return ANG::AcceptMatch.new accept.quality * priority.quality, score, index + end + + return nil if !accept_sub.includes?('+') || !priority_sub.includes?('+') + + accept_sub, accept_plus = self.split_sub_part accept_sub + priority_sub, priority_plus = self.split_sub_part priority_sub + + if ( + !(accept_base == "*" || base_equal) || + !(accept_sub == "*" || priority_sub == "*" || accept_plus == "*" || priority_plus == "*") + ) + return nil + end + + sub_equal = accept_sub.downcase == priority_sub.downcase + plus_equal = accept_plus.downcase == priority_plus.downcase + + if ( + (accept_sub == "*" || priority_sub == "*" || sub_equal) && + (accept_plus == "*" || priority_plus == '*' || plus_equal) && + intercection.size == accept.parameters.size + ) + # TODO: Calculate intercection between each header's parameters + + score = 100 * (base_equal ? 1 : 0) + 10 * (sub_equal ? 1 : 0) + (plus_equal ? 1 : 0) + intercection.size + return ANG::AcceptMatch.new accept.quality * priority.quality, score, index + end + + nil + end + + private def split_sub_part(sub_part : String) : Array(String) + return [sub_part, ""] unless sub_part.includes? '+' + + sub_part.split '+', limit: 2 + end + private def create_header(header : String) : ANG::BaseAccept ANG::Accept.new header end