/lib/thingfish/acceptparam.rb
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: