defmodule ReqSandbox do
@external_resource "README.md"
@moduledoc @external_resource
|> File.read!()
|> String.split("<!-- MDOC -->")
|> Enum.fetch!(1)
alias Req.{Request, Response}
@default_sandbox_url "/sandbox"
@default_sandbox_header :user_agent
@process_dict_key :req_sandbox
@doc """
Attaches to a Req request.
## Options
* `:sandbox_url` - The path to the sandbox. The default value is `"/sandbox"`.
* `:sandbox_header` - The header to put on the request. The default value is `:user_agent`.
* `:sandbox_header_token` - If provided, the sandbox header value. Otherwise, an existing
token from the process will be used. If no token is found, one will be requested from the
path at `:sandbox_url`. The default value is `nil`.
"""
def attach(%Request{} = req, options \\ []) do
req
|> Request.register_options([:sandbox_url, :sandbox_header, :sandbox_header_token])
|> Request.merge_options(options)
|> Request.append_request_steps(run_sandbox: &__MODULE__.run_sandbox/1)
end
@doc """
Deletes the sandbox token from the process dictionary if it exists.
"""
@spec delete!() :: String.t() | nil
def delete! do
Process.delete(@process_dict_key)
end
@doc """
Deletes the sandbox from the server and the process dictionary, if it exists.
"""
@spec delete!(Request.t()) :: String.t() | nil
def delete!(%Request{} = req) do
url = Map.get(req.options, :sandbox_url, @default_sandbox_url)
%Response{status: 200} = Req.delete!(req, url: url)
delete!()
end
@doc """
Returns the current sandbox token or `nil` if no sandbox exists.
## Examples
ReqSandbox.token()
#=> "BeamMetadata (g2gCZAACdjF0AAAAA2QABW93bmVyWGQAInZ2ZXMzM2o1LWxpdmVib29...)"
"""
@spec token :: nil | String.t()
def token do
callers = [self() | Process.get(:"$callers") || []]
Enum.find_value(callers, fn caller ->
case Process.info(caller, :dictionary) do
{:dictionary, dict} -> dict[@process_dict_key]
nil -> nil
end
end)
end
@doc false
def run_sandbox(%Request{} = req) do
sandbox =
if token = Map.get_lazy(req.options, :sandbox_header_token, &token/0) do
token
else
create_sandbox!(req)
end
header = Map.get(req.options, :sandbox_header, @default_sandbox_header)
put_sandbox_header(req, header, sandbox)
end
defp create_sandbox!(req) do
# todo: handle bad responses
%Response{status: 200, body: sandbox} =
req
|> put_sandbox_url()
|> remove_content()
|> Req.Request.put_header("content-length", "0")
|> Req.post!(sandbox_header_token: :ignore)
Process.put(@process_dict_key, sandbox)
sandbox
end
defp remove_content(req) do
req = %{req | body: nil}
update_in(req.options, &Map.drop(&1, [:form, :json]))
end
defp put_sandbox_url(req) do
sandbox_url = req.options |> Map.get(:sandbox_url, @default_sandbox_url) |> URI.parse()
# Apply the base URL (if present) to ensure we can merge from an absolute URL
req |> Req.Steps.put_base_url() |> put_sandbox_url(sandbox_url)
end
defp put_sandbox_url(req, sandbox_url) do
update_in(req.url, fn url ->
if url.scheme do
url |> URI.merge(sandbox_url)
else
sandbox_url
end
end)
end
defp put_sandbox_header(req, _header, :ignore) do
req
end
defp put_sandbox_header(req, header, sandbox) do
header =
case header do
atom when is_atom(header) ->
atom |> Atom.to_string() |> String.replace("_", "-")
binary when is_binary(binary) ->
binary
end
Request.put_header(req, header, sandbox)
end
end