/
nested_params_encoder.rb
176 lines (146 loc) · 4.77 KB
/
nested_params_encoder.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
174
175
176
# frozen_string_literal: true
module Faraday
# Sub-module for encoding parameters into query-string.
module EncodeMethods
# @param params [nil, Array, #to_hash] parameters to be encoded
#
# @return [String] the encoded params
#
# @raise [TypeError] if params can not be converted to a Hash
def encode(params)
return nil if params.nil?
unless params.is_a?(Array)
unless params.respond_to?(:to_hash)
raise TypeError, "Can't convert #{params.class} into Hash."
end
params = params.to_hash
params = params.map do |key, value|
key = key.to_s if key.is_a?(Symbol)
[key, value]
end
# Only to be used for non-Array inputs. Arrays should preserve order.
params.sort! if @sort_params
end
# The params have form [['key1', 'value1'], ['key2', 'value2']].
buffer = +''
params.each do |parent, value|
encoded_parent = escape(parent)
buffer << "#{encode_pair(encoded_parent, value)}&"
end
buffer.chop
end
protected
def encode_pair(parent, value)
if value.is_a?(Hash)
encode_hash(parent, value)
elsif value.is_a?(Array)
encode_array(parent, value)
elsif value.nil?
parent
else
encoded_value = escape(value)
"#{parent}=#{encoded_value}"
end
end
def encode_hash(parent, value)
value = value.map { |key, val| [escape(key), val] }.sort
buffer = +''
value.each do |key, val|
new_parent = "#{parent}%5B#{key}%5D"
buffer << "#{encode_pair(new_parent, val)}&"
end
buffer.chop
end
def encode_array(parent, value)
new_parent = "#{parent}%5B%5D"
return new_parent if value.empty?
buffer = +''
value.each { |val| buffer << "#{encode_pair(new_parent, val)}&" }
buffer.chop
end
end
# Sub-module for decoding query-string into parameters.
module DecodeMethods
# @param query [nil, String]
#
# @return [Array<Array, String>] the decoded params
#
# @raise [TypeError] if the nesting is incorrect
def decode(query)
return nil if query.nil?
params = {}
query.split('&').each do |pair|
next if pair.empty?
key, value = pair.split('=', 2)
key = unescape(key)
value = unescape(value.tr('+', ' ')) if value
decode_pair(key, value, params)
end
dehash(params, 0)
end
protected
SUBKEYS_REGEX = /[^\[\]]+(?:\]?\[\])?/.freeze
def decode_pair(key, value, context)
subkeys = key.scan(SUBKEYS_REGEX)
subkeys.each_with_index do |subkey, i|
is_array = subkey =~ /[\[\]]+\Z/
subkey = $` if is_array
last_subkey = i == subkeys.length - 1
context = prepare_context(context, subkey, is_array, last_subkey)
add_to_context(is_array, context, value, subkey) if last_subkey
end
end
def prepare_context(context, subkey, is_array, last_subkey)
if !last_subkey || is_array
context = new_context(subkey, is_array, context)
end
if context.is_a?(Array) && !is_array
context = match_context(context, subkey)
end
context
end
def new_context(subkey, is_array, context)
value_type = is_array ? Array : Hash
if context[subkey] && !context[subkey].is_a?(value_type)
raise TypeError, "expected #{value_type.name} " \
"(got #{context[subkey].class.name}) for param `#{subkey}'"
end
context[subkey] ||= value_type.new
end
def match_context(context, subkey)
context << {} if !context.last.is_a?(Hash) || context.last.key?(subkey)
context.last
end
def add_to_context(is_array, context, value, subkey)
is_array ? context << value : context[subkey] = value
end
# Internal: convert a nested hash with purely numeric keys into an array.
# FIXME: this is not compatible with Rack::Utils.parse_nested_query
# @!visibility private
def dehash(hash, depth)
hash.each do |key, value|
hash[key] = dehash(value, depth + 1) if value.is_a?(Hash)
end
if depth.positive? && !hash.empty? && hash.keys.all? { |k| k =~ /^\d+$/ }
hash.sort.map(&:last)
else
hash
end
end
end
# This is the default encoder for Faraday requests.
# Using this encoder, parameters will be encoded respecting their structure,
# so you can send objects such as Arrays or Hashes as parameters
# for your requests.
module NestedParamsEncoder
class << self
attr_accessor :sort_params
extend Forwardable
def_delegators :'Faraday::Utils', :escape, :unescape
end
# Useful default for OAuth and caching.
@sort_params = true
extend EncodeMethods
extend DecodeMethods
end
end