Refactor API to be more RFC compliant

Add docs
This commit is contained in:
George Dietrich 2020-12-24 00:20:25 -05:00
parent ae80dccf0c
commit e1c5459058
22 changed files with 295 additions and 103 deletions

View file

@ -1,12 +1,12 @@
require "./spec_helper" require "./spec_helper"
struct AcceptLanguageTest < ASPEC::TestCase struct AcceptLanguageTest < ASPEC::TestCase
@[DataProvider("type_data_provider")] @[DataProvider("accept_value_data_provider")]
def test_get_type(header : String?, expected : String?) : Nil def test_accept_value(header : String?, expected : String?) : Nil
ANG::AcceptLanguage.new(header).type.should eq expected ANG::AcceptLanguage.new(header).accept_value.should eq expected
end end
def type_data_provider : Tuple def accept_value_data_provider : Tuple
{ {
{"en;q=0.7", "en"}, {"en;q=0.7", "en"},
{"en-GB;q=0.8", "en-gb"}, {"en-GB;q=0.8", "en-gb"},
@ -17,12 +17,12 @@ struct AcceptLanguageTest < ASPEC::TestCase
} }
end end
@[DataProvider("value_data_provider")] @[DataProvider("header_data_provider")]
def test_get_value(header : String?, expected : String?) : Nil 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 end
def value_data_provider : Tuple def header_data_provider : Tuple
{ {
{"en;q=0.7", "en;q=0.7"}, {"en;q=0.7", "en;q=0.7"},
{"en-GB;q=0.8", "en-GB;q=0.8"}, {"en-GB;q=0.8", "en-GB;q=0.8"},

View file

@ -5,24 +5,24 @@ struct AcceptTest < ASPEC::TestCase
ANG::Accept.new("foo/bar; q=1; hello=world").parameters["hello"]?.should eq "world" ANG::Accept.new("foo/bar; q=1; hello=world").parameters["hello"]?.should eq "world"
end end
@[DataProvider("normalized_value_data_provider")] @[DataProvider("normalized_header_data_provider")]
def test_normalized_value(header : String, expected : String) : Nil def test_normalized_header(header : String, expected : String) : Nil
ANG::Accept.new(header).normalized_value.should eq expected ANG::Accept.new(header).normalized_header.should eq expected
end 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"}, {"application/pdf; q=1; param=p", "application/pdf; param=p"},
} }
end end
@[DataProvider("type_data_provider")] @[DataProvider("media_range_data_provider")]
def test_type(header : String, expected : String) : Nil def test_media_range(header : String, expected : String) : Nil
ANG::Accept.new(header).type.should eq expected ANG::Accept.new(header).media_range.should eq expected
end end
def type_data_provider : Tuple def media_range_data_provider : Tuple
{ {
{"text/html;hello=world", "text/html"}, {"text/html;hello=world", "text/html"},
{"application/pdf", "application/pdf"}, {"application/pdf", "application/pdf"},
@ -40,12 +40,12 @@ struct AcceptTest < ASPEC::TestCase
} }
end end
@[DataProvider("value_data_provider")] @[DataProvider("header_data_provider")]
def test_value(header : String, expected : String) : Nil def test_accept_value(header : String, expected : String) : Nil
ANG::Accept.new(header).value.should eq expected ANG::Accept.new(header).header.should eq expected
end 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"}, {"text/html;hello=world ;q=0.5", "text/html;hello=world ;q=0.5"},
{"application/pdf", "application/pdf"}, {"application/pdf", "application/pdf"},

View file

@ -5,12 +5,12 @@ private struct MockAccept < ANG::BaseAccept; end
struct BaseAcceptTest < ASPEC::TestCase struct BaseAcceptTest < ASPEC::TestCase
@[DataProvider("build_parameters_data_provider")] @[DataProvider("build_parameters_data_provider")]
def test_build_parameters_string(header : String, expected : String) : Nil 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 end
def build_parameters_data_provider : Tuple 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 end

View file

@ -16,21 +16,21 @@ struct CharsetNegotiatorTest < NegotiatorTestCase
accept = accept.should_not be_nil accept = accept.should_not be_nil
accept.should be_a ANG::AcceptCharset accept.should be_a ANG::AcceptCharset
accept.value.should eq "fr" accept.charset.should eq "fr"
end end
def test_best_respects_priorities : Nil def test_best_respects_priorities : Nil
accept = @negotiator.best "foo, bar, yo", {"yo"} accept = @negotiator.best "foo, bar, yo", {"yo"}
accept = accept.should_not be_nil accept = accept.should_not be_nil
accept.should be_a ANG::AcceptCharset accept.should be_a ANG::AcceptCharset
accept.type.should eq "yo" accept.charset.should eq "yo"
end end
def test_best_respects_quality : Nil 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 = @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 = accept.should_not be_nil
accept.should be_a ANG::AcceptCharset accept.should be_a ANG::AcceptCharset
accept.type.should eq "utf-8" accept.charset.should eq "utf-8"
end end
@[DataProvider("best_data_provider")] @[DataProvider("best_data_provider")]
@ -41,7 +41,7 @@ struct CharsetNegotiatorTest < NegotiatorTestCase
expected.should be_nil expected.should be_nil
else else
accept.should be_a ANG::AcceptCharset accept.should be_a ANG::AcceptCharset
accept.value.should eq expected accept.header.should eq expected
end end
end end

View file

@ -15,7 +15,7 @@ struct EncodingNegotiatorTest < NegotiatorTestCase
accept = @negotiator.best "gzip;q=0.7,identity", {"identity;q=0.5", "gzip;q=0.9"} accept = @negotiator.best "gzip;q=0.7,identity", {"identity;q=0.5", "gzip;q=0.9"}
accept = accept.should_not be_nil accept = accept.should_not be_nil
accept.should be_a ANG::AcceptEncoding accept.should be_a ANG::AcceptEncoding
accept.type.should eq "gzip" accept.coding.should eq "gzip"
end end
@[DataProvider("best_data_provider")] @[DataProvider("best_data_provider")]
@ -26,7 +26,7 @@ struct EncodingNegotiatorTest < NegotiatorTestCase
expected.should be_nil expected.should be_nil
else else
accept.should be_a ANG::AcceptEncoding accept.should be_a ANG::AcceptEncoding
accept.value.should eq expected accept.header.should eq expected
end end
end end

View file

@ -11,7 +11,7 @@ struct LanguageNegotiatorTest < NegotiatorTestCase
accept = @negotiator.best "en;q=0.5,de", {"de;q=0.3", "en;q=0.9"} accept = @negotiator.best "en;q=0.5,de", {"de;q=0.3", "en;q=0.9"}
accept = accept.should_not be_nil accept = accept.should_not be_nil
accept.should be_a ANG::AcceptLanguage accept.should be_a ANG::AcceptLanguage
accept.type.should eq "en" accept.language.should eq "en"
end end
@[DataProvider("best_data_provider")] @[DataProvider("best_data_provider")]
@ -22,7 +22,7 @@ struct LanguageNegotiatorTest < NegotiatorTestCase
expected.should be_nil expected.should be_nil
else else
accept.should be_a ANG::AcceptLanguage accept.should be_a ANG::AcceptLanguage
accept.value.should eq expected accept.header.should eq expected
end end
end end

View file

@ -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 = @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 = accept.should_not be_nil
accept.should be_a ANG::Accept accept.should be_a ANG::Accept
accept.type.should eq "text/plain" accept.media_range.should eq "text/plain"
end end
def test_best_invalid_unstrict def test_best_invalid_unstrict
@ -23,7 +23,7 @@ struct NegotiatorTest < NegotiatorTestCase
@negotiator.best "foo/bar", {"/qwer"} @negotiator.best "foo/bar", {"/qwer"}
end end
ex.type.should eq "/qwer" ex.media_range.should eq "/qwer"
end end
@[DataProvider("best_data_provider")] @[DataProvider("best_data_provider")]
@ -46,7 +46,7 @@ struct NegotiatorTest < NegotiatorTestCase
expected = expected.should_not be_nil 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) accept_header.parameters.should eq (expected[1] || Hash(String, String).new)
end end
@ -114,7 +114,7 @@ struct NegotiatorTest < NegotiatorTestCase
expected.each_with_index do |element, idx| expected.each_with_index do |element, idx|
elements[idx].should be_a ANG::Accept elements[idx].should be_a ANG::Accept
element.should eq elements[idx].value element.should eq elements[idx].header
end end
end end

View file

@ -1,3 +1,4 @@
# Base negotiator type. Implements logic common to all negotiators.
abstract class Athena::Negotiation::AbstractNegotiator abstract class Athena::Negotiation::AbstractNegotiator
private record OrderKey, quality : Float32, index : Int32, value : String do private record OrderKey, quality : Float32, index : Int32, value : String do
include Comparable(self) include Comparable(self)
@ -10,6 +11,9 @@ abstract class Athena::Negotiation::AbstractNegotiator
private abstract def create_header(header : String) : ANG::BaseAccept 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? 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 "priorities should not be empty." if priorities.empty?
raise ArgumentError.new "The header string should not be empty." if header.blank? 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] match.nil? ? nil : accepted_priorties[match.index]
end 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) def ordered_elements(header : String) : Array(ANG::BaseAccept)
raise ArgumentError.new "The header string should not be empty." if header.blank? 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| self.parse_header(header) do |h|
element = self.create_header h element = self.create_header h
elements << element elements << element
order_keys << OrderKey.new element.quality, idx, element.value order_keys << OrderKey.new element.quality, idx, element.header
rescue ex rescue ex
# skip # skip
ensure ensure
@ -60,12 +67,12 @@ abstract class Athena::Negotiation::AbstractNegotiator
end end
protected def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch? protected def match(header : ANG::BaseAccept, priority : ANG::BaseAccept, index : Int32) : ANG::AcceptMatch?
accept_type = header.type accept_value = header.accept_value
priority_type = priority.type 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 return ANG::AcceptMatch.new header.quality * priority.quality, 1 * (equal ? 1 : 0), index
end end

View file

@ -1,21 +1,45 @@
require "./base_accept" 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 struct Athena::Negotiation::Accept < Athena::Negotiation::BaseAccept
getter base_part : String # Returns the type for this `Accept` header.
getter sub_part : String # 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) def initialize(value : String)
super value 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 if parts.size != 2 || !parts[0].presence || !parts[1].presence
raise ANG::Exceptions::InvalidMediaType.new @type raise ANG::Exceptions::InvalidMediaType.new @accept_value
end end
@base_part = parts[0] @type = parts[0]
@sub_part = parts[1] @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
end end

View file

@ -1,4 +1,21 @@
require "./base_accept" 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 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 end

View file

@ -1,4 +1,21 @@
require "./base_accept" 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 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 end

View file

@ -1,27 +1,55 @@
require "./base_accept" 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 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 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 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) def initialize(value : String)
super value super value
parts = @type.split '-' parts = @accept_value.split '-'
case parts.size case parts.size
when 1
@language = parts[0]
when 2 when 2
@language = parts[0] @language = parts[0]
@region = parts[1] @region = parts[1]
when 1
@language = parts[0]
when 3 when 3
@language = parts[0] @language = parts[0]
@script = parts[1] @script = parts[1]
@region = parts[2] @region = parts[2]
else else
raise ANG::Exceptions::InvalidLanguage.new @type raise ANG::Exceptions::InvalidLanguage.new @accept_value
end end
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 end

View file

@ -1,3 +1,4 @@
# :nodoc:
struct Athena::Negotiation::AcceptMatch struct Athena::Negotiation::AcceptMatch
include Comparable(self) include Comparable(self)

View file

@ -13,16 +13,91 @@ require "./exceptions/*"
# Convenience alias to make referencing `Athena::Negotiation` types easier. # Convenience alias to make referencing `Athena::Negotiation` types easier.
alias ANG = Athena::Negotiation 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 module Athena::Negotiation
# Returns an `ANG::Negotiator` singleton instance. # Returns a lazily initialized `ANG::Negotiator` singleton instance.
class_getter(negotiator) { ANG::Negotiator.new } 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 } 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 } 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 } class_getter(language_negotiator) { ANG::LanguageNegotiator.new }
# Contains all custom exceptions defined within `Athena::Negotiation`.
module Exceptions; end
end end

View file

@ -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 abstract struct Athena::Negotiation::BaseAccept
getter quality : Float32 = 1.0 # Returns the full unaltered header `self` represents.
getter normalized_value : String # E.x. `text/html` or `unicode-1-1;q=0.8` or `zh-Hans-CN`.
getter value : String getter header : String
getter parameters : Hash(String, String) = Hash(String, String).new
getter type : String
def initialize(@value : String) # Returns a normalized version of the `#header`, excluding the `#quality` parameter.
# type, parameters = self.parse_accept_value value #
parts = @value.split ';' # This includes removing extraneous whitespace, and alphabetizing the `#parameters`.
@type = parts.shift.strip.downcase 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| parts.each do |part|
part = part.split '=' part = part.split '='
@ -20,16 +34,16 @@ abstract struct Athena::Negotiation::BaseAccept
end end
if quality = @parameters.delete "q" if quality = @parameters.delete "q"
@quality = quality.to_f32 # RFC Only allows max of 3 decimal points.
@quality = quality.to_f32.round 3
end end
@normalized_value = String.build do |io| @normalized_header = String.build do |io|
io << @type io << @accept_value
unless @parameters.empty? unless @parameters.empty?
io << "; " io << "; "
# TODO: Do we care the parameters aren't sorted? @parameters.keys.sort!.join(io, "; ") { |k, join_io| join_io << "#{k}=#{@parameters[k]}" }
parameters.join(io, "; ") { |(k, v), join_io| join_io << "#{k}=#{v}" }
end end
end end
end end

View file

@ -1,5 +1,6 @@
require "./abstract_negotiator" require "./abstract_negotiator"
# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptCharset` headers.
class Athena::Negotiation::CharsetNegotiator < Athena::Negotiation::AbstractNegotiator class Athena::Negotiation::CharsetNegotiator < Athena::Negotiation::AbstractNegotiator
private def create_header(header : String) : ANG::BaseAccept private def create_header(header : String) : ANG::BaseAccept
ANG::AcceptCharset.new header ANG::AcceptCharset.new header

View file

@ -1,5 +1,6 @@
require "./abstract_negotiator" require "./abstract_negotiator"
# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptEncoding` headers.
class Athena::Negotiation::EncodingNegotiator < Athena::Negotiation::AbstractNegotiator class Athena::Negotiation::EncodingNegotiator < Athena::Negotiation::AbstractNegotiator
private def create_header(header : String) : ANG::BaseAccept private def create_header(header : String) : ANG::BaseAccept
ANG::AcceptEncoding.new header ANG::AcceptEncoding.new header

View file

@ -1,7 +1,11 @@
require "./negotiation_exception" require "./negotiation_exception"
class Athena::Negotiation::Exceptions::InvalidLanguage < Athena::Negotiation::Exceptions::Exception # Represents an invalid `ANG::AcceptLanguage` header.
def initialize(type : String, cause : Exception? = nil) class Athena::Negotiation::Exceptions::InvalidLanguage < Athena::Negotiation::Exceptions::Negotiation
super type, "Invalid language: '#{type}'.", cause # Returns the invalid language code.
getter language : String
def initialize(@language : String, cause : Exception? = nil)
super "Invalid language: '#{@language}'.", cause
end end
end end

View file

@ -1,7 +1,11 @@
require "./negotiation_exception" require "./negotiation_exception"
class Athena::Negotiation::Exceptions::InvalidMediaType < Athena::Negotiation::Exceptions::Exception # Represents an invalid `ANG::Accept` header.
def initialize(type : String, cause : Exception? = nil) class Athena::Negotiation::Exceptions::InvalidMediaType < Athena::Negotiation::Exceptions::Negotiation
super type, "Invalid media type: '#{type}'.", cause # 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
end end

View file

@ -1,7 +1,4 @@
abstract class Athena::Negotiation::Exceptions::Exception < ::Exception # Base type of all `Athena::Negotiation` errors.
getter type : String # Can be used to rescue any exception originating from `Athena::Negotiation`.
abstract class Athena::Negotiation::Exceptions::Negotiation < ::Exception
def initialize(@type : String, message : String? = nil, cause : Exception? = nil)
super message, cause
end
end end

View file

@ -1,5 +1,6 @@
require "./abstract_negotiator" require "./abstract_negotiator"
# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::AcceptLanguage` headers.
class Athena::Negotiation::LanguageNegotiator < Athena::Negotiation::AbstractNegotiator class Athena::Negotiation::LanguageNegotiator < Athena::Negotiation::AbstractNegotiator
protected def match(accept : ANG::AcceptLanguage, priority : ANG::AcceptLanguage, index : Int32) : ANG::AcceptMatch? protected def match(accept : ANG::AcceptLanguage, priority : ANG::AcceptLanguage, index : Int32) : ANG::AcceptMatch?
accept_base = accept.language accept_base = accept.language

View file

@ -1,15 +1,16 @@
require "./abstract_negotiator" require "./abstract_negotiator"
# A `ANG::AbstractNegotiator` implementation to negotiate `ANG::Accept` headers.
class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator
# TODO: Make this method less complex. # TODO: Make this method less complex.
# #
# ameba:disable Metrics/CyclomaticComplexity # ameba:disable Metrics/CyclomaticComplexity
protected def match(accept : ANG::Accept, priority : ANG::Accept, index : Int32) : ANG::AcceptMatch? protected def match(accept : ANG::Accept, priority : ANG::Accept, index : Int32) : ANG::AcceptMatch?
accept_base = accept.base_part accept_type = accept.type
priority_base = priority.base_part priority_type = priority.type
accept_sub = accept.sub_part accept_sub_type = accept.sub_type
priority_sub = priority.sub_part priority_sub_type = priority.sub_type
intercection = accept.parameters.each_with_object({} of String => String) do |(k, v), params| intercection = accept.parameters.each_with_object({} of String => String) do |(k, v), params|
priority.parameters.tap do |pp| priority.parameters.tap do |pp|
@ -17,50 +18,50 @@ class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator
end end
end end
base_equal = accept_base.downcase == priority_base.downcase type_equals = accept_type.downcase == priority_type.downcase
sub_equal = accept_sub.downcase == priority_sub.downcase sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase
if ( if (
(accept_base == "*" || base_equal) && (accept_type == "*" || type_equals) &&
(accept_sub == "*" || sub_equal) && (accept_sub_type == "*" || sub_type_equals) &&
intercection.size == accept.parameters.size 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 return ANG::AcceptMatch.new accept.quality * priority.quality, score, index
end 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 accept_sub_type, accept_plus = self.split_sub_type accept_sub_type
priority_sub, priority_plus = self.split_sub_part priority_sub priority_sub_type, priority_plus = self.split_sub_type priority_sub_type
if ( if (
!(accept_base == "*" || base_equal) || !(accept_type == "*" || type_equals) ||
!(accept_sub == "*" || priority_sub == "*" || accept_plus == "*" || priority_plus == "*") !(accept_sub_type == "*" || priority_sub_type == "*" || accept_plus == "*" || priority_plus == "*")
) )
return nil return nil
end end
sub_equal = accept_sub.downcase == priority_sub.downcase sub_type_equals = accept_sub_type.downcase == priority_sub_type.downcase
plus_equal = accept_plus.downcase == priority_plus.downcase plus_equals = accept_plus.downcase == priority_plus.downcase
if ( if (
(accept_sub == "*" || priority_sub == "*" || sub_equal) && (accept_sub_type == "*" || priority_sub_type == "*" || sub_type_equals) &&
(accept_plus == "*" || priority_plus == '*' || plus_equal) && (accept_plus == "*" || priority_plus == '*' || plus_equals) &&
intercection.size == accept.parameters.size 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 return ANG::AcceptMatch.new accept.quality * priority.quality, score, index
end end
nil nil
end end
private def split_sub_part(sub_part : String) : Array(String) private def split_sub_type(sub_type : String) : Array(String)
return [sub_part, ""] unless sub_part.includes? '+' return [sub_type, ""] unless sub_type.includes? '+'
sub_part.split '+', limit: 2 sub_type.split '+', limit: 2
end end
private def create_header(header : String) : ANG::BaseAccept private def create_header(header : String) : ANG::BaseAccept