Module: RestClient::Utils

Defined in:
lib/restclient/utils.rb

Overview

Various utility methods

Class Method Summary collapse

Class Method Details

._cgi_parseparam(s) ⇒ Object

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



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/restclient/utils.rb', line 40

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 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))


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

def self.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



167
168
169
# File 'lib/restclient/utils.rb', line 167

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


231
232
233
# File 'lib/restclient/utils.rb', line 231

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)



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/restclient/utils.rb', line 186

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)

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



23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/restclient/utils.rb', line 23

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

  _content_type, params = cgi_parse_header(type_header)

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

  nil
end