From 8d6d8cbf79c8c9ccc929e6b1b32b4b58258e720e Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Tue, 22 Dec 2020 00:39:12 -0500 Subject: [PATCH] Scaffold out types and start of implementation --- src/abstract_negotiator.cr | 57 ++++++++++++++++++++++++++++++++++++++ src/accept.cr | 20 +++++++++++++ src/accept_charset.cr | 4 +++ src/accept_encoding.cr | 4 +++ src/accept_language.cr | 29 +++++++++++++++++++ src/accept_match.cr | 7 +++++ src/athena-negotiation.cr | 22 ++++++++++++++- src/base_accept.cr | 35 +++++++++++++++++++++++ src/negotiator.cr | 7 +++++ 9 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/abstract_negotiator.cr create mode 100644 src/accept.cr create mode 100644 src/accept_charset.cr create mode 100644 src/accept_encoding.cr create mode 100644 src/accept_language.cr create mode 100644 src/accept_match.cr create mode 100644 src/base_accept.cr create mode 100644 src/negotiator.cr diff --git a/src/abstract_negotiator.cr b/src/abstract_negotiator.cr new file mode 100644 index 0000000..0a2d846 --- /dev/null +++ b/src/abstract_negotiator.cr @@ -0,0 +1,57 @@ +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? + + accepted_headers = Array(ANG::BaseAccept).new + + self.parse_header(header) do |h| + accepted_headers << self.create_header h + rescue ex + raise ex if strict + end + + accepted_priorties = priorities.map &->create_header(String) + + matches = self.find_matches accepted_headers, accepted_priorties + + pp matches + + nil + end + + private def parse_header(header : String, & : String ->) : Nil + header.scan /(?:[^,\"]*+(?:"[^"]*+\")?)+[^,\"]*+/ do |match| + yield match[0] unless match[0].blank? + end + end + + private def find_matches(headers : Array(ANG::BaseAccept), priorities : Array(ANG::BaseAccept)) : Array(ANG::AcceptMatch) + matches = [] of ANG::AcceptMatch + + priorities.each_with_index do |priority, idx| + headers.each do |header| + if match = self.match(header, priority, idx) + matches << match + end + end + end + + 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 new file mode 100644 index 0000000..6a6366a --- /dev/null +++ b/src/accept.cr @@ -0,0 +1,20 @@ +require "./base_accept" + +struct Athena::Negotiation::Accept < Athena::Negotiation::BaseAccept + getter base_part : String + getter sub_part : String + + def initialize(value : String) + super value + + @type = "*/*" if @type == "*" + + parts = @type.split '/' + + # TODO: Use more specific exception + raise "Invalid media type: '#{@type}'." if parts.size != 2 || !parts[0].presence || !parts[1].presence + + @base_part = parts[0] + @sub_part = parts[1] + end +end diff --git a/src/accept_charset.cr b/src/accept_charset.cr new file mode 100644 index 0000000..e20a26a --- /dev/null +++ b/src/accept_charset.cr @@ -0,0 +1,4 @@ +require "./base_accept" + +struct Athena::Negotiation::AcceptCharset < Athena::Negotiation::BaseAccept +end diff --git a/src/accept_encoding.cr b/src/accept_encoding.cr new file mode 100644 index 0000000..31feae0 --- /dev/null +++ b/src/accept_encoding.cr @@ -0,0 +1,4 @@ +require "./base_accept" + +struct Athena::Negotiation::AcceptEncoding < Athena::Negotiation::BaseAccept +end diff --git a/src/accept_language.cr b/src/accept_language.cr new file mode 100644 index 0000000..3277585 --- /dev/null +++ b/src/accept_language.cr @@ -0,0 +1,29 @@ +require "./base_accept" + +struct Athena::Negotiation::AcceptLanguage < Athena::Negotiation::BaseAccept + getter language : String + getter script : String? = nil + getter region : String? = nil + + def initialize(value : String) + super value + + parts = @value.split '-' + + pp parts + + case parts.size + 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 "Invalid language: '#{@value}'." + end + end +end diff --git a/src/accept_match.cr b/src/accept_match.cr new file mode 100644 index 0000000..43b9474 --- /dev/null +++ b/src/accept_match.cr @@ -0,0 +1,7 @@ +struct Athena::Negotiation::AcceptMatch + getter quality : Float32 + getter score : Int32 + getter index : Int32 + + def initialize(@quality : Float32, @score : Int32, @index : Int32); end +end diff --git a/src/athena-negotiation.cr b/src/athena-negotiation.cr index 957a270..cfd24fb 100644 --- a/src/athena-negotiation.cr +++ b/src/athena-negotiation.cr @@ -1,6 +1,26 @@ +require "./accept" +require "./accept_match" +require "./accept_charset" +require "./accept_encoding" +require "./accept_language" +require "./negotiator" + # Convenience alias to make referencing `Athena::Negotiation` types easier. alias ANG = Athena::Negotiation module Athena::Negotiation - VERSION = "0.1.0" 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" + +# 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 diff --git a/src/base_accept.cr b/src/base_accept.cr new file mode 100644 index 0000000..3066652 --- /dev/null +++ b/src/base_accept.cr @@ -0,0 +1,35 @@ +abstract struct Athena::Negotiation::BaseAccept + getter quality : Float32 = 1.0 + getter normalized_value : String + getter value : String + getter parameters : Hash(String, String) + getter type : String + + def initialize(@value : String) + # type, parameters = self.parse_accept_value value + parts = @value.split ';' + @type = parts.shift.strip.downcase + + @parameters = parts.to_h do |part| + part = part.split '=' + + # TODO: Use more specific exception + raise ArgumentError.new "Invalid header: '#{@value}'." unless part.size == 2 + + {part[0].strip.downcase, part[1].strip(" \"")} + end + + if quality = @parameters.delete "q" + @quality = quality.to_f32 + end + + @normalized_value = String.build do |io| + io << @type + + unless @parameters.empty? + io << "; " + parameters.join(io, "; ") { |(k, v), io| io << "#{k}=#{v}" } + end + end + end +end diff --git a/src/negotiator.cr b/src/negotiator.cr new file mode 100644 index 0000000..dad27bb --- /dev/null +++ b/src/negotiator.cr @@ -0,0 +1,7 @@ +require "./abstract_negotiator" + +class Athena::Negotiation::Negotiator < Athena::Negotiation::AbstractNegotiator + private def create_header(header : String) : ANG::BaseAccept + ANG::Accept.new header + end +end