-
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
193 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# frozen_string_literal: true | ||
|
||
# Released under the MIT License. | ||
# Copyright, 2020-2023, by Samuel Williams. | ||
# Copyright, 2023, by Thomas Morgan. | ||
|
||
require_relative "split" | ||
require_relative "quoted_string" | ||
require_relative "../error" | ||
|
||
module Protocol | ||
module HTTP | ||
module Header | ||
# The `accept-content-type` header represents a list of content-types that the client can accept. | ||
class Accept < Array | ||
# Regular expression used to split values on commas, with optional surrounding whitespace, taking into account quoted strings. | ||
SPLIT = / | ||
(?: # Start non-capturing group | ||
"[^"\\]*" # Match quoted strings (no escaping of quotes within) | ||
| # OR | ||
[^,"]+ # Match non-quoted strings until a comma or quote | ||
)+ | ||
(?=,|\z) # Match until a comma or end of string | ||
/x | ||
|
||
ParseError = Class.new(Error) | ||
|
||
MEDIA_RANGE = /\A(?<type>#{TOKEN})\/(?<subtype>#{TOKEN})(?<parameters>.*)\z/ | ||
|
||
PARAMETER = /\s*;\s*(?<key>#{TOKEN})=((?<value>#{TOKEN})|(?<quoted_value>#{QUOTED_STRING}))/ | ||
|
||
# A single entry in the Accept: header, which includes a mime type and associated parameters. | ||
MediaRange = Struct.new(:type, :subtype, :parameters) do | ||
def initialize(type, subtype = '*', parameters = {}) | ||
super(type, subtype, parameters) | ||
end | ||
|
||
def <=> other | ||
other.quality_factor <=> self.quality_factor | ||
end | ||
|
||
def parameters_string | ||
return '' if parameters == nil or parameters.empty? | ||
|
||
parameters.collect do |key, value| | ||
"; #{key.to_s}=#{QuotedString.quote(value.to_s)}" | ||
end.join | ||
end | ||
|
||
def === other | ||
if other.is_a? self.class | ||
super | ||
else | ||
return self.mime_type === other | ||
end | ||
end | ||
|
||
def mime_type | ||
"#{type}/#{subtype}" | ||
end | ||
|
||
def to_s | ||
"#{type}/#{subtype}#{parameters_string}" | ||
end | ||
|
||
alias to_str to_s | ||
|
||
def quality_factor | ||
parameters.fetch('q', 1.0).to_f | ||
end | ||
|
||
def split(*args) | ||
return [type, subtype] | ||
end | ||
end | ||
|
||
def initialize(value = nil) | ||
if value | ||
super(value.scan(SPLIT).map(&:strip)) | ||
else | ||
end | ||
end | ||
|
||
# Adds one or more comma-separated values to the header. | ||
# | ||
# The input string is split into distinct entries and appended to the array. | ||
# | ||
# @parameter value [String] the value or values to add, separated by commas. | ||
def << (value) | ||
self.concat(value.scan(SPLIT).map(&:strip)) | ||
end | ||
|
||
# Serializes the stored values into a comma-separated string. | ||
# | ||
# @returns [String] the serialized representation of the header values. | ||
def to_s | ||
join(",") | ||
end | ||
|
||
# Parse the `accept` header. | ||
# | ||
# @returns [Array(Charset)] the list of content types and their associated parameters. | ||
def media_ranges | ||
self.map do |value| | ||
self.parse_media_range(value) | ||
end | ||
end | ||
|
||
private | ||
|
||
def parse_media_range(value) | ||
if match = value.match(MEDIA_RANGE) | ||
type = match[:type] | ||
subtype = match[:subtype] | ||
parameters = {} | ||
|
||
match[:parameters].scan(PARAMETER) do |key, value, quoted_value| | ||
parameters[key] = quoted_value || value | ||
end | ||
|
||
return MediaRange.new(type, subtype, parameters) | ||
else | ||
raise ArgumentError, "Invalid media type: #{value.inspect}" | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# frozen_string_literal: true | ||
|
||
# Released under the MIT License. | ||
# Copyright, 2016, by Matthew Kerwin. | ||
# Copyright, 2017-2024, by Samuel Williams. | ||
|
||
require 'protocol/http/header/accept' | ||
|
||
describe Protocol::HTTP::Header::Accept::MediaRange do | ||
it "should have default quality_factor of 1.0" do | ||
language = subject.new('text/plain', nil) | ||
expect(language.quality_factor).to be == 1.0 | ||
end | ||
end | ||
|
||
describe Protocol::HTTP::Header::Accept do | ||
let(:header) {subject.new(description)} | ||
let(:media_ranges) {header.media_ranges.sort} | ||
|
||
with "text/plain, text/html;q=0.5, text/xml;q=0.25" do | ||
it "can parse media ranges" do | ||
expect(header.length).to be == 3 | ||
|
||
expect(media_ranges[0].mime_type).to be == "text/plain" | ||
expect(media_ranges[0].quality_factor).to be == 1.0 | ||
|
||
expect(media_ranges[1].mime_type).to be == "text/html" | ||
expect(media_ranges[1].quality_factor).to be == 0.5 | ||
|
||
expect(media_ranges[2].mime_type).to be == "text/xml" | ||
expect(media_ranges[2].quality_factor).to be == 0.25 | ||
end | ||
end | ||
|
||
with "text/html;q=0.25, text/xml;q=0.5, text/plain" do | ||
it "should order based on quality factor" do | ||
expect(media_ranges.collect(&:mime_type)).to be == %w{text/plain text/xml text/html} | ||
end | ||
end | ||
|
||
with "text/html, text/plain;q=0.8, text/xml;q=0.6, application/json" do | ||
it "should order based on quality factor" do | ||
expect(media_ranges.collect(&:mime_type)).to be == %w{text/html application/json text/plain text/xml} | ||
end | ||
end | ||
|
||
with "*/*;q=0" do | ||
it "should accept wildcard media range" do | ||
expect(media_ranges[0].mime_type).to be == "*/*" | ||
expect(media_ranges[0].quality_factor).to be == 0 | ||
end | ||
end | ||
|
||
|
||
end |