From 3850074f0278302a2b426fcb751c42c5cb374ade Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Tue, 22 Dec 2020 13:59:40 -0500 Subject: [PATCH] Add more specialized negotiators Fix issue with accept language setting --- spec/charset_negotiator_spec.cr | 67 ++++++++++++++++++++++++++++++++ spec/encoding_negotiator_spec.cr | 42 ++++++++++++++++++++ spec/language_negotiator_spec.cr | 43 ++++++++++++++++++++ spec/negotiator_spec.cr | 12 +----- spec/negotiator_test_case.cr | 11 ++++++ spec/spec_helper.cr | 1 + src/accept_language.cr | 2 +- src/athena-negotiation.cr | 3 ++ src/charset_negotiator.cr | 7 ++++ src/encoding_negotiator.cr | 7 ++++ src/language_negotiator.cr | 26 +++++++++++++ src/negotiator.cr | 4 +- 12 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 spec/charset_negotiator_spec.cr create mode 100644 spec/encoding_negotiator_spec.cr create mode 100644 spec/language_negotiator_spec.cr create mode 100644 spec/negotiator_test_case.cr create mode 100644 src/charset_negotiator.cr create mode 100644 src/encoding_negotiator.cr create mode 100644 src/language_negotiator.cr diff --git a/spec/charset_negotiator_spec.cr b/spec/charset_negotiator_spec.cr new file mode 100644 index 0000000..057c49d --- /dev/null +++ b/spec/charset_negotiator_spec.cr @@ -0,0 +1,67 @@ +require "./spec_helper" + +struct CharsetNegotiatorTest < NegotiatorTestCase + @negotiator : ANG::CharsetNegotiator + + def initialize + @negotiator = ANG::CharsetNegotiator.new + end + + def test_best_unmatched_header : Nil + @negotiator.best("foo, bar, yo", {"baz"}).should be_nil + end + + def test_best_ignores_missing_content : Nil + accept = @negotiator.best "en; q=0.1, fr; q=0.4, bu; q=1.0", {"en", "fr"} + + accept = accept.should_not be_nil + accept.should be_a ANG::AcceptCharset + accept.value.should eq "fr" + end + + def test_best_respects_priorities : Nil + accept = @negotiator.best "foo, bar, yo", {"yo"} + accept = accept.should_not be_nil + accept.should be_a ANG::AcceptCharset + accept.type.should eq "yo" + end + + def test_best_respects_quality : Nil + accept = @negotiator.best "utf-8;q=0.5,iso-8859-1", {"iso-8859-1;q=0.3", "utf-8;q=0.9", "utf-16;q=1.0"} + accept = accept.should_not be_nil + accept.should be_a ANG::AcceptCharset + accept.type.should eq "utf-8" + end + + @[DataProvider("best_data_provider")] + def test_best(header : String, priorities : Indexable(String), expected : String?) : Nil + accept = @negotiator.best header, priorities + + if accept.nil? + expected.should be_nil + else + accept.should be_a ANG::AcceptCharset + accept.value.should eq expected + end + end + + def best_data_provider : Tuple + php_pear_charset = "ISO-8859-1, Big5;q=0.6,utf-8;q=0.7, *;q=0.5" + php_pear_charset2 = "ISO-8859-1, Big5;q=0.6,utf-8;q=0.7" + + { + {php_pear_charset, {"utf-8", "big5", "iso-8859-1", "shift-jis"}, "iso-8859-1"}, + {php_pear_charset, {"utf-8", "big5", "shift-jis"}, "utf-8"}, + {php_pear_charset, {"Big5", "shift-jis"}, "Big5"}, + {php_pear_charset, {"shift-jis"}, "shift-jis"}, + {php_pear_charset2, {"utf-8", "big5", "iso-8859-1", "shift-jis"}, "iso-8859-1"}, + {php_pear_charset2, {"utf-8", "big5", "shift-jis"}, "utf-8"}, + {php_pear_charset2, {"Big5", "shift-jis"}, "Big5"}, + {"utf-8;q=0.6,iso-8859-5;q=0.9", {"iso-8859-5", "utf-8"}, "iso-8859-5"}, + {"en, *;q=0.9", {"fr"}, "fr"}, + # Quality of source factors + {php_pear_charset, {"iso-8859-1;q=0.5", "utf-8", "utf-16;q=1.0"}, "utf-8"}, + {php_pear_charset, {"iso-8859-1;q=0.8", "utf-8", "utf-16;q=1.0"}, "iso-8859-1;q=0.8"}, + } + end +end diff --git a/spec/encoding_negotiator_spec.cr b/spec/encoding_negotiator_spec.cr new file mode 100644 index 0000000..4dcc985 --- /dev/null +++ b/spec/encoding_negotiator_spec.cr @@ -0,0 +1,42 @@ +require "./spec_helper" + +struct EncodingNegotiatorTest < NegotiatorTestCase + @negotiator : ANG::EncodingNegotiator + + def initialize + @negotiator = ANG::EncodingNegotiator.new + end + + def test_best_unmatched_header : Nil + @negotiator.best("foo, bar, yo", {"baz"}).should be_nil + end + + def test_best_respects_quality : Nil + accept = @negotiator.best "gzip;q=0.7,identity", {"identity;q=0.5", "gzip;q=0.9"} + accept = accept.should_not be_nil + accept.should be_a ANG::AcceptEncoding + accept.type.should eq "gzip" + end + + @[DataProvider("best_data_provider")] + def test_best(header : String, priorities : Indexable(String), expected : String?) : Nil + accept = @negotiator.best header, priorities + + if accept.nil? + expected.should be_nil + else + accept.should be_a ANG::AcceptEncoding + accept.value.should eq expected + end + end + + def best_data_provider : Tuple + { + {"gzip;q=1.0, identity; q=0.5, *;q=0", {"identity"}, "identity"}, + {"gzip;q=0.5, identity; q=0.5, *;q=0.7", {"bzip", "foo"}, "bzip"}, + {"gzip;q=0.7, identity; q=0.5, *;q=0.7", {"gzip", "foo"}, "gzip"}, + # Quality of source factors + {"gzip;q=0.7,identity", {"identity;q=0.5", "gzip;q=0.9"}, "gzip;q=0.9"}, + } + end +end diff --git a/spec/language_negotiator_spec.cr b/spec/language_negotiator_spec.cr new file mode 100644 index 0000000..d7304ac --- /dev/null +++ b/spec/language_negotiator_spec.cr @@ -0,0 +1,43 @@ +require "./spec_helper" + +struct LanguageNegotiatorTest < NegotiatorTestCase + @negotiator : ANG::LanguageNegotiator + + def initialize + @negotiator = ANG::LanguageNegotiator.new + end + + def test_best_respects_quality : Nil + accept = @negotiator.best "en;q=0.5,de", {"de;q=0.3", "en;q=0.9"} + accept = accept.should_not be_nil + accept.should be_a ANG::AcceptLanguage + accept.type.should eq "en" + end + + @[DataProvider("best_data_provider")] + def test_best(header : String, priorities : Indexable(String), expected : String?) : Nil + accept = @negotiator.best header, priorities + + if accept.nil? + expected.should be_nil + else + accept.should be_a ANG::AcceptLanguage + accept.value.should eq expected + end + end + + def best_data_provider : Tuple + { + {"en, de", {"fr"}, nil}, + {"foo, bar, yo", {"baz", "biz"}, nil}, + {"fr-FR, en;q=0.8", {"en-US", "de-DE"}, "en-US"}, + {"en, *;q=0.9", {"fr"}, "fr"}, + {"foo, bar, yo", {"yo"}, "yo"}, + {"en; q=0.1, fr; q=0.4, bu; q=1.0", {"en", "fr"}, "fr"}, + {"en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2", {"en", "fu"}, "fu"}, + {"fr, zh-Hans-CN;q=0.3", {"fr"}, "fr"}, + # Quality of source factors + {"en;q=0.5,de", {"de;q=0.3", "en;q=0.9"}, "en;q=0.9"}, + } + end +end diff --git a/spec/negotiator_spec.cr b/spec/negotiator_spec.cr index 3c5e59b..87887e9 100644 --- a/spec/negotiator_spec.cr +++ b/spec/negotiator_spec.cr @@ -1,6 +1,6 @@ require "./spec_helper" -struct NegotiatorTest < ASPEC::TestCase +struct NegotiatorTest < NegotiatorTestCase @negotiator : ANG::Negotiator def initialize @@ -18,20 +18,12 @@ struct NegotiatorTest < ASPEC::TestCase @negotiator.best("/qwer", {"foo/bar"}, false).should be_nil end - def test_best_exception_handling : Nil + def test_invalid_media_type : 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")] diff --git a/spec/negotiator_test_case.cr b/spec/negotiator_test_case.cr new file mode 100644 index 0000000..d8ccd8d --- /dev/null +++ b/spec/negotiator_test_case.cr @@ -0,0 +1,11 @@ +abstract struct NegotiatorTestCase < ASPEC::TestCase + def test_best_exception_handling : Nil + 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 +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index e891a3c..d613a60 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,6 +1,7 @@ require "spec" require "athena-spec" require "../src/athena-negotiation" +require "./negotiator_test_case" include ASPEC::Methods diff --git a/src/accept_language.cr b/src/accept_language.cr index 251d175..7df3f2d 100644 --- a/src/accept_language.cr +++ b/src/accept_language.cr @@ -8,7 +8,7 @@ struct Athena::Negotiation::AcceptLanguage < Athena::Negotiation::BaseAccept def initialize(value : String) super value - parts = @value.split '-' + parts = @type.split '-' case parts.size when 2 diff --git a/src/athena-negotiation.cr b/src/athena-negotiation.cr index a39aba5..3b511f2 100644 --- a/src/athena-negotiation.cr +++ b/src/athena-negotiation.cr @@ -3,6 +3,9 @@ require "./accept_match" require "./accept_charset" require "./accept_encoding" require "./accept_language" +require "./charset_negotiator" +require "./encoding_negotiator" +require "./language_negotiator" require "./negotiator" require "./exceptions/*" diff --git a/src/charset_negotiator.cr b/src/charset_negotiator.cr new file mode 100644 index 0000000..88c6c48 --- /dev/null +++ b/src/charset_negotiator.cr @@ -0,0 +1,7 @@ +require "./abstract_negotiator" + +class Athena::Negotiation::CharsetNegotiator < Athena::Negotiation::AbstractNegotiator + private def create_header(header : String) : ANG::BaseAccept + ANG::AcceptCharset.new header + end +end diff --git a/src/encoding_negotiator.cr b/src/encoding_negotiator.cr new file mode 100644 index 0000000..f7607f7 --- /dev/null +++ b/src/encoding_negotiator.cr @@ -0,0 +1,7 @@ +require "./abstract_negotiator" + +class Athena::Negotiation::EncodingNegotiator < Athena::Negotiation::AbstractNegotiator + private def create_header(header : String) : ANG::BaseAccept + ANG::AcceptEncoding.new header + end +end diff --git a/src/language_negotiator.cr b/src/language_negotiator.cr new file mode 100644 index 0000000..7a317de --- /dev/null +++ b/src/language_negotiator.cr @@ -0,0 +1,26 @@ +require "./abstract_negotiator" + +class Athena::Negotiation::LanguageNegotiator < Athena::Negotiation::AbstractNegotiator + protected def match(accept : ANG::AcceptLanguage, priority : ANG::AcceptLanguage, index : Int32) : ANG::AcceptMatch? + accept_base = accept.language + priority_base = priority.language + + accept_sub = accept.region + priority_sub = priority.region + + base_equal = accept_base.downcase == priority_base.downcase + sub_equal = accept_sub.try &.downcase == priority_sub.try &.downcase + + if ((accept_base == "*" || base_equal) && (accept_sub.nil? || sub_equal)) + score = 10 * (base_equal ? 1 : 0) + (sub_equal ? 1 : 0) + + return ANG::AcceptMatch.new accept.quality * priority.quality, score, index + end + + nil + end + + private def create_header(header : String) : ANG::BaseAccept + ANG::AcceptLanguage.new header + end +end diff --git a/src/negotiator.cr b/src/negotiator.cr index 352af49..07ee439 100644 --- a/src/negotiator.cr +++ b/src/negotiator.cr @@ -1,9 +1,7 @@ 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) - + protected def match(accept : ANG::Accept, priority : ANG::Accept, index : Int32) : ANG::AcceptMatch? accept_base = accept.base_part priority_base = priority.base_part