lib/protocol/http/header/accept.rb



# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.

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.
				SEPARATOR = /
					(?:            # 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. A media range can include wild cards, but a media type is a specific type and subtype.
				MediaRange = Struct.new(:type, :subtype, :parameters) do
					# Create a new media range.
					#
					# @parameter type [String] the type of the media range.
					# @parameter subtype [String] the subtype of the media range.
					# @parameter parameters [Hash] the parameters associated with the media range.
					def initialize(type, subtype = "*", parameters = {})
						super(type, subtype, parameters)
					end
					
					# Compare the media range with another media range or a string, based on the quality factor.
					def <=> other
						other.quality_factor <=> self.quality_factor
					end
					
					private 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
					
					# The string representation of the media range, including the type, subtype, and any parameters.
					def to_s
						"#{type}/#{subtype}#{parameters_string}"
					end
					
					alias to_str to_s
					
					# The quality factor associated with the media range, which is used to determine the order of preference.
					#
					# @returns [Float] the quality factor, which defaults to 1.0 if not specified.
					def quality_factor
						parameters.fetch("q", 1.0).to_f
					end
				end
				
				# Parse the `accept` header value into a list of content types.
				#
				# @parameter value [String] the value of the header.
				def initialize(value = nil)
					if value
						super(value.scan(SEPARATOR).map(&:strip))
					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(SEPARATOR).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|
							if quoted_value
								value = QuotedString.unquote(quoted_value)
							end
							
							parameters[key] = value
						end
						
						return MediaRange.new(type, subtype, parameters)
					else
						raise ParseError, "Invalid media type: #{value.inspect}"
					end
				end
			end
		end
	end
end