/lib/thingfish/acceptparam.rb

https://bitbucket.org/laika/thingfish · Ruby · 213 lines · 103 code · 45 blank · 65 comment · 24 complexity · 54dc8ada328e4492b97b72f632daa178 MD5 · raw file

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