Repository: wojtekmach/req (elixir)

Issue:
New step: `output`
```
% curl -o foo.html https://elixir-lang.org/index.html
% curl --remote-name https://elixir-lang.org/index.html
```

Let's add:

```elixir
iex> Req.get!(url, output: path)
iex> Req.get!(url, output: :remote_name)
```

Pull request:
Add `:output` step
Closes #21.

Patch:
diff --git a/lib/req.ex b/lib/req.ex
index 0d87571..04bf76a 100644
--- a/lib/req.ex
+++ b/lib/req.ex
@@ -84,6 +84,7 @@ defmodule Req do
         &Req.Steps.retry/1,
         &Req.Steps.follow_redirects/1,
         &Req.Steps.decompress/1,
+        &Req.Steps.output/1,
         &Req.Steps.decode_body/1
       ],
       error_steps: [
@@ -275,9 +276,14 @@ defmodule Req do
   Response body options:
 
     * `:raw` - if set to `true`, disables automatic body decompression
-      ([`decompress`](`Req.Steps.decompress/1`) step) and and decoding
+      ([`decompress`](`Req.Steps.decompress/1`) step) and decoding
       ([`decode_body`](`Req.Steps.decode_body/1`) step). Defaults to `false`.
 
+    * `:output` - if set, writes the response body to a file (via
+      [`output`](`Req.Steps.output/1`)). Can be set to a string path or an atom `:remote_name` which
+      would use the remote name as the filename in the current working directory. Once the file is
+      written, the response body is replaced with `""`.
+
   Response redirect options ([`follow_redirects`](`Req.Steps.follow_redirects/1`) step):
 
     * `:follow_redirects` - if set to `false`, disables automatic response redirects. Defaults to `true`.
diff --git a/lib/req/steps.ex b/lib/req/steps.ex
index a6d5f5f..f292a30 100644
--- a/lib/req/steps.ex
+++ b/lib/req/steps.ex
@@ -610,6 +610,52 @@ defmodule Req.Steps do
     end
   end
 
+  @doc """
+  Writes the response body to a file.
+
+  After the output file is written, the response body is set to `""`.
+
+  ## Request Options
+
+    * `:output` - if set, writes the response body to a file. Can be one of:
+
+      * `path` - writes to the given path
+
+      * `:remote_name` - uses the remote name as the filename in the current working directory
+
+  ## Examples
+
+      iex> Req.get!("https://elixir-lang.org/index.html", output: "/tmp/elixir_home.html")
+      iex> File.exists?("/tmp/elixir_home.html")
+      true
+
+      iex> Req.get!("https://elixir-lang.org/blog/index.html", output: :remote_name)
+      iex> File.exists?("index.html")
+      true
+  """
+  @doc step: :response
+  def output({request, response}) do
+    output({request, response}, Map.get(request.options, :output))
+  end
+
+  def output(request_response, nil) do
+    request_response
+  end
+
+  def output({request, response}, :remote_name) do
+    path = Path.basename(request.url.path || "")
+    output({request, response}, path)
+  end
+
+  def output(_request_response, "") do
+    raise "cannot write to file \"\""
+  end
+
+  def output({request, response}, path) do
+    File.write!(path, response.body)
+    {request, %{response | body: ""}}
+  end
+
   @doc """
   Decodes response body based on the detected format.
 


Test patch:
diff --git a/test/req/steps_test.exs b/test/req/steps_test.exs
index 5aea723..4cf01fe 100644
--- a/test/req/steps_test.exs
+++ b/test/req/steps_test.exs
@@ -283,6 +283,41 @@ defmodule Req.StepsTest do
     assert Req.get!(c.url).body == "foo"
   end
 
+  @tag :tmp_dir
+  test "output/1: path (compressed)", c do
+    Bypass.expect_once(c.bypass, "GET", "/save_to_path", fn conn ->
+      conn
+      |> Plug.Conn.put_resp_header("content-encoding", "x-gzip")
+      |> Plug.Conn.send_resp(200, :zlib.gzip("to be filed"))
+    end)
+
+    response = Req.get!(c.url <> "/save_to_path", output: c.tmp_dir <> "/saved.txt")
+    assert response.body == ""
+    assert File.read!(c.tmp_dir <> "/saved.txt") == "to be filed"
+  end
+
+  test "output/1: :remote_name (not compressed)", c do
+    Bypass.expect_once(c.bypass, "GET", "/directory/save_this.txt", fn conn ->
+      Plug.Conn.send_resp(conn, 200, "to be filed")
+    end)
+
+    response = Req.get!(c.url <> "/directory/save_this.txt", output: :remote_name)
+    assert response.body == ""
+    assert File.read!("save_this.txt") == "to be filed"
+  after
+    File.rm("save_this.txt")
+  end
+
+  test "output/1: empty filename", c do
+    Bypass.expect_once(c.bypass, "GET", "", fn conn ->
+      Plug.Conn.send_resp(conn, 200, "body contents")
+    end)
+
+    assert_raise RuntimeError, "cannot write to file \"\"", fn ->
+      Req.get!(c.url, output: :remote_name)
+    end
+  end
+
   test "decode_body/1: json", c do
     Bypass.expect(c.bypass, "GET", "/", fn conn ->
       conn