Module: RestClient::Utils

Defined in:
lib/restclient/utils.rb

Overview

Various utility methods

Class Method Summary collapse

Class Method Details

._cgi_parseparam(s) ⇒ Object

Deprecated.

This method is deprecated and only exists to support Ruby 2.0, which is not supported by HTTP::Accept.

TODO:

remove this method when dropping support for Ruby 2.0

Parse semi-colon separated, potentially quoted header string iteratively.


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/restclient/utils.rb', line 75

def self._cgi_parseparam(s)
  return enum_for(__method__, s) unless block_given?

  while s[0] == ';'
    s = s[1..-1]
    ends = s.index(';')
    while ends && ends > 0 \
          && (s[0...ends].count('"') -
              s[0...ends].scan('\"').count) % 2 != 0
      ends = s.index(';', ends + 1)
    end
    if ends.nil?
      ends = s.length
    end
    f = s[0...ends]
    yield f.strip
    s = s[ends..-1]
  end
  nil
end

.cgi_parse_header(line) ⇒ Array(String, Hash)

Parse a Content-Type like header.

Return the main content-type and a hash of params.

Parameters:

  • line (String)

Returns:

  • (Array(String, Hash))

56
57
58
59
60
61
62
63
64
# File 'lib/restclient/utils.rb', line 56

def self.cgi_parse_header(line)
  types = HTTP::Accept::MediaTypes.parse(line)

  if types.empty?
    raise HTTP::Accept::ParseError.new("Found no types in header line")
  end

  [types.first.mime_type, types.first.parameters]
end

.deprecated_cgi_parse_header(line) ⇒ Array(String, Hash)

Deprecated.

This method is deprecated and only exists to support Ruby 2.0, which is not supported by HTTP::Accept.

TODO:

remove this method when dropping support for Ruby 2.0

Parse a Content-Type like header.

Return the main content-type and a hash of options.

This method was ported directly from Python's cgi.parse_header(). It probably doesn't read or perform particularly well in ruby. github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331

Parameters:

  • line (String)

Returns:

  • (Array(String, Hash))

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/restclient/utils.rb', line 112

def self.deprecated_cgi_parse_header(line)
  parts = _cgi_parseparam(';' + line)
  key = parts.next
  pdict = {}

  begin
    while (p = parts.next)
      i = p.index('=')
      if i
        name = p[0...i].strip.downcase
        value = p[i+1..-1].strip
        if value.length >= 2 && value[0] == '"' && value[-1] == '"'
          value = value[1...-1]
          value = value.gsub('\\\\', '\\').gsub('\\"', '"')
        end
        pdict[name] = value
      end
    end
  rescue StopIteration
  end

  [key, pdict]
end

.encode_query_string(object) ⇒ String

Serialize a ruby object into HTTP query string parameters.

There is no standard for doing this, so we choose our own slightly idiosyncratic format. The output closely matches the format understood by Rails, Rack, and PHP.

If you don't want handling of complex objects and only want to handle simple flat hashes, you may want to use `URI.encode_www_form` instead, which implements HTML5-compliant URL encoded form data.

Notable differences from the ActiveSupport implementation:

  • Empty hash and empty array are treated the same as nil instead of being omitted entirely from the output. Rather than disappearing, they will appear to be nil instead.

It's most common to pass a Hash as the object to serialize, but you can also use a ParamsArray if you want to be able to pass the same key with multiple values and not use the rack/rails array convention.

Examples:

Simple hashes

>> encode_query_string({foo: 123, bar: 456})
=> 'foo=123&bar=456'

Simple arrays

>> encode_query_string({foo: [1,2,3]})
=> 'foo[]=1&foo[]=2&foo[]=3'

Nested hashes

>> encode_query_string({outer: {foo: 123, bar: 456}})
=> 'outer[foo]=123&outer[bar]=456'

Deeply nesting

>> encode_query_string({coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]})
=> 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3'

Null and empty values

>> encode_query_string({string: '', empty: nil, list: [], hash: {}})
=> 'string=&empty&list&hash'

Nested nulls

>> encode_query_string({foo: {string: '', empty: nil}})
=> 'foo[string]=&foo[empty]'

Multiple fields with the same name using ParamsArray

>> encode_query_string(RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]))
=> 'foo=1&foo=2&foo=3'

Nested ParamsArray

>> encode_query_string({foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])})
=> 'foo[a]=1&foo[a]=2'

>> encode_query_string(RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]))
=> 'foo[a]=1&foo[a]=2'

Parameters:

  • object (Hash, ParamsArray)

    The object to serialize

Returns:

  • (String)

    A string appropriate for use as an HTTP query string

See Also:

  • {flatten_params}
  • URI.encode_www_form
  • also Object#to_query in ActiveSupport
  • http_build_query in PHP
  • also Rack::Utils.build_nested_query in Rack

Since:

  • 2.0.0


206
207
208
# File 'lib/restclient/utils.rb', line 206

def self.encode_query_string(object)
  flatten_params(object, true).map {|k, v| v.nil? ? k : "#{k}=#{v}" }.join('&')
end

.escape(string) ⇒ Object

Encode string for safe transport by URI or form encoding. This uses a CGI style escape, which transforms ` ` into `+` and various special characters into percent encoded forms.

This calls URI.encode_www_form_component for the implementation. The only difference between this and CGI.escape is that it does not escape `*`. stackoverflow.com/questions/25085992/

See Also:

  • URI.encode_www_form_component

270
271
272
# File 'lib/restclient/utils.rb', line 270

def self.escape(string)
  URI.encode_www_form_component(string)
end

.flatten_params(object, uri_escape = false, parent_key = nil) ⇒ Object

Transform deeply nested param containers into a flat array of [key, value] pairs.

Examples:

>> flatten_params({key1: {key2: 123}})
=> [["key1[key2]", 123]]
>> flatten_params({key1: {key2: 123, arr: [1,2,3]}})
=> [["key1[key2]", 123], ["key1[arr][]", 1], ["key1[arr][]", 2], ["key1[arr][]", 3]]

Parameters:

  • object (Hash, ParamsArray)

    The container to flatten

  • uri_escape (Boolean) (defaults to: false)

    Whether to URI escape keys and values

  • parent_key (String) (defaults to: nil)

    Should not be passed (used for recursion)


225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/restclient/utils.rb', line 225

def self.flatten_params(object, uri_escape=false, parent_key=nil)
  unless object.is_a?(Hash) || object.is_a?(ParamsArray) ||
         (parent_key && object.is_a?(Array))
    raise ArgumentError.new('expected Hash or ParamsArray, got: ' + object.inspect)
  end

  # transform empty collections into nil, where possible
  if object.empty? && parent_key
    return [[parent_key, nil]]
  end

  # This is essentially .map(), but we need to do += for nested containers
  object.reduce([]) { |result, item|
    if object.is_a?(Array)
      # item is already the value
      k = nil
      v = item
    else
      # item is a key, value pair
      k, v = item
      k = escape(k.to_s) if uri_escape
    end

    processed_key = parent_key ? "#{parent_key}[#{k}]" : k

    case v
    when Array, Hash, ParamsArray
      result.concat flatten_params(v, uri_escape, processed_key)
    else
      v = escape(v.to_s) if uri_escape && v
      result << [processed_key, v]
    end
  }
end

.get_encoding_from_headers(headers) ⇒ String?

Return encoding from an HTTP header hash.

We use the RFC 7231 specification and do not impose a default encoding on text. This differs from the older RFC 2616 behavior, which specifies using ISO-8859-1 for text/* content types without a charset.

Strings will use the default encoding when this method returns nil. This default is likely to be UTF-8 for Ruby >= 2.0

Examples:

>> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'})
=> "UTF-8"

Parameters:

  • headers (Hash<Symbol,String>)

Returns:

  • (String, nil)

    Return the string encoding or nil if no header is found.


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/restclient/utils.rb', line 25

def self.get_encoding_from_headers(headers)
  type_header = headers[:content_type]
  return nil unless type_header

  # TODO: remove this hack once we drop support for Ruby 2.0
  if RUBY_VERSION.start_with?('2.0')
    _content_type, params = deprecated_cgi_parse_header(type_header)

    if params.include?('charset')
      return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
    end

  else

    begin
      _content_type, params = cgi_parse_header(type_header)
    rescue HTTP::Accept::ParseError
      return nil
    else
      params['charset']
    end
  end
end