Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85712806
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/tesla/multipart.ex b/lib/tesla/multipart.ex
index 9d4daaa..fbc6105 100644
--- a/lib/tesla/multipart.ex
+++ b/lib/tesla/multipart.ex
@@ -1,180 +1,196 @@
defmodule Tesla.Multipart do
@moduledoc """
Multipart functionality.
### Example
```
mp =
Multipart.new
|> Multipart.add_content_type_param("charset=utf-8")
|> Multipart.add_field("field1", "foo")
|> Multipart.add_field("field2", "bar", headers: [{"content-id", "1"}, {"content-type", "text/plain"}])
|> Multipart.add_file("test/tesla/multipart_test_file.sh")
|> Multipart.add_file("test/tesla/multipart_test_file.sh", name: "foobar")
|> Multipart.add_file_content("sample file content", "sample.txt")
response = client.post(url, mp)
```
"""
defmodule Part do
@moduledoc false
defstruct body: nil,
dispositions: [],
headers: []
@type t :: %__MODULE__{
body: String.t(),
headers: Tesla.Env.headers(),
dispositions: Keyword.t()
}
end
@boundary_chars "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|> String.split("")
- @type part_stream :: IO.Stream.t() | File.Stream.t() | Enumerable.t()
- @type part_value :: String.t() | part_stream
+ @type part_stream :: IO.Stream.t() | File.Stream.t()
+ @type part_value :: iodata | part_stream
defstruct parts: [],
boundary: nil,
content_type_params: []
@type t :: %__MODULE__{
parts: list(Tesla.Multipart.Part.t()),
boundary: String.t(),
content_type_params: [String.t()]
}
@doc """
Create a new Multipart struct to be used for a request body.
"""
@spec new() :: t
def new do
%__MODULE__{boundary: unique_string(32)}
end
@doc """
Add a parameter to the multipart content-type.
"""
@spec add_content_type_param(t, String.t()) :: t
def add_content_type_param(%__MODULE__{} = mp, param) do
%{mp | content_type_params: mp.content_type_params ++ [param]}
end
@doc """
Add a field part.
"""
@spec add_field(t, String.t(), part_value, Keyword.t()) :: t
def add_field(%__MODULE__{} = mp, name, value, opts \\ []) do
+ :ok = assert_part_value!(value)
{headers, opts} = Keyword.pop_first(opts, :headers, [])
part = %Part{
body: value,
headers: headers,
dispositions: [{:name, name}] ++ opts
}
%{mp | parts: mp.parts ++ [part]}
end
@doc """
Add a file part. The file will be streamed.
Options:
- `:name` - name of form param
- `:filename` - filename (defaults to path basename)
- `:headers` - additional headers
- `:detect_content_type` - auto-detect file content-type (defaults to false)
"""
@spec add_file(t, String.t(), Keyword.t()) :: t
def add_file(%__MODULE__{} = mp, path, opts \\ []) do
{filename, opts} = Keyword.pop_first(opts, :filename, Path.basename(path))
{headers, opts} = Keyword.pop_first(opts, :headers, [])
{detect_content_type, opts} = Keyword.pop_first(opts, :detect_content_type, false)
# add in detected content-type if necessary
headers =
case detect_content_type do
true -> List.keystore(headers, "content-type", 0, {"content-type", MIME.from_path(path)})
false -> headers
end
data = File.stream!(path, [:read], 2048)
add_file_content(mp, data, filename, opts ++ [headers: headers])
end
@doc """
Add a file part. Same of `add_file/3` but the file content is read from `data` input parameter.
Options:
- `:name` - name of form param
- `:headers` - additional headers
"""
@spec add_file_content(t, part_value, String.t(), Keyword.t()) :: t
def add_file_content(%__MODULE__{} = mp, data, filename, opts \\ []) do
{name, opts} = Keyword.pop_first(opts, :name, "file")
add_field(mp, name, data, opts ++ [filename: filename])
end
@doc false
@spec headers(t) :: Tesla.Env.headers()
def headers(%__MODULE__{boundary: boundary, content_type_params: params}) do
ct_params = (["boundary=#{boundary}"] ++ params) |> Enum.join("; ")
[{"content-type", "multipart/form-data; #{ct_params}"}]
end
@doc false
@spec body(t) :: part_stream
def body(%__MODULE__{boundary: boundary, parts: parts}) do
part_streams = Enum.map(parts, &part_as_stream(&1, boundary))
Stream.concat(part_streams ++ [["--#{boundary}--\r\n"]])
end
@doc false
@spec part_as_stream(Part.t(), String.t()) :: part_stream
def part_as_stream(
%Part{body: body, dispositions: dispositions, headers: part_headers},
boundary
) do
part_headers = Enum.map(part_headers, fn {k, v} -> "#{k}: #{v}\r\n" end)
part_headers = part_headers ++ [part_headers_for_disposition(dispositions)]
enum_body =
case body do
b when is_binary(b) -> [b]
b -> b
end
Stream.concat([
["--#{boundary}\r\n"],
part_headers,
["\r\n"],
enum_body,
["\r\n"]
])
end
@doc false
@spec part_headers_for_disposition(Keyword.t()) :: [String.t()]
def part_headers_for_disposition([]), do: []
def part_headers_for_disposition(kvs) do
ds =
kvs
|> Enum.map(fn {k, v} -> "#{k}=\"#{v}\"" end)
|> Enum.join("; ")
["content-disposition: form-data; #{ds}\r\n"]
end
@doc false
@spec unique_string(pos_integer) :: String.t()
defp unique_string(length) do
Enum.reduce(1..length, [], fn _i, acc ->
[Enum.random(@boundary_chars) | acc]
end)
|> Enum.join("")
end
+
+ @spec assert_part_value!(any) :: :ok | no_return
+ defp assert_part_value!(%maybe_stream{})
+ when maybe_stream == IO.Stream
+ when maybe_stream == File.Stream,
+ do: :ok
+
+ defp assert_part_value!(value)
+ when is_list(value)
+ when is_binary(value),
+ do: :ok
+
+ defp assert_part_value!(val) do
+ raise(ArgumentError, "#{inspect(val)} is not a supported multipart value.")
+ end
end
diff --git a/test/tesla/multipart_test.exs b/test/tesla/multipart_test.exs
index 1503514..9e76d64 100644
--- a/test/tesla/multipart_test.exs
+++ b/test/tesla/multipart_test.exs
@@ -1,223 +1,276 @@
defmodule Tesla.MultipartTest do
use ExUnit.Case
alias Tesla.Multipart
test "headers" do
mp = Multipart.new()
headers = Multipart.headers(mp)
assert headers == [{"content-type", "multipart/form-data; boundary=#{mp.boundary}"}]
end
test "add content-type param" do
mp =
Multipart.new()
|> Multipart.add_content_type_param("charset=utf-8")
headers = Multipart.headers(mp)
assert headers == [
{"content-type", "multipart/form-data; boundary=#{mp.boundary}; charset=utf-8"}
]
end
test "add content-type params" do
mp =
Multipart.new()
|> Multipart.add_content_type_param("charset=utf-8")
|> Multipart.add_content_type_param("foo=bar")
headers = Multipart.headers(mp)
assert headers == [
{"content-type",
"multipart/form-data; boundary=#{mp.boundary}; charset=utf-8; foo=bar"}
]
end
test "add_field" do
mp =
Multipart.new()
|> Multipart.add_field("foo", "bar")
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-disposition: form-data; name="foo"\r
\r
bar\r
--#{mp.boundary}--\r
"""
end
test "add_field with extra headers" do
mp =
Multipart.new()
|> Multipart.add_field(
"foo",
"bar",
headers: [{"content-id", "1"}, {"content-type", "text/plain"}]
)
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-id: 1\r
content-type: text/plain\r
content-disposition: form-data; name="foo"\r
\r
bar\r
--#{mp.boundary}--\r
"""
end
test "add_file (filename only)" do
mp =
Multipart.new()
|> Multipart.add_file("test/tesla/multipart_test_file.sh")
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-disposition: form-data; name="file"; filename="multipart_test_file.sh"\r
\r
#!/usr/bin/env bash
echo "test multipart file"
\r
--#{mp.boundary}--\r
"""
end
test "add_file (filename with name)" do
mp =
Multipart.new()
|> Multipart.add_file("test/tesla/multipart_test_file.sh", name: "foobar")
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-disposition: form-data; name="foobar"; filename="multipart_test_file.sh"\r
\r
#!/usr/bin/env bash
echo "test multipart file"
\r
--#{mp.boundary}--\r
"""
end
test "add_file (custom filename)" do
mp =
Multipart.new()
|> Multipart.add_file("test/tesla/multipart_test_file.sh", filename: "custom.png")
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-disposition: form-data; name="file"; filename="custom.png"\r
\r
#!/usr/bin/env bash
echo "test multipart file"
\r
--#{mp.boundary}--\r
"""
end
test "add_file (filename with name, extra headers)" do
mp =
Multipart.new()
|> Multipart.add_file(
"test/tesla/multipart_test_file.sh",
name: "foobar",
headers: [{"content-id", "1"}, {"content-type", "text/plain"}]
)
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-id: 1\r
content-type: text/plain\r
content-disposition: form-data; name="foobar"; filename="multipart_test_file.sh"\r
\r
#!/usr/bin/env bash
echo "test multipart file"
\r
--#{mp.boundary}--\r
"""
end
test "add_file (detect content type)" do
mp =
Multipart.new()
|> Multipart.add_file("test/tesla/multipart_test_file.sh", detect_content_type: true)
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-type: application/x-sh\r
content-disposition: form-data; name="file"; filename="multipart_test_file.sh"\r
\r
#!/usr/bin/env bash
echo "test multipart file"
\r
--#{mp.boundary}--\r
"""
end
test "add_file (detect content type overrides given header)" do
mp =
Multipart.new()
|> Multipart.add_file(
"test/tesla/multipart_test_file.sh",
detect_content_type: true,
headers: [{"content-type", "foo/bar"}]
)
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-type: application/x-sh\r
content-disposition: form-data; name="file"; filename="multipart_test_file.sh"\r
\r
#!/usr/bin/env bash
echo "test multipart file"
\r
--#{mp.boundary}--\r
"""
end
test "add_file_content" do
mp =
Multipart.new()
|> Multipart.add_file_content("file-data", "data.gif")
body = Multipart.body(mp) |> Enum.join()
assert body == """
--#{mp.boundary}\r
content-disposition: form-data; name="file"; filename="data.gif"\r
\r
file-data\r
--#{mp.boundary}--\r
"""
end
test "add non-existing file" do
mp =
Multipart.new()
|> Multipart.add_file("i-do-not-exists.txt")
assert_raise File.Error, fn ->
mp |> Multipart.body() |> Enum.to_list()
end
end
+
+ describe "add_field" do
+ test "numbers raise argument error" do
+ assert_raise ArgumentError, fn ->
+ Multipart.new()
+ |> Multipart.add_field("foo", 123)
+ end
+
+ assert_raise ArgumentError, fn ->
+ Multipart.new()
+ |> Multipart.add_field("bar", 123.00)
+ end
+ end
+
+ test "maps raise argument error" do
+ assert_raise ArgumentError, fn ->
+ Multipart.new()
+ |> Multipart.add_field("foo", %{hello: :world})
+ end
+ end
+
+ test "Iodata" do
+ mp =
+ Multipart.new()
+ |> Multipart.add_field("foo", ["bar", "baz"])
+
+ body = Multipart.body(mp) |> Enum.join()
+
+ assert body == """
+ --#{mp.boundary}\r
+ content-disposition: form-data; name="foo"\r
+ \r
+ barbaz\r
+ --#{mp.boundary}--\r
+ """
+ end
+
+ test "IO.Stream" do
+ mp =
+ Multipart.new()
+ |> Multipart.add_field("foo", %IO.Stream{})
+
+ assert is_function(Multipart.body(mp))
+ end
+
+ test "File.Stream" do
+ mp =
+ Multipart.new()
+ |> Multipart.add_field("foo", %File.Stream{})
+
+ assert is_function(Multipart.body(mp))
+ end
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 12:30 PM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769477
Default Alt Text
(13 KB)
Attached To
Mode
R28 tesla
Attached
Detach File
Event Timeline
Log In to Comment