From 16eff596f1cdf1966c8dd40d7c2417f3979eea7b Mon Sep 17 00:00:00 2001 From: jordan0day Date: Fri, 9 Jan 2015 16:22:06 -0600 Subject: [PATCH 1/3] add support for multipart/form-data POST,PUT,PATCH similar to httpbin's --- lib/httparrot/p_handler.ex | 69 +++++++++++++++++++++- test/p_handler_test.exs | 116 +++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) diff --git a/lib/httparrot/p_handler.ex b/lib/httparrot/p_handler.ex index 32de62c..d4221f2 100644 --- a/lib/httparrot/p_handler.ex +++ b/lib/httparrot/p_handler.ex @@ -21,7 +21,8 @@ defmodule HTTParrot.PHandler do {[{{"application", "json", :*}, :post_binary}, {{"application", "octet-stream", :*}, :post_binary}, {{"text", "plain", :*}, :post_binary}, - {{"application", "x-www-form-urlencoded", :*}, :post_form}], req, state} + {{"application", "x-www-form-urlencoded", :*}, :post_form}, + {{"multipart", "form-data", :*}, :post_multipart}], req, state} end def content_types_provided(req, state) do @@ -57,4 +58,70 @@ defmodule HTTParrot.PHandler do defp response(info, body) do info ++ body |> JSEX.encode! end + + def post_multipart(req, _state) do + {:ok, parts, req} = handle_multipart(req) + + filter = fn({type, _name, _body}, type_atom) -> type == type_atom end + reducer = fn({_type, name, body}, acc) -> acc ++ [{name, body}] end + + # the other post handlers return a list with a single empty tuple if + # there's no data for forms, so let's match that behavior... + normalize = fn(parts) -> if parts == [], do: [{}], else: parts end + + file_parts = Enum.filter(parts, &(filter.(&1, :file))) + |> Enum.reduce([], &(reducer.(&1, &2))) + |> normalize.() + + form_parts = Enum.filter(parts, &(filter.(&1, :form))) + |> Enum.reduce([], &(reducer.(&1, &2))) + |> normalize.() + + post(req, [form: form_parts, files: file_parts, data: "", json: nil]) + end + + defp handle_multipart(req, parts \\ []) do + case :cowboy_req.part(req) do + {:done, req} -> {:ok, parts, req} + {:ok, headers, req} -> + content_disposition = List.keyfind(headers, "content-disposition", 0) + if content_disposition != nil do + case parse_content_disposition_header(content_disposition) do + %{:type => "form-data", "name" => name, "filename" => _filename} -> + {:ok, file, req} = handle_multipart_body(req) + handle_multipart(req, parts ++ [{:file, name, file}]) + %{:type => "form-data", "name" => name} -> + {:ok, form_part, req} = handle_multipart_body(req) + handle_multipart(req, parts ++ [{:form, name, form_part}]) + _ -> + {:ok, parts, req} + end + else + {:ok, parts, req} + end + end + end + + defp handle_multipart_body(req, parts \\ []) do + case :cowboy_req.part_body(req) do + {:ok, data, req} -> + {:ok, Enum.join(parts ++ [data]), req} + {:more, data, req} -> + handle_multipart_body(req, parts ++ [data]) + end + end + + defp parse_content_disposition_header(header) do + parts = elem(header, 1) |> String.split(";") + + type = Enum.at(parts, 0) + parts = Enum.drop(parts, 1) + + Enum.reduce(parts, %{:type => type}, fn part, acc -> + [key, value] = String.split(part, "=") + key = String.strip(key) + value = String.strip(value) |> String.replace("\"", "") + Map.put(acc, key, value) + end) + end end diff --git a/test/p_handler_test.exs b/test/p_handler_test.exs index 279a875..e9e9653 100644 --- a/test/p_handler_test.exs +++ b/test/p_handler_test.exs @@ -70,4 +70,120 @@ defmodule HTTParrot.PHandlerTest do assert validate HTTParrot.GeneralRequestInfo end + + test "returns json with general info and P[OST, ATCH, UT] octet-stream body data for multipart request (simple)" do + expect(:cowboy_req, :part, fn req -> + case req do + :req1 -> + {:ok, [{"content-disposition", "form-data; name=\"key1\""}], :req2} + :req3 -> + {:done, :req4} + end + end) + + expect(:cowboy_req, :part_body, [{[:req2], {:ok, "value1", :req3}}]) + expect(:cowboy_req, :parse_header, + [{["content-type", :req4], + {:ok, {"multipart", "form-data", [{"boundary", "----WebKitFormBoundary8BEQxJvZANFsvRV9"}]}, :req5}}]) + expect(HTTParrot.GeneralRequestInfo, :retrieve, 1, {[:info], :req6}) + + expect(JSEX, :is_json?, 1, false) + expect(JSEX, :encode!, [{[[:info, {:form, [{"key1", "value1"}]}, {:files, [{}]}, {:data, ""}, {:json, nil}]], :response}]) + + expect(:cowboy_req, :set_resp_body, [{[:response, :req6], :req7}]) + + assert post_multipart(:req1, :state) == {true, :req7, nil} + assert validate HTTParrot.GeneralRequestInfo + end + + test "returns json with general info and P[OST, ATCH, UT] octet-stream body data for multipart requests (multiple parts)" do + expect(:cowboy_req, :part, fn req -> + case req do + :req1 -> + {:ok, [{"content-disposition", "form-data; name=\"key1\""}], :req2} + :req3 -> + {:ok, [{"content-disposition", "form-data; name=\"key2\""}], :req4} + :req5 -> + {:done, :req6} + end + end) + + expect(:cowboy_req, :part_body, fn req -> + case req do + :req2 -> {:ok, "value1", :req3} + :req4 -> {:ok, "value2", :req5} + end + end) + + expect(:cowboy_req, :parse_header, + [{["content-type", :req6], + {:ok, {"multipart", "form-data", [{"boundary", "----WebKitFormBoundary8BEQxJvZANFsvRV9"}]}, :req7}}]) + expect(HTTParrot.GeneralRequestInfo, :retrieve, 1, {[:info], :req8}) + + expect(JSEX, :is_json?, 1, false) + expect(JSEX, :encode!, [{[[:info, {:form, [{"key1", "value1"}, {"key2", "value2"}]}, {:files, [{}]}, {:data, ""}, {:json, nil}]], :response}]) + + expect(:cowboy_req, :set_resp_body, [{[:response, :req8], :req9}]) + + assert post_multipart(:req1, :state) == {true, :req9, nil} + assert validate HTTParrot.GeneralRequestInfo + end + + test "returns json with general info and P[OST, UT, ATCH] file data (one file)" do + expect(:cowboy_req, :part, fn req -> + case req do + :req1 -> + {:ok, [{"content-disposition", "form-data; name=\"file1\"; filename=\"testdata.txt\""}, {"content-type", "text/plain"}], :req2} + :req3 -> + {:done, :req4} + end + end) + + expect(:cowboy_req, :part_body, [{[:req2], {:ok, "here is some cool\ntest data.", :req3}}]) + expect(:cowboy_req, :parse_header, + [{["content-type", :req4], + {:ok, {"multipart", "form-data", [{"boundary", "----WebKitFormBoundary8BEQxJvZANFsvRV9"}]}, :req5}}]) + expect(HTTParrot.GeneralRequestInfo, :retrieve, 1, {[:info], :req6}) + + expect(JSEX, :is_json?, 1, false) + expect(JSEX, :encode!, [{[[:info, {:form, [{}]}, {:files, [{"file1", "here is some cool\ntest data."}]}, {:data, ""}, {:json, nil}]], :response}]) + + expect(:cowboy_req, :set_resp_body, [{[:response, :req6], :req7}]) + + assert post_multipart(:req1, :state) == {true, :req7, nil} + assert validate HTTParrot.GeneralRequestInfo + end + + test "returns json with general info and P[OST, UT, ATCH] file data (form-data plus one file)" do + expect(:cowboy_req, :part, fn req -> + case req do + :req1 -> + {:ok, [{"content-disposition", "form-data; name=\"key1\""}], :req2} + :req3 -> + {:ok, [{"content-disposition", "form-data; name=\"file1\"; filename=\"testdata.txt\""}, {"content-type", "text/plain"}], :req4} + :req5 -> + {:done, :req6} + end + end) + + expect(:cowboy_req, :part_body, fn req -> + case req do + :req2 -> {:ok, "value1", :req3} + :req4 -> {:ok, "here is some cool\ntest data", :req5} + end + end) + + expect(:cowboy_req, :parse_header, + [{["content-type", :req6], + {:ok, {"multipart", "form-data", [{"boundary", "----WebKitFormBoundary8BEQxJvZANFsvRV9"}]}, :req7}}]) + expect(HTTParrot.GeneralRequestInfo, :retrieve, 1, {[:info], :req8}) + + expect(JSEX, :is_json?, 1, false) + expect(JSEX, :encode!, [{[[:info, {:form, [{"key1", "value1"}]}, {:files, [{"file1", "here is some cool\ntest data"}]}, {:data, ""}, {:json, nil}]], :response}]) + + expect(:cowboy_req, :set_resp_body, [{[:response, :req8], :req9}]) + + assert post_multipart(:req1, :state) == {true, :req9, nil} + assert validate HTTParrot.GeneralRequestInfo + end end From d5d41bff49dc9d10c064e2dcb40cf6a164f69f47 Mon Sep 17 00:00:00 2001 From: Jordan Day Date: Sun, 11 Jan 2015 15:36:47 -0600 Subject: [PATCH 2/3] use list comprehensions where possible, move list normalizer into named fn --- lib/httparrot/p_handler.ex | 43 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/lib/httparrot/p_handler.ex b/lib/httparrot/p_handler.ex index d4221f2..4cf1e0e 100644 --- a/lib/httparrot/p_handler.ex +++ b/lib/httparrot/p_handler.ex @@ -62,22 +62,10 @@ defmodule HTTParrot.PHandler do def post_multipart(req, _state) do {:ok, parts, req} = handle_multipart(req) - filter = fn({type, _name, _body}, type_atom) -> type == type_atom end - reducer = fn({_type, name, body}, acc) -> acc ++ [{name, body}] end + file_parts = for file <- parts, elem(file, 0) == :file, do: {elem(file, 1), elem(file, 2)} + form_parts = for form <- parts, elem(form, 0) == :form, do: {elem(form, 1), elem(form, 2)} - # the other post handlers return a list with a single empty tuple if - # there's no data for forms, so let's match that behavior... - normalize = fn(parts) -> if parts == [], do: [{}], else: parts end - - file_parts = Enum.filter(parts, &(filter.(&1, :file))) - |> Enum.reduce([], &(reducer.(&1, &2))) - |> normalize.() - - form_parts = Enum.filter(parts, &(filter.(&1, :form))) - |> Enum.reduce([], &(reducer.(&1, &2))) - |> normalize.() - - post(req, [form: form_parts, files: file_parts, data: "", json: nil]) + post(req, [form: normalize_list(form_parts), files: normalize_list(file_parts), data: "", json: nil]) end defp handle_multipart(req, parts \\ []) do @@ -85,7 +73,7 @@ defmodule HTTParrot.PHandler do {:done, req} -> {:ok, parts, req} {:ok, headers, req} -> content_disposition = List.keyfind(headers, "content-disposition", 0) - if content_disposition != nil do + if content_disposition do case parse_content_disposition_header(content_disposition) do %{:type => "form-data", "name" => name, "filename" => _filename} -> {:ok, file, req} = handle_multipart_body(req) @@ -112,16 +100,19 @@ defmodule HTTParrot.PHandler do end defp parse_content_disposition_header(header) do - parts = elem(header, 1) |> String.split(";") + parts = elem(header, 1) + |> String.split(";") + |> Enum.map(&String.strip/1) - type = Enum.at(parts, 0) - parts = Enum.drop(parts, 1) - - Enum.reduce(parts, %{:type => type}, fn part, acc -> - [key, value] = String.split(part, "=") - key = String.strip(key) - value = String.strip(value) |> String.replace("\"", "") - Map.put(acc, key, value) - end) + for part <- parts, into: %{} do + case String.split(part, "=") |> Enum.map(&String.strip/1) do + [type] -> {:type, type} + [key, value] -> {key, String.replace(value, "\"", "")} + end + end end + + defp normalize_list(list) when list == [], do: [{}] + + defp normalize_list(list), do: list end From dbc1fc42c48727708a7582fb5ae37986b5cddf53 Mon Sep 17 00:00:00 2001 From: Jordan Day Date: Sun, 11 Jan 2015 16:07:06 -0600 Subject: [PATCH 3/3] small tweak for normalize_list fn --- lib/httparrot/p_handler.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/httparrot/p_handler.ex b/lib/httparrot/p_handler.ex index 4cf1e0e..d795d49 100644 --- a/lib/httparrot/p_handler.ex +++ b/lib/httparrot/p_handler.ex @@ -112,7 +112,7 @@ defmodule HTTParrot.PHandler do end end - defp normalize_list(list) when list == [], do: [{}] + defp normalize_list([]), do: [{}] defp normalize_list(list), do: list end