diff --git a/spec/accept_language_spec.cr b/spec/accept_language_spec.cr index c45b096..2107cfc 100644 --- a/spec/accept_language_spec.cr +++ b/spec/accept_language_spec.cr @@ -1,12 +1,12 @@ 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 + @[DataProvider("accept_value_data_provider")] + def test_accept_value(header : String?, expected : String?) : Nil + ANG::AcceptLanguage.new(header).accept_value.should eq expected end - def type_data_provider : Tuple + def accept_value_data_provider : Tuple { {"en;q=0.7", "en"}, {"en-GB;q=0.8", "en-gb"}, @@ -17,12 +17,12 @@ struct AcceptLanguageTest < ASPEC::TestCase } end - @[DataProvider("value_data_provider")] + @[DataProvider("header_data_provider")] def test_get_value(header : String?, expected : String?) : Nil - ANG::AcceptLanguage.new(header).value.should eq expected + ANG::AcceptLanguage.new(header).header.should eq expected end - def value_data_provider : Tuple + def header_data_provider : Tuple { {"en;q=0.7", "en;q=0.7"}, {"en-GB;q=0.8", "en-GB;q=0.8"}, diff --git a/spec/accept_spec.cr b/spec/accept_spec.cr index d962354..2884b0f 100644 --- a/spec/accept_spec.cr +++ b/spec/accept_spec.cr @@ -5,24 +5,24 @@ struct AcceptTest < ASPEC::TestCase 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 + @[DataProvider("normalized_header_data_provider")] + def test_normalized_header(header : String, expected : String) : Nil + ANG::Accept.new(header).normalized_header.should eq expected end - def normalized_value_data_provider : Tuple + def normalized_header_data_provider : Tuple { - {"text/html; z=y; a=b; c=d", "text/html; z=y; a=b; c=d"}, + {"text/html ; z=y; a = b; c=d", "text/html; a=b; c=d; z=y"}, {"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 + @[DataProvider("media_range_data_provider")] + def test_media_range(header : String, expected : String) : Nil + ANG::Accept.new(header).media_range.should eq expected end - def type_data_provider : Tuple + def media_range_data_provider : Tuple { {"text/html;hello=world", "text/html"}, {"application/pdf", "application/pdf"}, @@ -40,12 +40,12 @@ struct AcceptTest < ASPEC::TestCase } end - @[DataProvider("value_data_provider")] - def test_value(header : String, expected : String) : Nil - ANG::Accept.new(header).value.should eq expected + @[DataProvider("header_data_provider")] + def test_accept_value(header : String, expected : String) : Nil + ANG::Accept.new(header).header.should eq expected end - def value_data_provider : Tuple + def header_data_provider : Tuple { {"text/html;hello=world ;q=0.5", "text/html;hello=world ;q=0.5"}, {"application/pdf", "application/pdf"}, diff --git a/spec/base_accept_spec.cr b/spec/base_accept_spec.cr index 71305e8..e3675b9 100644 --- a/spec/base_accept_spec.cr +++ b/spec/base_accept_spec.cr @@ -5,12 +5,12 @@ 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 + MockAccept.new(header).normalized_header.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"}, + {"media/type; xxx = 1.0;level=2;foo=bar", "media/type; foo=bar; level=2; xxx=1.0"}, } end diff --git a/spec/charset_negotiator_spec.cr b/spec/charset_negotiator_spec.cr index 057c49d..54938b0 100644 --- a/spec/charset_negotiator_spec.cr +++ b/spec/charset_negotiator_spec.cr @@ -16,21 +16,21 @@ struct CharsetNegotiatorTest < NegotiatorTestCase accept = accept.should_not be_nil accept.should be_a ANG::AcceptCharset - accept.value.should eq "fr" + accept.charset.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" + accept.charset.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" + accept.charset.should eq "utf-8" end @[DataProvider("best_data_provider")] @@ -41,7 +41,7 @@ struct CharsetNegotiatorTest < NegotiatorTestCase expected.should be_nil else accept.should be_a ANG::AcceptCharset - accept.value.should eq expected + accept.header.should eq expected end end diff --git a/spec/encoding_negotiator_spec.cr b/spec/encoding_negotiator_spec.cr index 4dcc985..6c65ad5 100644 --- a/spec/encoding_negotiator_spec.cr +++ b/spec/encoding_negotiator_spec.cr @@ -15,7 +15,7 @@ struct EncodingNegotiatorTest < NegotiatorTestCase 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" + accept.coding.should eq "gzip" end @[DataProvider("best_data_provider")] @@ -26,7 +26,7 @@ struct EncodingNegotiatorTest < NegotiatorTestCase expected.should be_nil else accept.should be_a ANG::AcceptEncoding - accept.value.should eq expected + accept.header.should eq expected end end diff --git a/spec/language_negotiator_spec.cr b/spec/language_negotiator_spec.cr index d7304ac..a1a15c6 100644 --- a/spec/language_negotiator_spec.cr +++ b/spec/language_negotiator_spec.cr @@ -11,7 +11,7 @@ struct LanguageNegotiatorTest < NegotiatorTestCase 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" + accept.language.should eq "en" end @[DataProvider("best_data_provider")] @@ -22,7 +22,7 @@ struct LanguageNegotiatorTest < NegotiatorTestCase expected.should be_nil else accept.should be_a ANG::AcceptLanguage - accept.value.should eq expected + accept.header.should eq expected end end diff --git a/spec/negotiator_spec.cr b/spec/negotiator_spec.cr index 87887e9..946d3c2 100644 --- a/spec/negotiator_spec.cr +++ b/spec/negotiator_spec.cr @@ -11,7 +11,7 @@ struct NegotiatorTest < NegotiatorTestCase 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" + accept.media_range.should eq "text/plain" end def test_best_invalid_unstrict @@ -23,7 +23,7 @@ struct NegotiatorTest < NegotiatorTestCase @negotiator.best "foo/bar", {"/qwer"} end - ex.type.should eq "/qwer" + ex.media_range.should eq "/qwer" end @[DataProvider("best_data_provider")] @@ -46,7 +46,7 @@ struct NegotiatorTest < NegotiatorTestCase expected = expected.should_not be_nil - accept_header.type.should eq expected[0] + accept_header.media_range.should eq expected[0] accept_header.parameters.should eq (expected[1] || Hash(String, String).new) end @@ -114,7 +114,7 @@ struct NegotiatorTest < NegotiatorTestCase expected.each_with_index do |element, idx| elements[idx].should be_a ANG::Accept - element.should eq elements[idx].value + element.should eq elements[idx].header end end diff --git a/src/abstract_negotiator.cr b/src/abstract_negotiator.cr index a7e9dbd..a7c47c4 100644 --- a/src/abstract_negotiator.cr +++ b/src/abstract_negotiator.cr @@ -1,3 +1,4 @@ +# Base negotiator type. Implements logic common to all negotiators. abstract class Athena::Negotiation::AbstractNegotiator private record OrderKey, quality : Float32, index : Int32, value : String do include Comparable(self) @@ -10,6 +11,9 @@ abstract class Athena::Negotiation::AbstractNegotiator private abstract def create_header(header : String) : ANG::BaseAccept + # Returns the best `ANG::BaseAccept` type based on the provided *header* value and *priorities*. + # + # See `Athena::Negotiation` for examples. 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? @@ -37,6 +41,9 @@ abstract class Athena::Negotiation::AbstractNegotiator match.nil? ? nil : accepted_priorties[match.index] end + # Returns an array of `ANG::BaseAccept` types that the provided *header* allows, ordered so that the `#best` match is first. + # + # See `Athena::Negotiation` for examples. def ordered_elements(header : String) : Array(ANG::BaseAccept) raise ArgumentError.new "The header string should not be empty." if header.blank? @@ -47,7 +54,7 @@ abstract class Athena::Negotiation::AbstractNegotiator self.parse_header(header) do |h| element = self.create_header h elements << element - order_keys << OrderKey.new element.quality, idx, element.value + order_keys << OrderKey.new element.quality, idx, element.header rescue ex # skip ensure @@ -60,12 +67,12 @@ abstract class Athena::Negotiation::AbstractNegotiator end protected def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch? - accept_type = header.type - priority_type = priority.type + accept_value = header.accept_value + priority_value = priority.accept_value - equal = accept_type.downcase == priority_type.downcase + equal = accept_value.downcase == priority_value.downcase - if equal || accept_type == "*" + if equal || accept_value == "*" return ANG::AcceptMatch.new header.quality * priority.quality, 1 * (equal ? 1 : 0), index end diff --git a/src/accept.cr b/src/accept.cr index 25dcee0..34d85ec 100644 --- a/src/accept.cr +++ b/src/accept.cr @@ -1,21 +1,45 @@ require "./base_accept" +# Represents an [Accept](https://tools.ietf.org/html/rfc7231#section-5.3.2) header media type. +# +# ``` +# accept = ANG::Accept.new "application/json; q = 0.75; charset = UTF-8" +# +# accept.header # => "application/json; q = 0.9; charset = UTF-8" +# accept.normalized_header # => "application/json; charset=UTF-8" +# accept.parameters # => {"charset" => "UTF-8"} +# accept.quality # => 0.75 +# accept.type # => "application" +# accept.sub_type # => "json" +# ``` struct Athena::Negotiation::Accept < Athena::Negotiation::BaseAccept - getter base_part : String - getter sub_part : String + # Returns the type for this `Accept` header. + # E.x. if the `#media_range` is `application/json`, the type would be `application`. + getter type : String + + # Returns the sub type for this `Accept` header. + # E.x. if the `#media_range` is `application/json`, the sub type would be `json`. + getter sub_type : String def initialize(value : String) super value - @type = "*/*" if @type == "*" + @accept_value = "*/*" if @accept_value == "*" - parts = @type.split '/' + parts = @accept_value.split '/' if parts.size != 2 || !parts[0].presence || !parts[1].presence - raise ANG::Exceptions::InvalidMediaType.new @type + raise ANG::Exceptions::InvalidMediaType.new @accept_value end - @base_part = parts[0] - @sub_part = parts[1] + @type = parts[0] + @sub_type = parts[1] + end + + # Returns the media range this `Accept` header represents. + # + # I.e. `#header` minus the `#quality` and `#parameters`. + def media_range : String + @accept_value end end diff --git a/src/accept_charset.cr b/src/accept_charset.cr index e20a26a..bd04412 100644 --- a/src/accept_charset.cr +++ b/src/accept_charset.cr @@ -1,4 +1,21 @@ require "./base_accept" +# Represents an [Accept-Charset](https://tools.ietf.org/html/rfc7231#section-5.3.3) header character set. +# +# ``` +# accept = ANG::AcceptCharset.new "iso-8859-1; q = 0.5; key=value" +# +# accept.header # => "iso-8859-1; q = 0.5; key=value" +# accept.normalized_header # => "iso-8859-1; key=value" +# accept.parameters # => {"key" => "value"} +# accept.quality # => 0.5 +# accept.charset # => "iso-8859-1" +# ``` struct Athena::Negotiation::AcceptCharset < Athena::Negotiation::BaseAccept + # Returns the character set this `AcceptCharset` header represents. + # + # I.e. `#header` minus the `#quality` and `#parameters`. + def charset : String + @accept_value + end end diff --git a/src/accept_encoding.cr b/src/accept_encoding.cr index 31feae0..5157935 100644 --- a/src/accept_encoding.cr +++ b/src/accept_encoding.cr @@ -1,4 +1,21 @@ require "./base_accept" +# Represents an [Accept-Encoding](https://tools.ietf.org/html/rfc7231#section-5.3.4) header character set. +# +# ``` +# accept = ANG::AcceptEncoding.new "gzip; q = 0.5; key=value" +# +# accept.header # => "gzip-1; q = 0.5; key=value" +# accept.normalized_header # => "gzip-1; key=value" +# accept.parameters # => {"key" => "value"} +# accept.quality # => 0.5 +# accept.coding # => "gzip" +# ``` struct Athena::Negotiation::AcceptEncoding < Athena::Negotiation::BaseAccept + # Returns the content coding this `AcceptEncoding` header represents. + # + # I.e. `#header` minus the `#quality` and `#parameters`. + def coding : String + @accept_value + end end diff --git a/src/accept_language.cr b/src/accept_language.cr index a4db6cb..b4c3115 100644 --- a/src/accept_language.cr +++ b/src/accept_language.cr @@ -1,27 +1,55 @@ require "./base_accept" +# Represents an [Accept-Language](https://tools.ietf.org/html/rfc7231#section-5.3.5) header character set. +# +# ``` +# accept = ANG::AcceptLanguage.new "zh-Hans-CN; q = 0.3; key=value" +# +# accept.header # => "zh-Hans-CN; q = 0.3; key=value" +# accept.normalized_header # => "zh-Hans-CN; key=value" +# accept.parameters # => {"key" => "value"} +# accept.quality # => 0.3 +# accept.language # => "zh" +# accept.region # => "cn" +# accept.script # => "hans" +# ``` struct Athena::Negotiation::AcceptLanguage < Athena::Negotiation::BaseAccept + # Returns the language for this `AcceptLanguage` header. + # E.x. if the `#language_range` is `zh-Hans-CN`, the language would be `zh`. getter language : String - getter script : String? = nil + + # Returns the region, if any, for this `AcceptLanguage` header. + # E.x. if the `#language_range` is `zh-Hans-CN`, the language would be `cn` getter region : String? = nil + # Returns the script, if any, for this `AcceptLanguage` header. + # E.x. if the `#language_range` is `zh-Hans-CN`, the language would be `hans` + getter script : String? = nil + def initialize(value : String) super value - parts = @type.split '-' + parts = @accept_value.split '-' case parts.size + when 1 + @language = parts[0] when 2 @language = parts[0] @region = parts[1] - when 1 - @language = parts[0] when 3 @language = parts[0] @script = parts[1] @region = parts[2] else - raise ANG::Exceptions::InvalidLanguage.new @type + raise ANG::Exceptions::InvalidLanguage.new @accept_value end end + + # Returns the language range this `AcceptLanguage` header represents. + # + # I.e. `#header` minus the `#quality` and `#parameters`. + def language_range : String + @accept_value + end end diff --git a/src/accept_match.cr b/src/accept_match.cr index 948419e..2774bee 100644 --- a/src/accept_match.cr +++ b/src/accept_match.cr @@ -1,3 +1,4 @@ +# :nodoc: struct Athena::Negotiation::AcceptMatch include Comparable(self) diff --git a/src/athena-negotiation.cr b/src/athena-negotiation.cr index 44a2305..1b36c62 100644 --- a/src/athena-negotiation.cr +++ b/src/athena-negotiation.cr @@ -13,16 +13,91 @@ require "./exceptions/*" # Convenience alias to make referencing `Athena::Negotiation` types easier. alias ANG = Athena::Negotiation +# The `Athena::Negotiation` component allows an application to support [content negotiation](https://tools.ietf.org/html/rfc7231#section-5.3). +# The component has no dependencies and is framework agnostic; supporting various negotiators. +# +# ## Usage +# +# The main type of `Athena::Negotiation` is `ANG::AbstractNegotiator` which is used to implement negotiators for each `Accept*` header. +# `Athena::Negotiation` exposes class level getters for each negotiator; that return a lazily initialized singleton instance. +# Each negotiator exposes two methods: `ANG::AbstractNegotiator#best` and `ANG::AbstractNegotiator#ordered_elements`. +# +# ### Media Type +# +# ``` +# negotiator = ANG.negotiator +# +# accept_header = "text/html, application/xhtml+xml, application/xml;q=0.9" +# priorities = ["text/html; charset=UTF-8", "application/json", "application/xml;q=0.5"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.media_range # => "text/html" +# accept.parameters # => {"charset" => "UTF-8"} +# ``` +# +# The `ANG::Negotiator` type returns an `ANG::Accept`, or `nil` if negotiating the best media type has failed. +# +# ### Character Set +# +# ``` +# negotiator = ANG.charset_negotiator +# +# accept_header = "ISO-8859-1, UTF-8; q=0.9" +# priorities = ["iso-8859-1;q=0.3", "utf-8;q=0.9", "utf-16;q=1.0"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.charset # => "utf-8" +# accept.quality # => 0.9 +# ``` +# +# The `ANG::CharsetNegotiator` type returns an `ANG::AcceptCharset`, or `nil` if negotiating the best character set has failed. +# +# ### Encoding +# +# ``` +# negotiator = ANG.encoding_negotiator +# +# accept_header = "gzip;q=1.0, identity; q=0.5, *;q=0" +# priorities = ["gzip", "foo"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.coding # => "gzip" +# ``` +# +# The `ANG::EncodingNegotiator` type returns an `ANG::AcceptEncoding`, or `nil` if negotiating the best character set has failed. +# +# ### Language +# +# ``` +# negotiator = ANG.language_negotiator +# +# accept_header = "en; q=0.1, fr; q=0.4, zh-Hans-CN; q=0.9, de; q=0.2" +# priorities = ["de", "zh-Hans-CN", "en"] +# +# accept = negotiator.best(accept_header, priorities).not_nil! +# +# accept.language # => "zh" +# accept.region # => "cn" +# accept.script # => "hans" +# ``` +# +# The `ANG::LanguageNegotiator` type returns an `ANG::AcceptLanguage`, or `nil` if negotiating the best character set has failed. module Athena::Negotiation - # Returns an `ANG::Negotiator` singleton instance. + # Returns a lazily initialized `ANG::Negotiator` singleton instance. class_getter(negotiator) { ANG::Negotiator.new } - # Returns an `ANG::CharsetNegotiator` singleton instance. + # Returns a lazily initialized `ANG::CharsetNegotiator` singleton instance. class_getter(charset_negotiator) { ANG::CharsetNegotiator.new } - # Returns an `ANG::EncodingNegotiator` singleton instance. + # Returns a lazily initialized `ANG::EncodingNegotiator` singleton instance. class_getter(encoding_negotiator) { ANG::EncodingNegotiator.new } - # Returns an `ANG::LanguageNegotiator` singleton instance. + # Returns a lazily initialized `ANG::LanguageNegotiator` singleton instance. class_getter(language_negotiator) { ANG::LanguageNegotiator.new } + + # Contains all custom exceptions defined within `Athena::Negotiation`. + module Exceptions; end end diff --git a/src/base_accept.cr b/src/base_accept.cr index e4f703b..fc9507d 100644 --- a/src/base_accept.cr +++ b/src/base_accept.cr @@ -1,14 +1,28 @@ +# Base type for properties/logic all [Accept*](https://tools.ietf.org/html/rfc7231#section-5.3) headers share. abstract struct Athena::Negotiation::BaseAccept - getter quality : Float32 = 1.0 - getter normalized_value : String - getter value : String - getter parameters : Hash(String, String) = Hash(String, String).new - getter type : String + # Returns the full unaltered header `self` represents. + # E.x. `text/html` or `unicode-1-1;q=0.8` or `zh-Hans-CN`. + getter header : String - def initialize(@value : String) - # type, parameters = self.parse_accept_value value - parts = @value.split ';' - @type = parts.shift.strip.downcase + # Returns a normalized version of the `#header`, excluding the `#quality` parameter. + # + # This includes removing extraneous whitespace, and alphabetizing the `#parameters`. + getter normalized_header : String + + # Returns any extension parameters included in the header `self` represents. + # E.x. `charset=UTF-8` or `version=2`. + getter parameters : Hash(String, String) = Hash(String, String).new + + # Returns the [quality value](https://tools.ietf.org/html/rfc7231#section-5.3.1) of the header `self` represents. + getter quality : Float32 = 1.0 + + # Represents the base header value, e.g. `#header` minus the `#quality` and `#parameters`. + # This is exposed as a getter on each subtype to have a more descriptive API. + protected getter accept_value : String + + def initialize(@header : String) + parts = @header.split ';' + @accept_value = parts.shift.strip.downcase parts.each do |part| part = part.split '=' @@ -20,16 +34,16 @@ abstract struct Athena::Negotiation::BaseAccept end if quality = @parameters.delete "q" - @quality = quality.to_f32 + # RFC Only allows max of 3 decimal points. + @quality = quality.to_f32.round 3 end - @normalized_value = String.build do |io| - io << @type + @normalized_header = String.build do |io| + io << @accept_value unless @parameters.empty? io << "; " - # TODO: Do we care the parameters aren't sorted? - parameters.join(io, "; ") { |(k, v), join_io| join_io << "#{k}=#{v}" } + @parameters.keys.sort!.join(io, "; ") { |k, join_io| join_io << "#{k}=#{@parameters[k]}" } end end end diff --git a/src/charset_negotiator.cr b/src/charset_negotiator.cr index 88c6c48..37aa4c5 100644 --- a/src/charset_negotiator.cr +++ b/src/charset_negotiator.cr @@ -1,5 +1,6 @@ require "./abstract_negotiator" +# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptCharset` headers. class Athena::Negotiation::CharsetNegotiator < Athena::Negotiation::AbstractNegotiator private def create_header(header : String) : ANG::BaseAccept ANG::AcceptCharset.new header diff --git a/src/encoding_negotiator.cr b/src/encoding_negotiator.cr index f7607f7..40ec02f 100644 --- a/src/encoding_negotiator.cr +++ b/src/encoding_negotiator.cr @@ -1,5 +1,6 @@ require "./abstract_negotiator" +# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptEncoding` headers. class Athena::Negotiation::EncodingNegotiator < Athena::Negotiation::AbstractNegotiator private def create_header(header : String) : ANG::BaseAccept ANG::AcceptEncoding.new header diff --git a/src/exceptions/invalid_language.cr b/src/exceptions/invalid_language.cr index 8cde019..a8335bd 100644 --- a/src/exceptions/invalid_language.cr +++ b/src/exceptions/invalid_language.cr @@ -1,7 +1,11 @@ require "./negotiation_exception" -class Athena::Negotiation::Exceptions::InvalidLanguage < Athena::Negotiation::Exceptions::Exception - def initialize(type : String, cause : Exception? = nil) - super type, "Invalid language: '#{type}'.", cause +# Represents an invalid `ANG::AcceptLanguage` header. +class Athena::Negotiation::Exceptions::InvalidLanguage < Athena::Negotiation::Exceptions::Negotiation + # Returns the invalid language code. + getter language : String + + def initialize(@language : String, cause : Exception? = nil) + super "Invalid language: '#{@language}'.", cause end end diff --git a/src/exceptions/invalid_media_type.cr b/src/exceptions/invalid_media_type.cr index 28f7d49..366b8fd 100644 --- a/src/exceptions/invalid_media_type.cr +++ b/src/exceptions/invalid_media_type.cr @@ -1,7 +1,11 @@ require "./negotiation_exception" -class Athena::Negotiation::Exceptions::InvalidMediaType < Athena::Negotiation::Exceptions::Exception - def initialize(type : String, cause : Exception? = nil) - super type, "Invalid media type: '#{type}'.", cause +# Represents an invalid `ANG::Accept` header. +class Athena::Negotiation::Exceptions::InvalidMediaType < Athena::Negotiation::Exceptions::Negotiation + # Returns the invalid media range. + getter media_range : String + + def initialize(@media_range : String, cause : Exception? = nil) + super "Invalid media type: '#{@media_range}'.", cause end end diff --git a/src/exceptions/negotiation_exception.cr b/src/exceptions/negotiation_exception.cr index 5a1adf8..8c242fa 100644 --- a/src/exceptions/negotiation_exception.cr +++ b/src/exceptions/negotiation_exception.cr @@ -1,7 +1,4 @@ -abstract class Athena::Negotiation::Exceptions::Exception < ::Exception - getter type : String - - def initialize(@type : String, message : String? = nil, cause : Exception? = nil) - super message, cause - end +# Base type of all `Athena::Negotiation` errors. +# Can be used to rescue any exception originating from `Athena::Negotiation`. +abstract class Athena::Negotiation::Exceptions::Negotiation < ::Exception end diff --git a/src/language_negotiator.cr b/src/language_negotiator.cr index 7a317de..c881f6d 100644 --- a/src/language_negotiator.cr +++ b/src/language_negotiator.cr @@ -1,5 +1,6 @@ require "./abstract_negotiator" +# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptLanguage` headers. class Athena::Negotiation::LanguageNegotiator < Athena::Negotiation::AbstractNegotiator protected def match(accept : ANG::AcceptLanguage, priority : ANG::AcceptLanguage, index : Int32) : ANG::AcceptMatch? accept_base = accept.language diff --git a/src/negotiator.cr b/src/negotiator.cr index 8189348..c76f350 100644 --- a/src/negotiator.cr +++ b/src/negotiator.cr @@ -1,15 +1,16 @@ require "./abstract_negotiator" +# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::Accept` headers. class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator # TODO: Make this method less complex. # # ameba:disable Metrics/CyclomaticComplexity protected def match(accept : ANG::Accept, priority : ANG::Accept, index : Int32) : ANG::AcceptMatch? - accept_base = accept.base_part - priority_base = priority.base_part + accept_type = accept.type + priority_type = priority.type - accept_sub = accept.sub_part - priority_sub = priority.sub_part + accept_sub_type = accept.sub_type + priority_sub_type = priority.sub_type intercection = accept.parameters.each_with_object({} of String => String) do |(k, v), params| priority.parameters.tap do |pp| @@ -17,50 +18,50 @@ class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator end end - base_equal = accept_base.downcase == priority_base.downcase - sub_equal = accept_sub.downcase == priority_sub.downcase + type_equals = accept_type.downcase == priority_type.downcase + sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase if ( - (accept_base == "*" || base_equal) && - (accept_sub == "*" || sub_equal) && + (accept_type == "*" || type_equals) && + (accept_sub_type == "*" || sub_type_equals) && intercection.size == accept.parameters.size ) - score = 100 * (base_equal ? 1 : 0) + 10 * (sub_equal ? 1 : 0) + intercection.size + score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + intercection.size return ANG::AcceptMatch.new accept.quality * priority.quality, score, index end - return nil if !accept_sub.includes?('+') || !priority_sub.includes?('+') + return nil if !accept_sub_type.includes?('+') || !priority_sub_type.includes?('+') - accept_sub, accept_plus = self.split_sub_part accept_sub - priority_sub, priority_plus = self.split_sub_part priority_sub + accept_sub_type, accept_plus = self.split_sub_type accept_sub_type + priority_sub_type, priority_plus = self.split_sub_type priority_sub_type if ( - !(accept_base == "*" || base_equal) || - !(accept_sub == "*" || priority_sub == "*" || accept_plus == "*" || priority_plus == "*") + !(accept_type == "*" || type_equals) || + !(accept_sub_type == "*" || priority_sub_type == "*" || accept_plus == "*" || priority_plus == "*") ) return nil end - sub_equal = accept_sub.downcase == priority_sub.downcase - plus_equal = accept_plus.downcase == priority_plus.downcase + sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase + plus_equals = accept_plus.downcase == priority_plus.downcase if ( - (accept_sub == "*" || priority_sub == "*" || sub_equal) && - (accept_plus == "*" || priority_plus == '*' || plus_equal) && + (accept_sub_type == "*" || priority_sub_type == "*" || sub_type_equals) && + (accept_plus == "*" || priority_plus == '*' || plus_equals) && intercection.size == accept.parameters.size ) - score = 100 * (base_equal ? 1 : 0) + 10 * (sub_equal ? 1 : 0) + (plus_equal ? 1 : 0) + intercection.size + score = 100 * (type_equals ? 1 : 0) + 10 * (sub_type_equals ? 1 : 0) + (plus_equals ? 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? '+' + private def split_sub_type(sub_type : String) : Array(String) + return [sub_type, ""] unless sub_type.includes? '+' - sub_part.split '+', limit: 2 + sub_type.split '+', limit: 2 end private def create_header(header : String) : ANG::BaseAccept