Skip to content

Commit

Permalink
Add escape hatch
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach committed Aug 24, 2023
1 parent 6ce83c9 commit 675f138
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 130 deletions.
50 changes: 30 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

## HEAD

* Change `request.headers` and `response.headers` to be maps.

This is a major breaking change. If you cannot easily update your app
or your dependencies, do:

# config/config.exs
config :req, legacy_headers_as_lists: true

This legacy fallback will be removed on Req 1.0.

* Make `request.registered_options` internal representation private.

* Make `request.options` internal representation private.

Currently `request.options` field is a map but it may change in the future.
One possible future change is using keywords lists internally which would
allow, for example, `Req.new(params: [a: 1]) |> Req.update(params: [b: 2])`
to keep duplicate `:params` in `request.options` which would then allow to
decide the duplicate key semantics on a per-step basis. And so, for example,
[`put_params`] would _merge_ params but most steps would simply use the
first value.

To have some room for manoeuvre in the future we should stop pattern
matching on `request.options`. Calling `request.options[key]`,
`put_in(request.options[key], value)`, and
`update_in(request.options[key], fun)` _is_ allowed.
New functions [`Req.Request.get_option/3`],
[`Req.Request.fetch_option!/2`], and [`Req.Request.delete_option/1`] have been
added for additional ways to manipulate the internal representation.

* Fix typespecs for some functions

* [`decompress_body`]: Remove support for `deflate` compression
Expand Down Expand Up @@ -33,26 +63,6 @@

* [`Req.Request`]: Fix displaying redacted basic authentication

* Make `request.registered_options` internal representation private.

* Make `request.options` internal representation private.

Currently `request.options` field is a map but it may change in the future.
One possible future change is using keywords lists internally which would
allow, for example, `Req.new(params: [a: 1]) |> Req.update(params: [b: 2])`
to keep duplicate `:params` in `request.options` which would then allow to
decide the duplicate key semantics on a per-step basis. And so, for example,
[`put_params`] would _merge_ params but most steps would simply use the
first value.

To have some room for manoeuvre in the future we should stop pattern
matching on `request.options`. Calling `request.options[key]`,
`put_in(request.options[key], value)`, and
`update_in(request.options[key], fun)` _is_ allowed.
New functions [`Req.Request.get_option/3`],
[`Req.Request.fetch_option!/2`], and [`Req.Request.delete_option/1`] have been
added for additional ways to manipulate the internal representation.

## v0.3.11 (2023-07-24)

* Support `Req.get(options)`, `Req.post(options)`, etc
Expand Down
46 changes: 30 additions & 16 deletions lib/req.ex
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,13 @@ defmodule Req do

{:headers, new_headers}, acc ->
update_in(acc.headers, fn old_headers ->
Map.merge(old_headers, encode_headers(new_headers))
if unquote(Req.MixProject.legacy_headers_as_lists?()) do
new_headers = encode_headers(new_headers)
new_header_names = Enum.map(new_headers, &elem(&1, 0))
Enum.reject(old_headers, &(elem(&1, 0) in new_header_names)) ++ new_headers
else
Map.merge(old_headers, encode_headers(new_headers))
end
end)

{name, value}, acc ->
Expand Down Expand Up @@ -941,23 +947,31 @@ defmodule Req do
Application.put_env(:req, :default_options, options)
end

defp encode_headers(headers) do
Enum.reduce(headers, %{}, fn {name, value}, acc ->
Map.update(
acc,
encode_header_name(name),
encode_header_values(List.wrap(value)),
&(&1 ++ encode_header_values(List.wrap(value)))
)
end)
end
if Req.MixProject.legacy_headers_as_lists?() do
defp encode_headers(headers) do
for {name, value} <- headers do
{encode_header_name(name), encode_header_value(value)}
end
end
else
defp encode_headers(headers) do
Enum.reduce(headers, %{}, fn {name, value}, acc ->
Map.update(
acc,
encode_header_name(name),
encode_header_values(List.wrap(value)),
&(&1 ++ encode_header_values(List.wrap(value)))
)
end)
end

defp encode_header_values([value | rest]) do
[encode_header_value(value) | encode_header_values(rest)]
end
defp encode_header_values([value | rest]) do
[encode_header_value(value) | encode_header_values(rest)]
end

defp encode_header_values([]) do
[]
defp encode_header_values([]) do
[]
end
end

defp encode_header_name(name) when is_atom(name) do
Expand Down
140 changes: 98 additions & 42 deletions lib/req/request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ defmodule Req.Request do

defstruct method: :get,
url: URI.parse(""),
headers: %{},
headers: if(Req.MixProject.legacy_headers_as_lists?(), do: [], else: %{}),
body: nil,
options: %{},
halted: false,
Expand Down Expand Up @@ -368,25 +368,37 @@ defmodule Req.Request do
## Examples
iex> req = Req.Request.new(url: "https://api.github.com/repos/wojtekmach/req")
iex> {request, response} = Req.Request.run_request(req)
iex> request.url.host
iex> {req, resp} = Req.Request.run_request(req)
iex> req.url.host
"api.github.com"
iex> response.status
iex> resp.status
200
"""
def new(options) do
options =
options
|> Keyword.validate!([:method, :url, :headers, :body, :adapter, :options])
|> Keyword.update(:url, URI.new!(""), &URI.new!/1)
|> Keyword.update(:headers, %{}, fn headers ->
Map.new(headers, fn {key, value} ->
{key, List.wrap(value)}
if Req.MixProject.legacy_headers_as_lists?() do
def new(options) do
options =
options
|> Keyword.validate!([:method, :url, :headers, :body, :adapter, :options])
|> Keyword.update(:url, URI.new!(""), &URI.new!/1)
|> Keyword.update(:options, %{}, &Map.new/1)

struct!(__MODULE__, options)
end
else
def new(options) do
options =
options
|> Keyword.validate!([:method, :url, :headers, :body, :adapter, :options])
|> Keyword.update(:url, URI.new!(""), &URI.new!/1)
|> Keyword.update(:headers, %{}, fn headers ->
Map.new(headers, fn {key, value} ->
{key, List.wrap(value)}
end)
end)
end)
|> Keyword.update(:options, %{}, &Map.new/1)
|> Keyword.update(:options, %{}, &Map.new/1)

struct!(__MODULE__, options)
struct!(__MODULE__, options)
end
end

@doc """
Expand Down Expand Up @@ -664,8 +676,16 @@ defmodule Req.Request do
"""
@spec get_header(t(), binary()) :: [binary()]
def get_header(%Req.Request{} = request, key) when is_binary(key) do
Map.get(request.headers, key, [])
if Req.MixProject.legacy_headers_as_lists?() do
def get_header(%Req.Request{} = request, key) when is_binary(key) do
for {^key, value} <- request.headers do
value
end
end
else
def get_header(%Req.Request{} = request, key) when is_binary(key) do
Map.get(request.headers, key, [])
end
end

@doc """
Expand All @@ -684,16 +704,24 @@ defmodule Req.Request do
## Examples
iex> req = Req.new()
iex> Req.Request.get_header(req, "accept")
[]
iex> req = Req.Request.put_header(req, "accept", "application/json")
iex> req.headers
%{"accept" => ["application/json"]}
iex> Req.Request.get_header(req, "accept")
["application/json"]
"""
@spec put_header(t(), binary(), binary()) :: t()
def put_header(%Req.Request{} = request, key, value)
when is_binary(key) and
(is_binary(value) or is_list(value)) do
put_in(request.headers[key], List.wrap(value))
if Req.MixProject.legacy_headers_as_lists?() do
def put_header(%Req.Request{} = request, key, value)
when is_binary(key) and is_binary(value) do
%{request | headers: List.keystore(request.headers, key, 0, {key, value})}
end
else
def put_header(%Req.Request{} = request, key, value)
when is_binary(key) and (is_binary(value) or is_list(value)) do
put_in(request.headers[key], List.wrap(value))
end
end

@doc """
Expand All @@ -705,8 +733,10 @@ defmodule Req.Request do
iex> req = Req.new()
iex> req = Req.Request.put_headers(req, [{"accept", "text/html"}, {"accept-encoding", "gzip"}])
iex> req.headers
%{"accept" => ["text/html"], "accept-encoding" => ["gzip"]}
iex> Req.Request.get_header(req, "accept")
["text/html"]
iex> Req.Request.get_header(req, "accept-encoding")
["gzip"]
"""
@spec put_headers(t(), [{binary(), binary()}]) :: t()
def put_headers(%Req.Request{} = request, headers) do
Expand All @@ -726,13 +756,35 @@ defmodule Req.Request do
...> Req.new()
...> |> Req.Request.put_new_header("accept", "application/json")
...> |> Req.Request.put_new_header("accept", "application/html")
iex> req.headers
%{"accept" => ["application/json"]}
iex> Req.Request.get_header(req, "accept")
["application/json"]
"""
@spec put_new_header(t(), binary(), binary()) :: t()
def put_new_header(%Req.Request{} = request, key, value)
when is_binary(key) and (is_binary(value) or is_list(value)) do
update_in(request.headers, &Map.put_new(&1, key, List.wrap(value)))
if Req.MixProject.legacy_headers_as_lists?() do
def put_new_header(%Req.Request{} = request, key, value) do
case get_header(request, key) do
[] ->
put_header(request, key, value)

_ ->
request
end
end
else
def put_new_header(%Req.Request{} = request, key, value)
when is_binary(key) and (is_binary(value) or is_list(value)) do
update_in(request.headers, &Map.put_new(&1, key, List.wrap(value)))
end
end

if Req.MixProject.legacy_headers_as_lists?() do
def delete_header(%Req.Request{} = request, key) when is_binary(key) do
%{request | headers: List.keydelete(request.headers, key, 0)}
end
else
def delete_header(%Req.Request{} = request, key) when is_binary(key) do
update_in(request.headers, &Map.delete(&1, key))
end
end

@doc """
Expand Down Expand Up @@ -914,19 +966,23 @@ defmodule Req.Request do
{headers, options} =
if Req.Request.get_option(request, :redact_auth, true) do
headers =
case request.headers do
headers when is_map(headers) ->
for {name, values} <- request.headers, into: %{} do
if name in ["authorization", "Authorization"] do
[_] = values
{name, ["[redacted]"]}
else
{name, values}
end
if Req.MixProject.legacy_headers_as_lists?() do
for {name, value} <- request.headers do
if name in ["authorization", "Authorization"] do
{name, "[redacted]"}
else
{name, value}
end

# TODO: headers are always a map on Req v1.0
#
end
else
for {name, values} <- request.headers, into: %{} do
if name in ["authorization", "Authorization"] do
[_] = values
{name, ["[redacted]"]}
else
{name, values}
end
end
end

options =
Expand Down
Loading

0 comments on commit 675f138

Please sign in to comment.