lib/req.ex

defmodule Req do
  @moduledoc ~S"""
  The high-level API.

  Req is composed of:

    * `Req` - the high-level API (you're here!)

    * `Req.Request` - the low-level API and the request struct

    * `Req.Steps` - the collection of built-in steps

    * `Req.Test` - the testing conveniences

  The high-level API is what most users of Req will use most of the time.

  ## Examples

  Making a GET request with `Req.get!/1`:

      iex> Req.get!("https://api.github.com/repos/wojtekmach/req").body["description"]
      "Req is a batteries-included HTTP client for Elixir."

  Same, but by explicitly building request struct first:

      iex> req = Req.new(base_url: "https://api.github.com")
      iex> Req.get!(req, url: "/repos/wojtekmach/req").body["description"]
      "Req is a batteries-included HTTP client for Elixir."

  Return the request that was sent using `Req.run!/2`:

      iex> {req, resp} = Req.run!("https://httpbin.org/basic-auth/foo/bar", auth: {:basic, "foo:bar"})
      iex> req.headers["authorization"]
      ["Basic Zm9vOmJhcg=="]
      iex> resp.status
      200

  Making a POST request with `Req.post!/2`:

      iex> Req.post!("https://httpbin.org/post", form: [comments: "hello!"]).body["form"]
      %{"comments" => "hello!"}

  Set connection timeout:

      iex> resp = Req.get!("https://httpbin.org", connect_options: [timeout: 100])
      iex> resp.status
      200

  See [`run_finch`](`Req.Steps.run_finch/1`) for more connection related options and usage examples.

  Stream request body:

      iex> stream = Stream.duplicate("foo", 3)
      iex> Req.post!("https://httpbin.org/post", body: stream).body["data"]
      "foofoofoo"

  Stream response body using a callback:

      iex> resp =
      ...>   Req.get!("http://httpbin.org/stream/2", into: fn {:data, data}, {req, resp} ->
      ...>     IO.puts(data)
      ...>     {:cont, {req, resp}}
      ...>   end)
      # output: {"url": "http://httpbin.org/stream/2", ...}
      # output: {"url": "http://httpbin.org/stream/2", ...}
      iex> resp.status
      200
      iex> resp.body
      ""

  Stream response body into a `Collectable`:

      iex> resp = Req.get!("http://httpbin.org/stream/2", into: IO.stream())
      # output: {"url": "http://httpbin.org/stream/2", ...}
      # output: {"url": "http://httpbin.org/stream/2", ...}
      iex> resp.status
      200
      iex> resp.body
      %IO.Stream{}

  Stream response body to the current process and parse incoming messages using `Req.parse_message/2`.

      iex> resp = Req.get!("http://httpbin.org/stream/2", into: :self)
      iex> Req.parse_message(resp, receive do message -> message end)
      {:ok, [data: "{\"url\": \"http://httpbin.org/stream/2\", ..., \"id\": 0}\n"]}
      iex> Req.parse_message(resp, receive do message -> message end)
      {:ok, [data: "{\"url\": \"http://httpbin.org/stream/2\", ..., \"id\": 1}\n"]}
      iex> Req.parse_message(resp, receive do message -> message end)
      {:ok, [:done]}
      ""

  Same as above, using enumerable API:

      iex> resp = Req.get!("http://httpbin.org/stream/2", into: :self)
      iex> resp.body
      #Req.Response.Async<...>
      iex> Enum.each(resp.body, &IO.puts/1)
      # {"url": "http://httpbin.org/stream/2", ..., "id": 0}
      # {"url": "http://httpbin.org/stream/2", ..., "id": 1}
      :ok

  See `:into` option in `Req.new/1` documentation for more information on response body streaming.

  ## Header Names

  The HTTP specification requires that header names should be case-insensitive.
  Req allows two ways to access the headers; using functions and by accessing
  the data directly:

      iex> Req.Response.get_header(response, "content-type")
      ["text/html"]

      iex> response.headers["content-type"]
      ["text/html"]

  While we can ensure case-insensitive handling in the former case, we can't
  in the latter. For this reason, Req made the following design choices:

    * header names are stored as downcased

    * functions like `Req.Request.get_header/2`, `Req.Request.put_header/3`,
      `Req.Response.get_header/2`, `Req.Response.put_header/3`, etc
      automatically downcase the given header name.
  """

  # Response streaming to caller:
  #
  #     iex> {req, resp} = Req.async_request!("http://httpbin.org/stream/2")
  #     iex> resp.status
  #     200
  #     iex> resp.body
  #     ""
  #     iex> Req.parse_message(req, receive do message -> message end)
  #     [{:data, "{\"url\": \"http://httpbin.org/stream/2\"" <> ...}]
  #     iex> Req.parse_message(req, receive do message -> message end)
  #     [{:data, "{\"url\": \"http://httpbin.org/stream/2\"" <> ...}]
  #     iex> Req.parse_message(req, receive do message -> message end)
  #     [:done]
  #     ""

  @type url() :: URI.t() | String.t()

  @req Req.Request.new()
       |> Req.Steps.attach()

  @default_finch_options Req.Finch.pool_options(%{})

  @doc """
  Returns a new request struct with built-in steps.

  See `request/2`, `run/2`, as well as `get/2`, `post/2`, and similar functions for
  making requests.

  Also see `Req.Request` module documentation for more information on the underlying request
  struct.

  ## Options

  Basic request options:

    * `:method` - the request method, defaults to `:get`.

    * `:url` - the request URL.

    * `:headers` - the request headers as a `{key, value}` enumerable (e.g. map, keyword list).

      The header names should be downcased.

      The headers are automatically encoded using these rules:

        * atom header names are turned into strings, replacing `_` with `-`. For example,
          `:user_agent` becomes `"user-agent"`.

        * string header names are downcased.

        * `%DateTime{}` header values are encoded as "HTTP date".

      If you set `:headers` options both in `Req.new/1` and `request/2`, the header lists are merged.

      See also "Header Names" section in the module documentation.

    * `:body` - the request body.

      Can be one of:

        * `iodata` - send request body eagerly

        * `enumerable` - stream `enumerable` as request body

  Additional URL options:

    * `:base_url` - if set, the request URL is prepended with this base URL (via
      [`put_base_url`](`Req.Steps.put_base_url/1`) step.)

    * `:params` - if set, appends parameters to the request query string (via
      [`put_params`](`Req.Steps.put_params/1`) step.)

    * `:path_params` - if set, uses a templated request path (via
      [`put_path_params`](`Req.Steps.put_path_params/1`) step.)

    * `:path_params_style` (*available since v0.5.1*) - how path params are expressed (via
      [`put_path_params`](`Req.Steps.put_path_params/1`) step). Can be one of:

         * `:colon` - (default) for Plug-style parameters, such as `:code` in
           `https://httpbin.org/status/:code`.

         * `:curly` - for [OpenAPI](https://swagger.io/specification/)-style parameters, such as
           `{code}` in `https://httpbin.org/status/{code}`.

  Authentication options:

    * `:auth` - sets request authentication (via [`auth`](`Req.Steps.auth/1`) step.)

      Can be one of:

        * `{:basic, userinfo}` - uses Basic HTTP authentication.

        * `{:bearer, token}` - uses Bearer HTTP authentication.

        * `:netrc` - load credentials from the default .netrc file.

        * `{:netrc, path}` - load credentials from `path`.

        * `string` - sets to this value.

        * `&fun/0` - a function that returns one of the above (such as a `{:bearer, token}`).

  Request body encoding options ([`encode_body`](`Req.Steps.encode_body/1`)):

    * `:form` - if set, encodes the request body as `application/x-www-form-urlencoded`

    * `:form_multipart` - if set, encodes the request body as `multipart/form-data`.

    * `:json` - if set, encodes the request body as JSON

  Other request body options:

    * `:compress_body` - if set to `true`, compresses the request body using gzip (via [`compress_body`](`Req.Steps.compress_body/1`) step.)
      Defaults to `false`.

  AWS Signature Version 4 options ([`put_aws_sigv4`](`Req.Steps.put_aws_sigv4/1`) step):

    * `:aws_sigv4` - if set, the AWS options to sign request:

        * `:access_key_id` - the AWS access key id.

        * `:secret_access_key` - the AWS secret access key.

        * `:service` - the AWS service.

        * `:region` - if set, AWS region. Defaults to `"us-east-1"`.

        * `:datetime` - the request datetime, defaults to `DateTime.utc_now(:second)`.

  Response body options:

    * `:compressed` - if set to `true`, asks the server to return compressed response.
      (via [`compressed`](`Req.Steps.compressed/1`) step.) Defaults to `true`.

    * `:raw` - if set to `true`, disables automatic body decompression
      ([`decompress_body`](`Req.Steps.decompress_body/1`) step) and decoding
      ([`decode_body`](`Req.Steps.decode_body/1`) step.) Defaults to `false`.

    * `:decode_body` - if set to `false`, disables automatic response body decoding.
      Defaults to `true`.

    * `:decode_json` - options to pass to `Jason.decode!/2`, defaults to `[]`.

    * `:into` - where to send the response body. It can be one of:

        * `nil` - (default) read the whole response body and store it in the `response.body`
          field.

        * `fun` - stream response body using a function. The first argument is a `{:data, data}`
          tuple containing the chunk of the response body. The second argument is a
          `{request, response}` tuple. To continue streaming chunks, return `{:cont, {req, resp}}`.
          To cancel, return `{:halt, {req, resp}}`. For example:

              into: fn {:data, data}, {req, resp} ->
                IO.puts(data)
                {:cont, {req, resp}}
              end

        * `collectable` - stream response body into a `t:Collectable.t/0`. For example:

               into: File.stream!("path")

        * `:self` - stream response body into the current process mailbox.

          Received messages should be parsed with `Req.parse_message/2`.

          `response.body` is set to opaque data structure `Req.Response.Async` which implements
          `Enumerable` that receives and automatically parses messages. See module documentation
          for example usage.

          If the request is sent using HTTP/1, an extra process is spawned to consume messages
          from the underlying socket. On both HTTP/1 and HTTP/2 the messages are sent to the
          current process as soon as they arrive, as a firehose. If you wish to maximize request
          rate or have more control over how messages are streamed, use `into: fun` or
          `into: collectable` instead.

  Response redirect options ([`redirect`](`Req.Steps.redirect/1`) step):

    * `:redirect` - if set to `false`, disables automatic response redirects. Defaults to `true`.

    * `:redirect_trusted` - by default, authorization credentials are only sent on redirects
      with the same host, scheme and port. If `:redirect_trusted` is set to `true`, credentials
      will be sent to any host.

    * `:max_redirects` - the maximum number of redirects, defaults to `10`.

  Retry options ([`retry`](`Req.Steps.retry/1`) step):

    * `:retry` - can be one of the following:

        * `:safe_transient` (default) - retry safe (GET/HEAD) requests on one of:

            * HTTP 408/429/500/502/503/504 responses

            * `Req.TransportError` with `reason: :timeout | :econnrefused | :closed`

            * `Req.HTTPError` with `protocol: :http2, reason: :unprocessed`

        * `:transient` - same as `:safe_transient` except retries all HTTP methods (POST, DELETE, etc.)

        * `fun` - a 2-arity function that accepts a `Req.Request` and either a `Req.Response` or an exception struct
          and returns one of the following:

            * `true` - retry with the default delay controller by default delay option described below.

            * `{:delay, milliseconds}` - retry with the given delay.

            * `false/nil` - don't retry.

        * `false` - don't retry.

    * `:retry_delay` - if not set, which is the default, the retry delay is determined by
      the value of the `Retry-After` header on HTTP 429/503 responses. If the header is not set,
      the default delay follows a simple exponential backoff: 1s, 2s, 4s, 8s, ...

      `:retry_delay` can be set to a function that receives the retry count (starting at 0)
      and returns the delay, the number of milliseconds to sleep before making another attempt.

    * `:retry_log_level` - the log level to emit retry logs at. Can also be set to `false` to disable
      logging these messages. Defaults to `:warning`.

    * `:max_retries` - maximum number of retry attempts, defaults to `3` (for a total of `4`
      requests to the server, including the initial one.)

  Caching options ([`cache`](`Req.Steps.cache/1`) step):

    * `:cache` - if `true`, performs HTTP caching. Defaults to `false`.

    * `:cache_dir` - the directory to store the cache, defaults to `<user_cache_dir>/req`
      (see: `:filename.basedir/3`)

  Request adapters:

    * `:adapter` - adapter to use to make the actual HTTP request. See `:adapter` field description
      in the `Req.Request` module documentation for more information.

      The default is [`run_finch`](`Req.Steps.run_finch/1`).

    * `:plug` - if set, calls the given plug instead of making an HTTP request over the network (via [`run_plug`](`Req.Steps.run_plug/1`) step).

      The plug can be one of:

        * A _function_ plug: a `fun(conn)` or `fun(conn, options)` function that takes a
          `Plug.Conn` and returns a `Plug.Conn`.

        * A _module_ plug: a `module` name or a `{module, options}` tuple.

  Finch options ([`run_finch`](`Req.Steps.run_finch/1`) step)

    * `:finch` - the Finch pool to use. Defaults to pool automatically started by `Req`.

    * `:connect_options` - dynamically starts (or re-uses already started) Finch pool with
      the given connection options:

        * `:timeout` - socket connect timeout in milliseconds, defaults to `30_000`.

        * `:protocols` - the HTTP protocols to use, defaults to
          `#{inspect(Keyword.fetch!(@default_finch_options, :protocols))}`.

        * `:hostname` - Mint explicit hostname.

        * `:transport_opts` - Mint transport options.

        * `:proxy_headers` - Mint proxy headers.

        * `:proxy` - Mint HTTP/1 proxy settings, a `{schema, address, port, options}` tuple.

        * `:client_settings` - Mint HTTP/2 client settings.

    * `:inet6` - if set to true, uses IPv6. Defaults to `false`.

    * `:pool_timeout` - pool checkout timeout in milliseconds, defaults to `5000`.

    * `:receive_timeout` - socket receive timeout in milliseconds, defaults to `15_000`.

    * `:unix_socket` - if set, connect through the given UNIX domain socket.

    * `:finch_private` - a map or keyword list of private metadata to add to the Finch request. May be useful
      for adding custom data when handling telemetry with `Finch.Telemetry`.

    * `:finch_request` - a function that executes the Finch request, defaults to using `Finch.request/3`.

  ## Examples

      iex> req = Req.new(url: "https://elixir-lang.org")
      iex> req.method
      :get
      iex> URI.to_string(req.url)
      "https://elixir-lang.org"

  Fake adapter:

      iex> fake = fn request ->
      ...>   {request, Req.Response.new(status: 200, body: "it works!")}
      ...> end
      iex>
      iex> req = Req.new(adapter: fake)
      iex> Req.get!(req).body
      "it works!"

  """
  @spec new(options :: keyword()) :: Req.Request.t()
  def new(options \\ []) do
    options = Keyword.merge(default_options(), options)
    {plugins, options} = Keyword.pop(options, :plugins, [])

    @req
    |> run_plugins(plugins)
    |> merge(options)
  end

  defp new(%Req.Request{} = request, options) when is_list(options) do
    Req.merge(request, options)
  end

  defp new(options1, options2) when is_list(options1) and is_list(options2) do
    new(options1 ++ options2)
  end

  defp new(url, options) when (is_binary(url) or is_struct(url, URI)) and is_list(options) do
    new([url: url] ++ options)
  end

  defp new(request, options) when is_list(options) do
    raise ArgumentError,
          "expected 1st argument to be a request, got: #{inspect(request)}"
  end

  defp new(_request, options) do
    raise ArgumentError,
          "expected 2nd argument to be an options keywords list, got: #{inspect(options)}"
  end

  @doc false
  @deprecated "Use Req.merge/2 instead"
  def update(request, options) do
    Req.merge(request, options)
  end

  @doc """
  Updates a request struct.

  See `new/1` for a list of available options. Also see `Req.Request` module documentation
  for more information on the underlying request struct.

  ## Examples

      iex> req = Req.new(base_url: "https://httpbin.org")
      iex> req = Req.merge(req, auth: {:basic, "alice:secret"})
      iex> req.options[:base_url]
      "https://httpbin.org"
      iex> req.options[:auth]
      {:basic, "alice:secret"}

  Passing `:headers` will automatically encode and merge them:

      iex> req = Req.new(headers: %{point_x: 1})
      iex> req = Req.merge(req, headers: %{point_y: 2})
      iex> req.headers
      %{"point-x" => ["1"], "point-y" => ["2"]}

  The same header names are overwritten however:

      iex> req = Req.new(headers: %{authorization: "bearer foo"})
      iex> req = Req.merge(req, headers: %{authorization: "bearer bar"})
      iex> req.headers
      %{"authorization" => ["bearer bar"]}

  Similarly to headers, `:params` are merged too:

      req = Req.new(url: "https://httpbin.org/anything", params: [a: 1, b: 1])
      req = Req.merge(req, params: [a: 2])
      Req.get!(req).body["args"]
      #=> %{"a" => "2", "b" => "1"}
  """
  @spec merge(Req.Request.t(), options :: keyword()) :: Req.Request.t()
  def merge(%Req.Request{} = request, options) when is_list(options) do
    # TODO: Remove on Req 1.0
    if Keyword.has_key?(options, :redact_auth) do
      IO.warn("Setting :redact_auth is deprecated and has no effect")
    end

    request_option_names = [:method, :url, :headers, :body, :adapter, :into]

    {request_options, options} = Keyword.split(options, request_option_names)

    if options[:output] && unquote(!System.get_env("REQ_NOWARN_OUTPUT")) do
      IO.warn("setting `output: path` is deprecated in favour of `into: File.stream!(path)`")
    end

    registered =
      MapSet.union(
        request.registered_options,
        MapSet.new(request_option_names)
      )

    Req.Request.validate_options(options, registered)

    request =
      Enum.reduce(request_options, request, fn
        {:url, url}, acc ->
          put_in(acc.url, URI.parse(url))

        {:headers, new_headers}, acc ->
          update_in(acc.headers, fn old_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 ->
          %{acc | name => value}
      end)

    update_in(
      request.options,
      &Map.merge(&1, Map.new(options), fn
        :params, old, new ->
          Keyword.merge(old, new)

        _, _, new ->
          new
      end)
    )
  end

  @doc """
  Makes a GET request and returns a response or an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> {:ok, resp} = Req.get("https://api.github.com/repos/wojtekmach/req")
      iex> resp.body["description"]
      "Req is a batteries-included HTTP client for Elixir."

  With options:

      iex> {:ok, resp} = Req.get(url: "https://api.github.com/repos/wojtekmach/req")
      iex> resp.status
      200

  With request struct:

      iex> req = Req.new(base_url: "https://api.github.com")
      iex> {:ok, resp} = Req.get(req, url: "/repos/elixir-lang/elixir")
      iex> resp.status
      200

  """
  @doc type: :request
  @spec get(url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def get(request, options \\ []) do
    request(%{new(request, options) | method: :get})
  end

  @doc """
  Makes a GET request and returns a response or raises an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> Req.get!("https://api.github.com/repos/wojtekmach/req").body["description"]
      "Req is a batteries-included HTTP client for Elixir."

  With options:

      iex> Req.get!(url: "https://api.github.com/repos/wojtekmach/req").status
      200

  With request struct:

      iex> req = Req.new(base_url: "https://api.github.com")
      iex> Req.get!(req, url: "/repos/elixir-lang/elixir").status
      200

  """
  @doc type: :request
  @spec get!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t()
  def get!(request, options \\ []) do
    request!(%{new(request, options) | method: :get})
  end

  @doc """
  Makes a HEAD request and returns a response or an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> {:ok, resp} = Req.head("https://httpbin.org/status/201")
      iex> resp.status
      201

  With options:

      iex> {:ok, resp} = Req.head(url: "https://httpbin.org/status/201")
      iex> resp.status
      201

  With request struct:

      iex> req = Req.new(base_url: "https://httpbin.org")
      iex> {:ok, resp} = Req.head(req, url: "/status/201")
      iex> resp.status
      201

  """
  @doc type: :request
  @spec head(url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def head(request, options \\ []) do
    request(%{new(request, options) | method: :head})
  end

  @doc """
  Makes a HEAD request and returns a response or raises an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> Req.head!("https://httpbin.org/status/201").status
      201

  With options:

      iex> Req.head!(url: "https://httpbin.org/status/201").status
      201

  With request struct:

      iex> req = Req.new(base_url: "https://httpbin.org")
      iex> Req.head!(req, url: "/status/201").status
      201
  """
  @doc type: :request
  @spec head!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t()
  def head!(request, options \\ []) do
    request!(%{new(request, options) | method: :head})
  end

  @doc """
  Makes a POST request and returns a response or an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> {:ok, resp} = Req.post("https://httpbin.org/anything", body: "hello!")
      iex> resp.body["data"]
      "hello!"

      iex> {:ok, resp} = Req.post("https://httpbin.org/anything", form: [x: 1])
      iex> resp.body["form"]
      %{"x" => "1"}

      iex> {:ok, resp} = Req.post("https://httpbin.org/anything", json: %{x: 2})
      iex> resp.body["json"]
      %{"x" => 2}

  With options:

      iex> {:ok, resp} = Req.post(url: "https://httpbin.org/anything", body: "hello!")
      iex> resp.body["data"]
      "hello!"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> {:ok, resp} = Req.post(req, body: "hello!")
      iex> resp.body["data"]
      "hello!"
  """
  @doc type: :request
  @spec post(url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def post(request, options \\ []) do
    request(%{new(request, options) | method: :post})
  end

  @doc """
  Makes a POST request and returns a response or raises an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> Req.post!("https://httpbin.org/anything", body: "hello!").body["data"]
      "hello!"

      iex> Req.post!("https://httpbin.org/anything", form: [x: 1]).body["form"]
      %{"x" => "1"}

      iex> Req.post!("https://httpbin.org/anything", json: %{x: 2}).body["json"]
      %{"x" => 2}

  With options:

      iex> Req.post!(url: "https://httpbin.org/anything", body: "hello!").body["data"]
      "hello!"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> Req.post!(req, body: "hello!").body["data"]
      "hello!"
  """
  @doc type: :request
  @spec post!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t()
  def post!(request, options \\ []) do
    request!(%{new(request, options) | method: :post})
  end

  @doc """
  Makes a PUT request and returns a response or an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> {:ok, resp} = Req.put("https://httpbin.org/anything", body: "hello!")
      iex> resp.body["data"]
      "hello!"

  With options:

      iex> {:ok, resp} = Req.put(url: "https://httpbin.org/anything", body: "hello!")
      iex> resp.body["data"]
      "hello!"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> {:ok, resp} = Req.put(req, body: "hello!")
      iex> resp.body["data"]
      "hello!"
  """
  @doc type: :request
  @spec put(url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def put(request, options \\ []) do
    request(%{new(request, options) | method: :put})
  end

  @doc """
  Makes a PUT request and returns a response or raises an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> Req.put!("https://httpbin.org/anything", body: "hello!").body["data"]
      "hello!"

  With options:

      iex> Req.put!(url: "https://httpbin.org/anything", body: "hello!").body["data"]
      "hello!"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> Req.put!(req, body: "hello!").body["data"]
      "hello!"
  """
  @doc type: :request
  @spec put!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t()
  def put!(request, options \\ []) do
    request!(%{new(request, options) | method: :put})
  end

  @doc """
  Makes a PATCH request and returns a response or an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> {:ok, resp} = Req.patch("https://httpbin.org/anything", body: "hello!")
      iex> resp.body["data"]
      "hello!"

  With options:

      iex> {:ok, resp} = Req.patch(url: "https://httpbin.org/anything", body: "hello!")
      iex> resp.body["data"]
      "hello!"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> {:ok, resp} = Req.patch(req, body: "hello!")
      iex> resp.body["data"]
      "hello!"
  """
  @doc type: :request
  @spec patch(url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def patch(request, options \\ []) do
    request(%{new(request, options) | method: :patch})
  end

  @doc """
  Makes a PATCH request and returns a response or raises an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> Req.patch!("https://httpbin.org/anything", body: "hello!").body["data"]
      "hello!"

  With options:

      iex> Req.patch!(url: "https://httpbin.org/anything", body: "hello!").body["data"]
      "hello!"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> Req.patch!(req, body: "hello!").body["data"]
      "hello!"
  """
  @doc type: :request
  @spec patch!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t()
  def patch!(request, options \\ []) do
    request!(%{new(request, options) | method: :patch})
  end

  @doc """
  Makes a DELETE request and returns a response or an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> {:ok, resp} = Req.delete("https://httpbin.org/anything")
      iex> resp.body["method"]
      "DELETE"

  With options:

      iex> {:ok, resp} = Req.delete(url: "https://httpbin.org/anything")
      iex> resp.body["method"]
      "DELETE"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> {:ok, resp} = Req.delete(req)
      iex> resp.body["method"]
      "DELETE"
  """
  @doc type: :request
  @spec delete(url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def delete(request, options \\ []) do
    request(%{new(request, options) | method: :delete})
  end

  @doc """
  Makes a DELETE request and returns a response or raises an error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  ## Examples

  With URL:

      iex> Req.delete!("https://httpbin.org/anything").body["method"]
      "DELETE"

  With options:

      iex> Req.delete!(url: "https://httpbin.org/anything").body["method"]
      "DELETE"

  With request struct:

      iex> req = Req.new(url: "https://httpbin.org/anything")
      iex> Req.delete!(req).body["method"]
      "DELETE"
  """
  @doc type: :request
  @spec delete!(url() | keyword() | Req.Request.t(), options :: keyword()) :: Req.Response.t()
  def delete!(request, options \\ []) do
    request!(%{new(request, options) | method: :delete})
  end

  @doc """
  Makes an HTTP request and returns a response or an error.

  `request` can be one of:

    * a `Keyword` options;
    * a `Req.Request` struct

  See `new/1` for a list of available options.

  Also see `run/2` for a similar function that returns the request and the response or error.

  ## Examples

  With options keywords list:

      iex> {:ok, response} = Req.request(url: "https://api.github.com/repos/wojtekmach/req")
      iex> response.status
      200
      iex> response.body["description"]
      "Req is a batteries-included HTTP client for Elixir."

  With request struct:

      iex> req = Req.new(url: "https://api.github.com/repos/elixir-lang/elixir")
      iex> {:ok, response} = Req.request(req)
      iex> response.status
      200
  """
  @doc type: :request
  @spec request(request :: Req.Request.t() | keyword(), options :: keyword()) ::
          {:ok, Req.Response.t()} | {:error, Exception.t()}
  def request(request, options \\ []) do
    Req.Request.run(new(request, options))
  end

  @doc """
  Makes an HTTP request and returns a response or raises an error.

  See `new/1` for a list of available options.

  Also see `run!/2` for a similar function that returns the request and the response or error.

  ## Examples

  With options keywords list:

      iex> Req.request!(url: "https://api.github.com/repos/elixir-lang/elixir").status
      200

  With request struct:

      iex> req = Req.new(url: "https://api.github.com/repos/elixir-lang/elixir")
      iex> Req.request!(req).status
      200
  """
  @doc type: :request
  @spec request!(request :: Req.Request.t() | keyword(), options :: keyword()) :: Req.Response.t()
  def request!(request, options \\ []) do
    case request(request, options) do
      {:ok, response} -> response
      {:error, exception} -> raise exception
    end
  end

  @doc """
  Makes an HTTP request and returns the request and response or error.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  Also see `request/2` for a similar function that returns the response or error
  (without the request).

  ## Examples

  With options keywords list:

      iex> {req, resp} = Req.run(url: "https://api.github.com/repos/elixir-lang/elixir")
      iex> req.url.host
      "api.github.com"
      iex> resp.status
      200

  With request struct and options:

      iex> req = Req.new(base_url: "https://api.github.com")
      iex> {req, resp} = Req.run(req, url: "/repos/elixir-lang/elixir")
      iex> req.url.host
      "api.github.com"
      iex> resp.status
      200

  Returns an error:

      iex> {_req, exception} = Req.run("http://localhost:9999", retry: false)
      iex> exception
      %Req.TransportError{reason: :econnrefused}

  """
  @doc type: :request
  @spec run(request :: url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {Req.Request.t(), Req.Response.t() | Exception.t()}
  def run(request, options \\ [])

  def run(request, options) when is_list(options) do
    Req.Request.run_request(new(request, options))
  end

  def run(_request, options) do
    raise ArgumentError,
          "expected 2nd argument to be an options keywords list, got: #{inspect(options)}"
  end

  @doc """
  Makes an HTTP request and returns the request and response or raises on errors.

  `request` can be one of:

    * an url (`String` or `URI`);

    * a `Keyword` options;

    * a `Req.Request` struct

  See `new/1` for a list of available options.

  Also see `request!/2` for a similar function that returns the response (without the request).

  ## Examples

  With options keywords list:

      iex> {req, resp} = Req.run!(url: "https://api.github.com/repos/elixir-lang/elixir")
      iex> req.url.host
      "api.github.com"
      iex> resp.status
      200

  With request struct and options:

      iex> req = Req.new(base_url: "https://api.github.com")
      iex> {req, resp} = Req.run!(req, url: "/repos/elixir-lang/elixir")
      iex> req.url.host
      "api.github.com"
      iex> resp.status
      200

  Raises an error:

      iex> Req.run!("http://localhost:9999", retry: false)
      ** (Req.TransportError) connection refused
  """
  @doc type: :request
  @spec run!(request :: url() | keyword() | Req.Request.t(), options :: keyword()) ::
          {Req.Request.t(), Req.Response.t()}
  def run!(request, options \\ []) do
    case run(request, options) do
      {req, %Req.Response{} = resp} -> {req, resp}
      {_req, exception} -> raise exception
    end
  end

  @doc false
  @deprecated "use Req.request(into: self()) instead"
  def async_request(request, options \\ []) do
    Req.Request.run_request(%{new(request, options) | into: :legacy_self})
  end

  @deprecated "use Req.request!(into: self()) instead"
  @doc false
  def async_request!(request, options \\ []) do
    case async_request(request, options) do
      {request, %Req.Response{} = response} ->
        {request, response}

      {_request, exception} ->
        raise exception
    end
  end

  @doc """
  Parses asynchronous response body message.

  A request with option `:into` set to `:self` returns response with asynchronous body.
  In that case, Req sends chunks to the calling process as messages. You'd typically
  get them using `receive/1` or [`handle_info/2`](`c:GenServer.handle_info/2`) in a GenServer.
  These messages should be parsed using this function. The possible return values are:

    * `{:ok, chunks}` - where a chunk can be `{:data, binary}`, `{:trailers, trailers}`, or
      `:done`.

    * `{:error, reason}` - an error occured

    * `:unknown` - the message was not meant for this response.

  See also `Req.Response.Async`.

  ## Examples

      iex> resp = Req.get!("http://httpbin.org/stream/2", into: :self)
      iex> Req.parse_message(resp, receive do message -> message end)
      {:ok, [data: "{\"url\": \"http://httpbin.org/stream/2\", ..., \"id\": 0}\\n"]}
      iex> Req.parse_message(resp, receive do message -> message end)
      {:ok, [data: "{\"url\": \"http://httpbin.org/stream/2\", ..., \"id\": 1}\\n"]}
      iex> Req.parse_message(resp, receive do message -> message end)
      {:ok, [:done]}
      iex> Req.parse_message(resp, :other)
      :unknown
  """
  @doc type: :async
  def parse_message(response, message)

  def parse_message(%Req.Response{body: %Req.Response.Async{stream_fun: fun, ref: ref}}, message) do
    fun.(ref, message)
  end

  def parse_message(%Req.Request{} = request, message) do
    IO.warn(
      "passing %Req.Request{} to parse_message/2 is deprecated. Pass %Req.Response{} instead"
    )

    request.async.stream_fun.(request.async.ref, message)
  end

  @doc """
  Cancels an asynchronous response.

  An asynchronous response is a result of request with `into: :self`.
  See also `Req.Response.Async`.

  ## Examples

      iex> resp = Req.get!("http://httpbin.org/stream/2", into: :self)
      iex> Req.cancel_async_response(resp)
      :ok
  """
  @doc type: :async
  def cancel_async_response(%Req.Response{body: %Req.Response.Async{cancel_fun: fun, ref: ref}}) do
    fun.(ref)
  end

  @deprecated "use Req.cancel_async_response(resp)) instead"
  @doc false
  def cancel_async_request(%Req.Request{} = request) do
    request.async.cancel_fun.(request.async.ref)
  end

  @doc """
  Returns default options.

  See `default_options/1` for more information.
  """
  @spec default_options() :: keyword()
  def default_options() do
    Application.get_env(:req, :default_options, [])
  end

  @doc """
  Sets default options for `Req.new/1`.

  Avoid setting default options in libraries as they are global.

  ## Examples

      iex> Req.default_options(base_url: "https://httpbin.org")
      iex> Req.get!("/statuses/201").status
      201
      iex> Req.new() |> Req.get!(url: "/statuses/201").status
      201
  """
  @spec default_options(keyword()) :: :ok
  def default_options(options) do
    Application.put_env(:req, :default_options, options)
  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([]) do
      []
    end
  end

  defp encode_header_name(name) when is_atom(name) do
    name |> Atom.to_string() |> String.replace("_", "-") |> __ensure_header_downcase__()
  end

  defp encode_header_name(name) when is_binary(name) do
    __ensure_header_downcase__(name)
  end

  defp encode_header_value(%DateTime{} = datetime) do
    datetime |> DateTime.shift_zone!("Etc/UTC") |> Req.Utils.format_http_datetime()
  end

  defp encode_header_value(%NaiveDateTime{} = datetime) do
    IO.warn("setting header to %NaiveDateTime{} is deprecated, use %DateTime{} instead")
    Req.Utils.format_http_datetime(datetime)
  end

  defp encode_header_value(value) when is_binary(value) do
    value
  end

  defp encode_header_value(value) when is_integer(value) do
    Integer.to_string(value)
  end

  defp encode_header_value(value) do
    IO.warn(
      "setting header to value other than string, integer, or %DateTime{} is deprecated," <>
        " got: #{inspect(value)}"
    )

    String.Chars.to_string(value)
  end

  # Plugins support is experimental and undocumented.
  defp run_plugins(request, [plugin | rest]) when is_atom(plugin) do
    run_plugins(plugin.attach(request), rest)
  end

  defp run_plugins(request, [plugin | rest]) when is_function(plugin, 1) do
    run_plugins(plugin.(request), rest)
  end

  defp run_plugins(request, []) do
    request
  end

  @doc false
  @deprecated "Manually build Req.Request struct instead"
  def build(method, url, options \\ []) do
    %Req.Request{
      method: method,
      url: URI.parse(url),
      headers: Keyword.get(options, :headers, []),
      body: Keyword.get(options, :body, "")
    }
  end

  def __ensure_header_downcase__(name) do
    String.downcase(name, :ascii)
  end
end