PageRenderTime 43ms CodeModel.GetById 18ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/thingfish/acceptparam.rb

https://bitbucket.org/laika/thingfish
Ruby | 213 lines | 103 code | 45 blank | 65 comment | 24 complexity | 54dc8ada328e4492b97b72f632daa178 MD5 | raw file
Possible License(s): BSD-3-Clause
  1#!/usr/bin/env ruby
  2
  3require 'thingfish'
  4require 'thingfish/exceptions'
  5require 'thingfish/mixins'
  6require 'thingfish/utils'
  7
  8
  9# ThingFish::AcceptParam -- a parser for request Accept headers,
 10# allowing for weighted and wildcard comparisions.
 11#
 12# == Synopsis
 13#
 14#   require 'thingfish/acceptparam'
 15#	ap = AcceptParam.parse( "text/html;q=0.9;level=2" )
 16#
 17#	ap.type         #=> 'text'
 18#	ap.subtype      #=> 'html'
 19#	ap.qvalue       #=> 0.9
 20#	ap =~ 'text/*'  #=> true
 21#
 22# == Version
 23#
 24#  $Id$
 25#
 26# == Authors
 27#
 28# * Michael Granger <ged@FaerieMUD.org>
 29# * Mahlon E. Smith <mahlon@martini.nu>
 30#
 31# :include: LICENSE
 32#
 33#---
 34#
 35# Please see the file LICENSE in the top-level directory for licensing details.
 36#
 37class ThingFish::AcceptParam
 38	include Comparable,
 39		ThingFish::Loggable,
 40		ThingFish::HtmlInspectableObject
 41
 42	# The default quality value (weight) if none is specified
 43	Q_DEFAULT = 1.0
 44	Q_MAX = Q_DEFAULT
 45
 46
 47	### Parse the given +accept_param+ and return an AcceptParam object.
 48	def self::parse( accept_param )
 49		raise ThingFish::RequestError, "Bad Accept param: no media-range" unless
 50			accept_param =~ %r{/}
 51		media_range, *stuff = accept_param.split( /\s*;\s*/ )
 52		type, subtype = media_range.downcase.split( '/', 2 )
 53		qval, opts = stuff.partition {|par| par =~ /^q\s*=/ }
 54
 55		return new( type, subtype, qval.first, *opts )
 56	end
 57
 58
 59	### Create a new ThingFish::Request::AcceptParam with the
 60	### given media +range+, quality value (+qval+), and extensions
 61	def initialize( type, subtype, qval=Q_DEFAULT, *extensions )
 62		type    = nil if type == '*'
 63		subtype = nil if subtype == '*'
 64
 65		@type       = type
 66		@subtype    = subtype
 67		@qvalue     = normalize_qvalue( qval )
 68		@extensions = extensions.flatten
 69	end
 70
 71
 72	######
 73	public
 74	######
 75
 76	# The 'type' part of the media range
 77	attr_reader :type
 78
 79	# The 'subtype' part of the media range
 80	attr_reader :subtype
 81
 82	# The weight of the param
 83	attr_reader :qvalue
 84
 85	# An array of any accept-extensions specified with the parameter
 86	attr_reader :extensions
 87
 88
 89	### Match operator -- returns true if +other+ (an AcceptParan or something
 90	### that can to_s to a mime type) is a mime type which matches the receiving
 91	### AcceptParam.
 92	def =~( other )
 93		unless other.is_a?( ThingFish::AcceptParam )
 94			other = self.class.parse( other.to_s ) rescue nil
 95			return false unless other
 96		end
 97
 98		# */* returns true in either side of the comparison.
 99		# ASSUMPTION: There will never be a case when a type is wildcarded
100		#             and the subtype is specific. (e.g., */xml)
101		#             We gave up trying to read RFC 2045.
102		return true if other.type.nil? || self.type.nil?
103
104		# text/html =~ text/html
105		# text/* =~ text/html
106		# text/html =~ text/*
107		if other.type == self.type
108			return true if other.subtype.nil? || self.subtype.nil?
109			return true if other.subtype == self.subtype
110		end
111
112		return false
113	end
114
115
116	### Return a human-readable version of the object
117	def inspect
118		return "#<%s:0x%07x '%s/%s' q=%0.3f %p>" % [
119			self.class.name,
120			self.object_id * 2,
121			self.type || '*',
122			self.subtype || '*',
123			self.qvalue,
124			self.extensions,
125		]
126	end
127
128
129	### Return the parameter as a String suitable for inclusion in an Accept
130	### HTTP header
131	def to_s
132		return [
133			self.mediatype,
134			self.qvaluestring,
135			self.extension_strings
136		].compact.join(';')
137	end
138
139
140	### The mediatype of the parameter, consisting of the type and subtype
141	### separated by '/'.
142	def mediatype
143		return "%s/%s" % [ self.type || '*', self.subtype || '*' ]
144	end
145	alias_method :mimetype, :mediatype
146	alias_method :content_type, :mediatype
147
148
149	### The weighting or "qvalue" of the parameter in the form "q=<value>"
150	def qvaluestring
151		# 3 digit precision, trim excess zeros
152		return sprintf( "q=%0.3f", self.qvalue ).gsub(/0{1,2}$/, '')
153	end
154
155
156	### Return a String containing any extensions for this parameter, joined
157	### with ';'
158	def extension_strings
159		return nil if @extensions.empty?
160		return @extensions.compact.join('; ')
161	end
162
163
164	### Comparable interface. Sort parameters by weight: Returns -1 if +other+
165	### is less specific than the receiver, 0 if +other+ is as specific as
166	### the receiver, and +1 if +other+ is more specific than the receiver.
167	def <=>( other )
168
169		if rval = (other.qvalue <=> @qvalue).nonzero?
170			return rval
171		end
172
173		if @type.nil?
174			return 1 if ! other.type.nil?
175		elsif other.type.nil?
176			return -1
177		end
178
179		if @subtype.nil?
180			return 1 if ! other.subtype.nil?
181		elsif other.subtype.nil?
182			return -1
183		end
184
185		if rval = (other.extensions.length <=> @extensions.length).nonzero?
186			return rval
187		end
188
189		return self.mediatype <=> other.mediatype
190	end
191
192
193	#######
194	private
195	#######
196
197	### Given an input +qvalue+, return the Float equivalent.
198	def normalize_qvalue( qvalue )
199		return Q_DEFAULT unless qvalue
200		qvalue = Float( qvalue.to_s.sub(/q=/, '') ) unless qvalue.is_a?( Float )
201
202		if qvalue > Q_MAX
203			self.log.warn "Squishing invalid qvalue %p to %0.1f" %
204				[ qvalue, Q_DEFAULT ]
205			return Q_DEFAULT
206		end
207
208		return qvalue
209	end
210
211end # ThingFish::AcceptParam
212
213# vim: set nosta noet ts=4 sw=4: