added Response#form (supports only x-www-urlencoded for now)

This commit is contained in:
HoneyryderChuck 2021-08-06 15:06:51 +01:00
parent a85828d0d5
commit f2d3c1f09b
8 changed files with 124 additions and 30 deletions

View File

@ -59,7 +59,7 @@ module HTTPX
@registry ||= {}
return @registry if tag.nil?
handler = @registry.fetch(tag)
handler = @registry[tag]
raise(Error, "#{tag} is not registered in #{self}") unless handler
handler

View File

@ -45,7 +45,7 @@ module HTTPX
end
def content_type
ContentType.parse(@headers["content-type"])
@content_type ||= ContentType.new(@headers["content-type"])
end
def complete?
@ -72,17 +72,23 @@ module HTTPX
decode("json", options)
end
def form
decode("form")
end
private
def decode(format, options = nil)
# TODO: check if content-type is a valid format, i.e. "application/json" for json parsinng
# TODO: check if content-type is a valid format, i.e. "application/json" for json parsing
transcoder = Transcoder.registry(format)
raise Error, "no decoder available for \"#{format}\"" unless transcoder.respond_to?(:decoder)
raise Error, "no decoder available for \"#{format}\"" unless transcoder.respond_to?(:decode)
decoder = transcoder.decoder
decoder = transcoder.decode(self)
decoder.call(@body, options)
raise Error, "no decoder available for \"#{format}\"" unless decoder
decoder.call(self, options)
rescue Registry::Error
raise Error, "no decoder available for \"#{format}\""
end
@ -281,30 +287,22 @@ module HTTPX
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
attr_reader :mime_type, :charset
def initialize(mime_type, charset)
@mime_type = mime_type
@charset = charset
def initialize(header_value)
@header_value = header_value
end
class << self
# Parse string and return ContentType struct
def parse(str)
new(mime_type(str), charset(str))
end
def mime_type
return @mime_type if defined?(@mime_type)
private
m = @header_value.to_s[MIME_TYPE_RE, 1]
m && @mime_type = m.strip.downcase
end
def mime_type(str)
m = str.to_s[MIME_TYPE_RE, 1]
m && m.strip.downcase
end
def charset
return @charset if defined?(@charset)
def charset(str)
m = str.to_s[CHARSET_RE, 1]
m && m.strip.delete('"')
end
m = @header_value.to_s[CHARSET_RE, 1]
m && @charset = m.strip.delete('"')
end
end

View File

@ -4,7 +4,9 @@ module HTTPX
module Transcoder
extend Registry
def self.normalize_keys(key, value, cond = nil, &block)
module_function
def normalize_keys(key, value, cond = nil, &block)
if (cond && cond.call(value))
block.call(key.to_s, value)
elsif value.respond_to?(:to_ary)
@ -23,6 +25,63 @@ module HTTPX
block.call(key.to_s, value)
end
end
# based on https://github.com/rack/rack/blob/d15dd728440710cfc35ed155d66a98dc2c07ae42/lib/rack/query_parser.rb#L82
def normalize_query(params, name, v, depth)
raise Error, "params depth surpasses what's supported" if depth <= 0
name =~ /\A[\[\]]*([^\[\]]+)\]*/
k = Regexp.last_match(1) || ""
after = Regexp.last_match ? Regexp.last_match.post_match : ""
if k.empty?
return Array(v) if !v.empty? && name == "[]"
return
end
case after
when ""
params[k] = v
when "["
params[name] = v
when "[]"
params[k] ||= []
raise Error, "expected Array (got #{params[k].class}) for param '#{k}'" unless params[k].is_a?(Array)
params[k] << v
when /^\[\]\[([^\[\]]+)\]$/, /^\[\](.+)$/
child_key = Regexp.last_match(1)
params[k] ||= []
raise Error, "expected Array (got #{params[k].class}) for param '#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params_hash_has_key?(params[k].last, child_key)
normalize_query(params[k].last, child_key, v, depth - 1)
else
params[k] << normalize_query({}, child_key, v, depth - 1)
end
else
params[k] ||= {}
raise Error, "expected Hash (got #{params[k].class}) for param '#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_query(params[k], after, v, depth - 1)
end
params
end
def params_hash_has_key?(hash, key)
return false if /\[\]/.match?(key)
key.split(/[\[\]]+/).inject(hash) do |h, part|
next h if part == ""
return false unless h.is_a?(Hash) && h.key?(part)
h[part]
end
true
end
end
end

View File

@ -7,6 +7,8 @@ module HTTPX::Transcoder
module Form
module_function
PARAM_DEPTH_LIMIT = 32
class Encoder
extend Forwardable
@ -31,9 +33,23 @@ module HTTPX::Transcoder
end
end
module Decoder
module_function
def call(response, _)
URI.decode_www_form(response.to_s).each_with_object({}) do |(field, value), params|
HTTPX::Transcoder.normalize_query(params, field, value, PARAM_DEPTH_LIMIT)
end
end
end
def encode(form)
Encoder.new(form)
end
def decode(_response)
Decoder
end
end
register "form", Form
end

View File

@ -28,7 +28,7 @@ module HTTPX::Transcoder
Encoder.new(json)
end
def decoder
def decode(_response)
::JSON.method(:parse)
end
end

View File

@ -29,6 +29,8 @@ module HTTPX
def json: (?json_options opts) -> untyped
def form: () -> Hash[String, untyped]
private
def initialize: (Request request, String | Integer status, String version, headers?) -> untyped

View File

@ -6,10 +6,12 @@ module HTTPX
| () -> Hash[String, _Encode]
def self?.register: (String tag, _Encode handler) -> void
def self?.normalize_keys: (_ToS key, _ToAry[untyped] | _ToHash[_ToS, untyped] | untyped value) { (String, ?untyped) -> void } -> void
| (_ToS key, untyped value, Proc? cond) { (String, untyped) -> void } -> void
def self?.normalize_query: (Hash[String, untyped] params, String name, String v, Integer depth)
interface _Encode
def encode: (untyped payload) -> (_Encoder | _Each[String])
end

View File

@ -136,8 +136,25 @@ class ResponseTest < Minitest::Test
assert form_response.form == { "a" => "b", "c" => "d" }
form2_response = Response.new(request, 200, "2.0", { "content-type" => "application/x-www-form-urlencoded" })
form2_response << "a[]=b&a[]=c&d[e]=f"
assert form2_response.form == { "a" => %w[b c], "d" => { "e" => "f" } }
form2_response << "a[]=b&a[]=c&d[e]=f&g[h][i][j]=k&l[m][][n]=o&l[m][][p]=q&l[m][][n]=r&s[=t"
assert form2_response.form == {
"a" => %w[b c],
"d" => { "e" => "f" },
"g" => { "h" => { "i" => { "j" => "k" } } },
"l" => { "m" => [{ "n" => "o", "p" => "q" }, { "n" => "r" }] },
"s[" => "t",
}
form3_response = Response.new(request, 200, "2.0", { "content-type" => "application/x-www-form-urlencoded" })
form3_response << "a[][]=3"
assert form3_response.form == { "a" => [["3"]] }
form4_response = Response.new(request, 200, "2.0", { "content-type" => "application/x-www-form-urlencoded" })
form4_response << "[]"
assert form4_response.form == {}
error = assert_raises(HTTPX::Error) { form2_response.__send__(:decode, "bla") }
assert error.message =~ /no decoder available for/, "failed with unexpected error"
end
private