Module: Rack::Utils

Included in:
Chunked, ContentLength
Defined in:
lib/rack/utils.rb

Overview

Rack::Utils contains a grab-bag of useful methods for writing web applications adopted from all kinds of Ruby libraries.

Defined Under Namespace

Modules: Multipart Classes: Context, HeaderHash

Constant Summary

DEFAULT_SEP =
/[&;] */n
ESCAPE_HTML =
{
  "&" => "&",
  "<" => "&lt;",
  ">" => "&gt;",
  "'" => "&#39;",
  '"' => "&quot;",
}
ESCAPE_HTML_PATTERN =
Regexp.union(ESCAPE_HTML.keys)
HTTP_STATUS_CODES =

Every standard HTTP code mapped to the appropriate message. Generated with:

curl -s http://www.iana.org/assignments/http-status-codes | \
  ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
             puts "      #{m[1]}  => \x27#{m[2].strip}x27,"'
{
  100  => 'Continue',
  101  => 'Switching Protocols',
  102  => 'Processing',
  200  => 'OK',
  201  => 'Created',
  202  => 'Accepted',
  203  => 'Non-Authoritative Information',
  204  => 'No Content',
  205  => 'Reset Content',
  206  => 'Partial Content',
  207  => 'Multi-Status',
  226  => 'IM Used',
  300  => 'Multiple Choices',
  301  => 'Moved Permanently',
  302  => 'Found',
  303  => 'See Other',
  304  => 'Not Modified',
  305  => 'Use Proxy',
  306  => 'Reserved',
  307  => 'Temporary Redirect',
  400  => 'Bad Request',
  401  => 'Unauthorized',
  402  => 'Payment Required',
  403  => 'Forbidden',
  404  => 'Not Found',
  405  => 'Method Not Allowed',
  406  => 'Not Acceptable',
  407  => 'Proxy Authentication Required',
  408  => 'Request Timeout',
  409  => 'Conflict',
  410  => 'Gone',
  411  => 'Length Required',
  412  => 'Precondition Failed',
  413  => 'Request Entity Too Large',
  414  => 'Request-URI Too Long',
  415  => 'Unsupported Media Type',
  416  => 'Requested Range Not Satisfiable',
  417  => 'Expectation Failed',
  422  => 'Unprocessable Entity',
  423  => 'Locked',
  424  => 'Failed Dependency',
  426  => 'Upgrade Required',
  500  => 'Internal Server Error',
  501  => 'Not Implemented',
  502  => 'Bad Gateway',
  503  => 'Service Unavailable',
  504  => 'Gateway Timeout',
  505  => 'HTTP Version Not Supported',
  506  => 'Variant Also Negotiates',
  507  => 'Insufficient Storage',
  510  => 'Not Extended',
}
STATUS_WITH_NO_ENTITY_BODY =

Responses with HTTP status codes that should not have an entity body

Set.new((100..199).to_a << 204 << 304)
SYMBOL_TO_STATUS_CODE =
HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
  hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
  hash
}

Class Method Summary (collapse)

Class Method Details

+ (Object) build_nested_query(value, prefix = nil)



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/rack/utils.rb', line 112

def build_nested_query(value, prefix = nil)
  case value
  when Array
    value.map { |v|
      build_nested_query(v, "#{prefix}[]")
    }.join("&")
  when Hash
    value.map { |k, v|
      build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
    }.join("&")
  when String
    raise ArgumentError, "value must be a Hash" if prefix.nil?
    "#{prefix}=#{escape(value)}"
  else
    prefix
  end
end

+ (Object) build_query(params)



101
102
103
104
105
106
107
108
109
# File 'lib/rack/utils.rb', line 101

def build_query(params)
  params.map { |k, v|
    if v.class == Array
      build_query(v.map { |x| [k, x] })
    else
      "#{escape(k)}=#{escape(v)}"
    end
  }.join("&")
end

+ (Object) bytesize(string)



238
239
240
# File 'lib/rack/utils.rb', line 238

def bytesize(string)
  string.bytesize
end


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/rack/utils.rb', line 207

def delete_cookie_header!(header, key, value = {})
  case header["Set-Cookie"]
  when nil, ''
    cookies = []
  when String
    cookies = header["Set-Cookie"].split("\n")
  when Array
    cookies = header["Set-Cookie"]
  end

  cookies.reject! { |cookie|
    if value[:domain]
      cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
    else
      cookie =~ /\A#{escape(key)}=/
    end
  }

  header["Set-Cookie"] = cookies.join("\n")

  set_cookie_header!(header, key,
             {:value => '', :path => nil, :domain => nil,
               :expires => Time.at(0) }.merge(value))

  nil
end

+ (Object) escape(s)

Performs URI escaping so that you can construct proper query strings faster. Use this rather than the cgi.rb version since it's faster. (Stolen from Camping).



15
16
17
18
19
# File 'lib/rack/utils.rb', line 15

def escape(s)
  s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
    '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
  }.tr(' ', '+')
end

+ (Object) escape_html(string)

Escape ampersands, brackets and quotes to their HTML/XML entities.



141
142
143
# File 'lib/rack/utils.rb', line 141

def escape_html(string)
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
end

+ (Object) normalize_params(params, name, v = nil)



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rack/utils.rb', line 69

def normalize_params(params, name, v = nil)
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
  k = $1 || ''
  after = $' || ''

  return if k.empty?

  if after == ""
    params[k] = v
  elsif after == "[]"
    params[k] ||= []
    raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
    params[k] << v
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
    child_key = $1
    params[k] ||= []
    raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
    if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
      normalize_params(params[k].last, child_key, v)
    else
      params[k] << normalize_params({}, child_key, v)
    end
  else
    params[k] ||= {}
    raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
    params[k] = normalize_params(params[k], after, v)
  end

  return params
end

+ (Object) parse_nested_query(qs, d = nil)



57
58
59
60
61
62
63
64
65
66
# File 'lib/rack/utils.rb', line 57

def parse_nested_query(qs, d = nil)
  params = {}

  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
    k, v = unescape(p).split('=', 2)
    normalize_params(params, k, v)
  end

  return params
end

+ (Object) parse_query(qs, d = nil)

Stolen from Mongrel, with some small modifications: Parses a query string by breaking it up at the '&' and ';' characters. You can also use this to parse cookies by changing the characters used in the second parameter (which defaults to '&;').



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/rack/utils.rb', line 37

def parse_query(qs, d = nil)
  params = {}

  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
    k, v = p.split('=', 2).map { |x| unescape(x) }
    if cur = params[k]
      if cur.class == Array
        params[k] << v
      else
        params[k] = [cur, v]
      end
    else
      params[k] = v
    end
  end

  return params
end

+ (Object) rfc2822(time)

Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead of '% %b %Y'. It assumes that the time is in GMT to comply to the RFC 2109.

NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough that I'm certain someone implemented only that option. Do not use %a and %b from Time.strptime, it would use localized names for weekday and month.



257
258
259
260
261
# File 'lib/rack/utils.rb', line 257

def rfc2822(time)
  wday = Time::RFC2822_DAY_NAME[time.wday]
  mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
  time.strftime("#{wday}, %d-#{mon}-%Y %T GMT")
end

+ (Object) select_best_encoding(available_encodings, accept_encoding)



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/rack/utils.rb', line 146

def select_best_encoding(available_encodings, accept_encoding)
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

  expanded_accept_encoding =
    accept_encoding.map { |m, q|
      if m == "*"
        (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
      else
        [[m, q]]
      end
    }.inject([]) { |mem, list|
      mem + list
    }

  encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }

  unless encoding_candidates.include?("identity")
    encoding_candidates.push("identity")
  end

  expanded_accept_encoding.find_all { |m, q|
    q == 0.0
  }.each { |m, _|
    encoding_candidates.delete(m)
  }

  return (encoding_candidates & available_encodings)[0]
end


176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/rack/utils.rb', line 176

def set_cookie_header!(header, key, value)
  case value
  when Hash
    domain  = "; domain="  + value[:domain] if value[:domain]
    path    = "; path="    + value[:path]   if value[:path]
    # According to RFC 2109, we need dashes here.
    # N.B.: cgi.rb uses spaces...
    expires = "; expires=" +
      rfc2822(value[:expires].clone.gmtime) if value[:expires]
    secure = "; secure"  if value[:secure]
    httponly = "; HttpOnly" if value[:httponly]
    value = value[:value]
  end
  value = [value] unless Array === value
  cookie = escape(key) + "=" +
    value.map { |v| escape v }.join("&") +
    "#{domain}#{path}#{expires}#{secure}#{httponly}"

  case header["Set-Cookie"]
  when nil, ''
    header["Set-Cookie"] = cookie
  when String
    header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
  when Array
    header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
  end

  nil
end

+ (Object) status_code(status)



430
431
432
433
434
435
436
# File 'lib/rack/utils.rb', line 430

def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE[status] || 500
  else
    status.to_i
  end
end

+ (Object) unescape(s)

Unescapes a URI escaped string. (Stolen from Camping).



23
24
25
26
27
# File 'lib/rack/utils.rb', line 23

def unescape(s)
  s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
    [$1.delete('%')].pack('H*')
  }
end