diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ab9ba50..4dde7577 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: elixir-version: '1.13' otp-version: '24.3' - name: Restore dependencies cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd223c5a..fede8547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: otp-version: ${{ matrix.otp }} version-type: strict - name: Restore dependencies cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} @@ -38,6 +38,44 @@ jobs: - name: Run Tests run: mix test --trace + Test-gun1: + runs-on: ubuntu-latest + name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} - Gun1 + strategy: + matrix: + elixir: + - 1.14 + - 1.13 + otp: + - 25.3 + - 24.3 + steps: + - uses: actions/checkout@v4 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + version-type: strict + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('test/lockfiles/gun1.lock') }} + restore-keys: ${{ runner.os }}-mix- + - name: Install Dependencies + env: + MIX_ENV: test + LOCKFILE: gun1 + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + - name: Run Tests + env: + LOCKFILE: gun1 + run: mix test test/tesla/adapter/gun_test.exs --trace + Linting: runs-on: ubuntu-latest steps: @@ -49,7 +87,7 @@ jobs: otp-version: '24.3' version-type: strict - name: Restore dependencies cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} diff --git a/README.md b/README.md index f51312e9..18e0a929 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Tesla is an HTTP client loosely based on [Faraday](https://github.com/lostisland It embraces the concept of middleware when processing the request/response cycle. > Note that this README refers to the `master` branch of Tesla, not the latest - released version on Hex. See [the documentation](https://hexdocs.pm/tesla) for - the documentation of the version you're using. +> released version on Hex. See [the documentation](https://hexdocs.pm/tesla) for +> the documentation of the version you're using. For the list of changes, checkout the latest [release notes](https://github.com/teamon/tesla/releases). @@ -61,13 +61,13 @@ Add `:tesla` as dependency in `mix.exs`: ```elixir defp deps do [ - {:tesla, "~> 1.4"}, + {:tesla, "~> 1.9"}, # optional, but recommended adapter - {:hackney, "~> 1.17"}, + {:hackney, "~> 1.20"}, # optional, required by JSON middleware - {:jason, ">= 1.0.0"} + {:jason, "~> 1.4"} ] end ``` @@ -83,8 +83,8 @@ config :tesla, adapter: Tesla.Adapter.Hackney ``` > The default adapter is erlang's built-in `httpc`, but it is not recommended -to use it in production environment as it does not validate SSL certificates -[among other issues](https://github.com/teamon/tesla/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Ahttpc+). +> to use it in production environment as it does not validate SSL certificates +> [among other issues](https://github.com/teamon/tesla/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Ahttpc+). ## Documentation @@ -198,8 +198,8 @@ When using adapter other than `:httpc` remember to add it to the dependencies li ```elixir defp deps do [ - {:tesla, "~> 1.4.0"}, - {:hackney, "~> 1.10"} # when using hackney adapter + {:tesla, "~> 1.9"}, + {:hackney, "~> 1.20"} # when using hackney adapter ] end ``` @@ -243,7 +243,11 @@ Tesla.get(client, "/", opts: [adapter: [recv_timeout: 30_000]]) ## Streaming -If adapter supports it, you can pass a [Stream](https://elixir-lang.org/docs/stable/elixir/Stream.html) as body, e.g.: +### Streaming Request Body + +If adapter supports it, you can pass a +[Stream](https://hexdocs.pm/elixir/main/Stream.html) as request +body, e.g.: ```elixir defmodule ElasticSearch do @@ -259,7 +263,41 @@ defmodule ElasticSearch do end ``` -Each piece of stream will be encoded as JSON and sent as a new line (conforming to JSON stream format). +Each piece of stream will be encoded as JSON and sent as a new line (conforming +to JSON stream format). + +### Streaming Response Body + +If adapter supports it, you can pass a `response: :stream` option to return +response body as a +[Stream](https://elixir-lang.org/docs/stable/elixir/Stream.html) + +```elixir +defmodule OpenAI do + def new(token) do + middleware = [ + {Tesla.Middleware.BaseUrl, "https://api.openai.com/v1"}, + {Tesla.Middleware.BearerAuth, token: token}, + {Tesla.Middleware.JSON, decode_content_types: ["text/event-stream"]}, + {Tesla.Middleware.SSE, only: :data} + ] + Tesla.client(middleware, {Tesla.Adapter.Finch, name: MyFinch}) + end + + def completion(client, prompt) do + data = %{ + model: "gpt-3.5-turbo", + messages: [%{role: "user", content: prompt}], + stream: true + } + Tesla.post(client, "/chat/completions", data, opts: [adapter: [response: :stream]]) + end +end +client = OpenAI.new("") +{:ok, env} = OpenAI.completion(client, "What is the meaning of life?") +env.body +|> Stream.each(fn chunk -> IO.inspect(chunk) end) +``` ## Multipart @@ -476,6 +514,7 @@ use Tesla, except: [:delete, :options] ```elixir use Tesla, docs: false ``` + ### Encode only JSON request (do not decode response) ```elixir diff --git a/lib/tesla.ex b/lib/tesla.ex index e5fb3757..3f9f3813 100644 --- a/lib/tesla.ex +++ b/lib/tesla.ex @@ -115,7 +115,7 @@ defmodule Tesla do defp prepare(module, %{pre: pre, post: post} = client, options) do env = struct(Env, options ++ [__module__: module, __client__: client]) - stack = pre ++ module.__middleware__ ++ post ++ [effective_adapter(module, client)] + stack = pre ++ module.__middleware__() ++ post ++ [effective_adapter(module, client)] {env, stack} end diff --git a/lib/tesla/adapter/finch.ex b/lib/tesla/adapter/finch.ex index bed1afa1..0bf65b6b 100644 --- a/lib/tesla/adapter/finch.ex +++ b/lib/tesla/adapter/finch.ex @@ -52,37 +52,99 @@ if Code.ensure_loaded?(Finch) do @behaviour Tesla.Adapter alias Tesla.Multipart + @defaults [ + receive_timeout: 15_000 + ] + @impl Tesla.Adapter def call(%Tesla.Env{} = env, opts) do - opts = Tesla.Adapter.opts(env, opts) + opts = Tesla.Adapter.opts(@defaults, env, opts) name = Keyword.fetch!(opts, :name) url = Tesla.build_url(env.url, env.query) req_opts = Keyword.take(opts, [:pool_timeout, :receive_timeout]) + req = build(env.method, url, env.headers, env.body) - case request(name, env.method, url, env.headers, env.body, req_opts) do + case request(req, name, req_opts, opts) do {:ok, %Finch.Response{status: status, headers: headers, body: body}} -> {:ok, %Tesla.Env{env | status: status, headers: headers, body: body}} - {:error, mint_error} -> - {:error, Exception.message(mint_error)} + {:error, %Mint.TransportError{reason: reason}} -> + {:error, reason} + + {:error, reason} -> + {:error, reason} end end - defp request(name, method, url, headers, %Multipart{} = mp, opts) do + defp build(method, url, headers, %Multipart{} = mp) do headers = headers ++ Multipart.headers(mp) body = Multipart.body(mp) |> Enum.to_list() - request(name, method, url, headers, body, opts) + build(method, url, headers, body) end - defp request(_name, _method, _url, _headers, %Stream{}, _opts) do - raise "Streaming is not supported by this adapter!" + defp build(method, url, headers, %Stream{} = body_stream) do + build(method, url, headers, {:stream, body_stream}) end - defp request(name, method, url, headers, body, opts) do + defp build(method, url, headers, body_stream_fun) when is_function(body_stream_fun) do + build(method, url, headers, {:stream, body_stream_fun}) + end + + defp build(method, url, headers, body) do Finch.build(method, url, headers, body) - |> Finch.request(name, opts) + end + + defp request(req, name, req_opts, opts) do + case opts[:response] do + :stream -> stream(req, name, req_opts) + nil -> Finch.request(req, name, req_opts) + other -> raise "Unknown response option: #{inspect(other)}" + end + end + + defp stream(req, name, opts) do + owner = self() + ref = make_ref() + + fun = fn + {:status, status}, _acc -> status + {:headers, headers}, status -> send(owner, {ref, {:status, status, headers}}) + {:data, data}, _acc -> send(owner, {ref, {:data, data}}) + end + + task = + Task.async(fn -> + case Finch.stream(req, name, nil, fun, opts) do + {:ok, _acc} -> send(owner, {ref, :eof}) + {:error, error} -> send(owner, {ref, {:error, error}}) + end + end) + + receive do + {^ref, {:status, status, headers}} -> + body = + Stream.unfold(nil, fn _ -> + receive do + {^ref, {:data, data}} -> + {data, nil} + + {^ref, :eof} -> + Task.await(task) + nil + after + opts[:receive_timeout] -> + Task.shutdown(task, :brutal_kill) + nil + end + end) + + {:ok, %Finch.Response{status: status, headers: headers, body: body}} + after + opts[:receive_timeout] -> + {:error, :timeout} + end end end end diff --git a/lib/tesla/adapter/gun.ex b/lib/tesla/adapter/gun.ex index 686a1be7..587e3f74 100644 --- a/lib/tesla/adapter/gun.ex +++ b/lib/tesla/adapter/gun.ex @@ -55,7 +55,7 @@ if Code.ensure_loaded?(:gun) do [ssl_verify_fun.erl](https://github.com/deadtrickster/ssl_verify_fun.erl) - `:proxy` - Proxy for requests. - **Socks proxy are supported only for gun master branch**. + **Socks proxy are supported from gun >= 2.0**. Examples: `{'localhost', 1234}`, `{{127, 0, 0, 1}, 1234}`, `{:socks5, 'localhost', 1234}`. **NOTE:** By default GUN uses TLS as transport if the specified port is 443, @@ -435,7 +435,7 @@ if Code.ensure_loaded?(:gun) do end defp open_stream(pid, method, path, headers, body, req_opts, :stream) do - stream = :gun.request(pid, method, path, headers, "", req_opts) + stream = perform_stream_request(pid, method, path, headers, req_opts) for data <- body, do: :ok = :gun.data(pid, stream, :nofin, data) :gun.data(pid, stream, :fin, "") stream @@ -553,5 +553,16 @@ if Code.ensure_loaded?(:gun) do ip end end + + # Backwards compatibility with gun < 2.0. See https://ninenines.eu/docs/en/gun/2.0/manual/gun.headers/ + if Application.spec(:gun, :vsn) |> List.to_string() |> Version.match?("~> 2.0") do + defp perform_stream_request(pid, method, path, headers, req_opts) do + :gun.headers(pid, method, path, headers, req_opts) + end + else + defp perform_stream_request(pid, method, path, headers, req_opts) do + :gun.request(pid, method, path, headers, "", req_opts) + end + end end end diff --git a/lib/tesla/middleware/json.ex b/lib/tesla/middleware/json.ex index b0e481f8..c720b2d8 100644 --- a/lib/tesla/middleware/json.ex +++ b/lib/tesla/middleware/json.ex @@ -113,12 +113,18 @@ defmodule Tesla.Middleware.JSON do end end + defp decode_body(body, opts) when is_struct(body, Stream) or is_function(body), + do: {:ok, decode_stream(body, opts)} + defp decode_body(body, opts), do: process(body, :decode, opts) defp decodable?(env, opts), do: decodable_body?(env) && decodable_content_type?(env, opts) defp decodable_body?(env) do - (is_binary(env.body) && env.body != "") || (is_list(env.body) && env.body != []) + (is_binary(env.body) && env.body != "") || + (is_list(env.body) && env.body != []) || + is_function(env.body) || + is_struct(env.body, Stream) end defp decodable_content_type?(env, opts) do @@ -128,6 +134,15 @@ defmodule Tesla.Middleware.JSON do end end + defp decode_stream(body, opts) do + Stream.map(body, fn chunk -> + case decode_body(chunk, opts) do + {:ok, item} -> item + _ -> chunk + end + end) + end + defp content_types(opts), do: @default_content_types ++ Keyword.get(opts, :decode_content_types, []) diff --git a/lib/tesla/middleware/path_params.ex b/lib/tesla/middleware/path_params.ex index 81dc4c21..e9b98b9a 100644 --- a/lib/tesla/middleware/path_params.ex +++ b/lib/tesla/middleware/path_params.ex @@ -1,12 +1,33 @@ defmodule Tesla.Middleware.PathParams do @moduledoc """ - Use templated URLs with separate params. + Use templated URLs with provided parameters in either Phoenix style (`:id`) + or OpenAPI style (`{id}`). - Useful when logging or reporting metric per URL. + Useful when logging or reporting metrics per URL. + + ## Parameter Values + + Parameter values may be `t:struct/0` or must implement the `Enumerable` + protocol and produce `{key, value}` tuples when enumerated. + + ## Parameter Name Restrictions + + Phoenix style parameters may contain letters, numbers, or underscores, + matching this regular expression: + + :[a-zA-Z][_a-zA-Z0-9]*\b + + OpenAPI style parameters may contain letters, numbers, underscores, or + hyphens (`-`), matching this regular expression: + + \{[a-zA-Z][-_a-zA-Z0-9]*\} + + In either case, parameters that begin with underscores (`_`), hyphens (`-`), + or numbers (`0-9`) are ignored and left as-is. ## Examples - ``` + ```elixir defmodule MyClient do use Tesla @@ -16,7 +37,12 @@ defmodule Tesla.Middleware.PathParams do def user(id) do params = [id: id] - get("/users/:id", opts: [path_params: params]) + get("/users/{id}", opts: [path_params: params]) + end + + def posts(id, post_id) do + params = [id: id, post_id: post_id] + get("/users/:id/posts/:post_id", opts: [path_params: params]) end end ``` @@ -24,19 +50,36 @@ defmodule Tesla.Middleware.PathParams do @behaviour Tesla.Middleware - @rx ~r/:([a-zA-Z]{1}[\w_]*)/ - @impl Tesla.Middleware def call(env, next, _) do url = build_url(env.url, env.opts[:path_params]) Tesla.run(%{env | url: url}, next) end + @rx ~r/:([a-zA-Z][a-zA-Z0-9_]*)|[{]([a-zA-Z][-a-zA-Z0-9_]*)[}]/ + defp build_url(url, nil), do: url - defp build_url(url, params) do - Regex.replace(@rx, url, fn match, key -> - to_string(params[String.to_existing_atom(key)] || match) + defp build_url(url, params) when is_struct(params), do: build_url(url, Map.from_struct(params)) + + defp build_url(url, params) when is_map(params) or is_list(params) do + safe_params = Map.new(params, fn {name, value} -> {to_string(name), value} end) + + Regex.replace(@rx, url, fn + # OpenAPI matches + match, "", name -> replace_param(safe_params, name, match) + # Phoenix matches + match, name, _ -> replace_param(safe_params, name, match) end) end + + defp build_url(url, _params), do: url + + defp replace_param(params, name, match) do + case Map.fetch(params, name) do + {:ok, nil} -> match + :error -> match + {:ok, value} -> URI.encode_www_form(to_string(value)) + end + end end diff --git a/lib/tesla/middleware/sse.ex b/lib/tesla/middleware/sse.ex new file mode 100644 index 00000000..ada9bfac --- /dev/null +++ b/lib/tesla/middleware/sse.ex @@ -0,0 +1,102 @@ +defmodule Tesla.Middleware.SSE do + @moduledoc """ + Decode Server Sent Events. + + This middleware is mostly useful when streaming response body. + + ## Examples + + ``` + plug Tesla.Middleware.SSE, only: :data + + ``` + + ## Options + + - `:only` - keep only specified keys in event (necessary for using with `JSON` middleware) + - `:decode_content_types` - list of additional decodable content-types + """ + + @behaviour Tesla.Middleware + + @default_content_types ["text/event-stream"] + + @impl Tesla.Middleware + def call(env, next, opts) do + opts = opts || [] + + with {:ok, env} <- Tesla.run(env, next) do + decode(env, opts) + end + end + + def decode(env, opts) do + if decodable_content_type?(env, opts) do + {:ok, %{env | body: decode_body(env.body, opts)}} + else + {:ok, env} + end + end + + defp decode_body(body, opts) when is_struct(body, Stream) or is_function(body) do + body + |> Stream.chunk_while( + "", + fn elem, acc -> + {lines, [rest]} = (acc <> elem) |> String.split("\n\n") |> Enum.split(-1) + {:cont, lines, rest} + end, + fn + "" -> {:cont, ""} + acc -> {:cont, acc, ""} + end + ) + |> Stream.flat_map(& &1) + |> Stream.map(&decode_message/1) + |> Stream.flat_map(&only(&1, opts[:only])) + end + + defp decode_body(binary, opts) when is_binary(binary) do + binary + |> String.split("\n\n") + |> Enum.map(&decode_message/1) + |> Enum.flat_map(&only(&1, opts[:only])) + end + + defp decode_message(message) do + message + |> String.split("\n") + |> Enum.map(&decode_body/1) + |> Enum.reduce(%{}, fn + :empty, acc -> acc + {:data, data}, acc -> Map.update(acc, :data, data, &(&1 <> "\n" <> data)) + {key, value}, acc -> Map.put_new(acc, key, value) + end) + end + + defp decode_body(": " <> comment), do: {:comment, comment} + defp decode_body("data: " <> data), do: {:data, data} + defp decode_body("event: " <> event), do: {:event, event} + defp decode_body("id: " <> id), do: {:id, id} + defp decode_body("retry: " <> retry), do: {:retry, retry} + defp decode_body(""), do: :empty + + defp decodable_content_type?(env, opts) do + case Tesla.get_header(env, "content-type") do + nil -> false + content_type -> Enum.any?(content_types(opts), &String.starts_with?(content_type, &1)) + end + end + + defp content_types(opts), + do: @default_content_types ++ Keyword.get(opts, :decode_content_types, []) + + defp only(message, nil), do: [message] + + defp only(message, key) do + case Map.get(message, key) do + nil -> [] + val -> [val] + end + end +end diff --git a/mix.exs b/mix.exs index 15635e1c..0fa4044c 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Tesla.Mixfile do use Mix.Project @source_url "https://github.com/teamon/tesla" - @version "1.7.0" + @version "1.9.0" def project do [ @@ -58,7 +58,7 @@ defmodule Tesla.Mixfile do # http clients {:ibrowse, "4.4.2", optional: true}, {:hackney, "~> 1.6", optional: true}, - {:gun, "~> 1.3", optional: true}, + {:gun, ">= 1.0.0", optional: true}, {:finch, "~> 0.13", optional: true}, {:castore, "~> 0.1 or ~> 1.0", optional: true}, {:mint, "~> 1.0", optional: true}, @@ -85,7 +85,7 @@ defmodule Tesla.Mixfile do # httparrot dependencies {:httparrot, "~> 1.3", only: :test}, - {:cowlib, "~> 2.9", only: :test, override: true}, + {:cowlib, "~> 2.9", only: [:dev, :test], override: true}, {:ranch, "~> 2.1", only: :test, override: true} ] end @@ -129,11 +129,13 @@ defmodule Tesla.Mixfile do Tesla.Middleware.JSON, Tesla.Middleware.KeepRequest, Tesla.Middleware.Logger, + Tesla.Middleware.MessagePack, Tesla.Middleware.MethodOverride, Tesla.Middleware.Opts, Tesla.Middleware.PathParams, Tesla.Middleware.Query, Tesla.Middleware.Retry, + Tesla.Middleware.SSE, Tesla.Middleware.Telemetry, Tesla.Middleware.Timeout ] diff --git a/mix.lock b/mix.lock index 318c6f28..d73523a7 100644 --- a/mix.lock +++ b/mix.lock @@ -1,22 +1,22 @@ %{ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "con_cache": {:hex, :con_cache, "0.14.0", "863acb90fa08017be3129074993af944cf7a4b6c3ee7c06c5cd0ed6b94fbc223", [:mix], [], "hexpm", "50887a8949377d0b707a3c6653b7610de06074751b52d0f267f52135f391aece"}, "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, - "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, - "excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"}, + "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, + "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "fuse": {:hex, :fuse, "2.5.0", "71afa90be21da4e64f94abba9d36472faa2d799c67fedc3bd1752a88ea4c4753", [:rebar3], [], "hexpm", "7f52a1c84571731ad3c91d569e03131cc220ebaa7e2a11034405f0bac46a4fef"}, - "gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"}, - "hackney": {:hex, :hackney, "1.19.1", "59de4716e985dd2b5cbd4954fa1ae187e2b610a9c4520ffcb0b1653c3d6e5559", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "8aa08234bdefc269995c63c2282cf3cd0e36febe3a6bfab11b610572fdd1cad0"}, + "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "httparrot": {:hex, :httparrot, "1.3.0", "dd8dd51e3a3ca30af0f09cb4b9a9773ed41244514d12faa8aa6cf0efd55a0546", [:mix], [{:con_cache, "~> 0.14.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:cowboy, "~> 2.8.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:exjsx, "~> 3.0 or ~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}], "hexpm", "0911d64fa650f13198ea4f7d18c49f73d0d3e060eafbb193dfffa2464f04a0bf"}, "ibrowse": {:hex, :ibrowse, "4.4.2", "7fe943ba6cb88514dca631c7408c4a7897ce69fb0905244e2bfbc839d42f8d45", [:rebar3], [], "hexpm", "f088cee1faf6514b18c7783e8bc64c628d140a239786dc1f58fe9766e9584f41"}, @@ -24,20 +24,20 @@ "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, - "mix_test_watch": {:hex, :mix_test_watch, "1.1.1", "eee6fc570d77ad6851c7bc08de420a47fd1e449ef5ccfa6a77ef68b72e7e51ad", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f82262b54dee533467021723892e15c3267349849f1f737526523ecba4e6baae"}, + "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, - "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, - "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.1", "7b69ed4f40025c005de0b74fce8c0549625d59cb4df12d15c32fe6dc5076ff42", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "6d7a27b7cad2ad69a09cabf6670514cafcec717c8441beb5c96322bac3d05350"}, - "opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.2.2", "85244a49f0c32ae1e2f3d58c477c265bd6125ee3480ade82b0fa9324b85ed3f0", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "04db13302a34bea8350a13ed9d49c22dfd32c4bc590d8aa88b6b4b7e4f346c61"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, + "opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.3.0", "ef5b2059403a1e2b2d2c65914e6962e56371570b8c3ab5323d7a8d3444fb7f84", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "7243cb6de1523c473cba5b1aefa3f85e1ff8cc75d08f367104c1e11919c8c029"}, "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, diff --git a/test/lockfiles/gun1.lock b/test/lockfiles/gun1.lock new file mode 100644 index 00000000..4b822e78 --- /dev/null +++ b/test/lockfiles/gun1.lock @@ -0,0 +1,48 @@ +%{ + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "con_cache": {:hex, :con_cache, "0.14.0", "863acb90fa08017be3129074993af944cf7a4b6c3ee7c06c5cd0ed6b94fbc223", [:mix], [], "hexpm", "50887a8949377d0b707a3c6653b7610de06074751b52d0f267f52135f391aece"}, + "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, + "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, + "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, + "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, + "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, + "fuse": {:hex, :fuse, "2.5.0", "71afa90be21da4e64f94abba9d36472faa2d799c67fedc3bd1752a88ea4c4753", [:rebar3], [], "hexpm", "7f52a1c84571731ad3c91d569e03131cc220ebaa7e2a11034405f0bac46a4fef"}, + "gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"}, + "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "httparrot": {:hex, :httparrot, "1.3.0", "dd8dd51e3a3ca30af0f09cb4b9a9773ed41244514d12faa8aa6cf0efd55a0546", [:mix], [{:con_cache, "~> 0.14.0", [hex: :con_cache, repo: "hexpm", optional: false]}, {:cowboy, "~> 2.8.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:exjsx, "~> 3.0 or ~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}], "hexpm", "0911d64fa650f13198ea4f7d18c49f73d0d3e060eafbb193dfffa2464f04a0bf"}, + "ibrowse": {:hex, :ibrowse, "4.4.0", "2d923325efe0d2cb09b9c6a047b2835a5eda69d8a47ed6ff8bc03628b764e991", [:rebar3], [], "hexpm", "6a8e5988872086f0506bef68311493551ac5beae7c06ba2a00d5e9f97a60f1c2"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, + "msgpax": {:hex, :msgpax, "2.3.1", "28e17c4abb4c57da742e75de62abd9d01c76f1da0b103334de3fb1199610b3d9", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "17c8bf2fc2327b74e4bc6633dd520ffa10ea07b0a2f8ab1932db99044e116df5"}, + "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.1", "7b69ed4f40025c005de0b74fce8c0549625d59cb4df12d15c32fe6dc5076ff42", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "6d7a27b7cad2ad69a09cabf6670514cafcec717c8441beb5c96322bac3d05350"}, + "opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.2.2", "85244a49f0c32ae1e2f3d58c477c265bd6125ee3480ade82b0fa9324b85ed3f0", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "04db13302a34bea8350a13ed9d49c22dfd32c4bc590d8aa88b6b4b7e4f346c61"}, + "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, + "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, +} diff --git a/test/support/adapter_case/stream_request_body.ex b/test/support/adapter_case/stream_request_body.ex index 69cb1c28..bb1be7ff 100644 --- a/test/support/adapter_case/stream_request_body.ex +++ b/test/support/adapter_case/stream_request_body.ex @@ -3,7 +3,7 @@ defmodule Tesla.AdapterCase.StreamRequestBody do quote do alias Tesla.Env - describe "Stream" do + describe "Stream Request" do test "stream request body: Stream.map" do request = %Env{ method: :post, diff --git a/test/support/adapter_case/stream_response_body.ex b/test/support/adapter_case/stream_response_body.ex new file mode 100644 index 00000000..c8cd113f --- /dev/null +++ b/test/support/adapter_case/stream_response_body.ex @@ -0,0 +1,23 @@ +defmodule Tesla.AdapterCase.StreamResponseBody do + defmacro __using__(_) do + quote do + alias Tesla.Env + + describe "Stream Response" do + test "stream response body" do + request = %Env{ + method: :get, + url: "#{@http}/stream/20" + } + + assert {:ok, %Env{} = response} = call(request, response: :stream) + assert response.status == 200 + assert is_function(response.body) || response.body.__struct__ == Stream + + body = Enum.to_list(response.body) + assert Enum.count(body) == 20 + end + end + end + end +end diff --git a/test/tesla/adapter/finch_test.exs b/test/tesla/adapter/finch_test.exs index afe5a076..0eeb30f3 100644 --- a/test/tesla/adapter/finch_test.exs +++ b/test/tesla/adapter/finch_test.exs @@ -6,7 +6,8 @@ defmodule Tesla.Adapter.FinchTest do use Tesla.AdapterCase, adapter: {Tesla.Adapter.Finch, [name: @finch_name]} use Tesla.AdapterCase.Basic use Tesla.AdapterCase.Multipart - # use Tesla.AdapterCase.StreamRequestBody + use Tesla.AdapterCase.StreamRequestBody + use Tesla.AdapterCase.StreamResponseBody use Tesla.AdapterCase.SSL setup do @@ -24,4 +25,22 @@ defmodule Tesla.Adapter.FinchTest do start_supervised!({Finch, opts}) :ok end + + test "Delay request" do + request = %Env{ + method: :head, + url: "#{@http}/delay/1" + } + + assert {:error, :timeout} = call(request, receive_timeout: 100) + end + + test "Delay request with stream" do + request = %Env{ + method: :head, + url: "#{@http}/delay/1" + } + + assert {:error, :timeout} = call(request, receive_timeout: 100, response: :stream) + end end diff --git a/test/tesla/adapter/gun_test.exs b/test/tesla/adapter/gun_test.exs index 924c18e0..af0e7e67 100644 --- a/test/tesla/adapter/gun_test.exs +++ b/test/tesla/adapter/gun_test.exs @@ -16,10 +16,6 @@ defmodule Tesla.Adapter.GunTest do import ExUnit.CaptureLog - setup do - on_exit(fn -> assert Supervisor.which_children(:gun_sup) == [] end) - end - test "fallback adapter timeout option" do request = %Env{ method: :get, @@ -190,16 +186,6 @@ defmodule Tesla.Adapter.GunTest do assert response.status == 500 end - test "error on socks proxy" do - request = %Env{ - method: :get, - url: "#{@http}/status/500" - } - - assert {:error, "socks protocol is not supported"} == - call(request, proxy: {:socks5, 'localhost', 1234}) - end - test "receive gun_up message when receive is false" do request = %Env{ method: :get, @@ -236,6 +222,19 @@ defmodule Tesla.Adapter.GunTest do assert log =~ "Unknown CA" end + # Gun 1.0 backwards compatibility tests + if not (Application.spec(:gun, :vsn) |> List.to_string() |> Version.match?("~> 2.0")) do + test "error on socks proxy" do + request = %Env{ + method: :get, + url: "#{@http}/status/500" + } + + assert {:error, "socks protocol is not supported"} == + call(request, proxy: {:socks5, ~c"localhost", 1234}) + end + end + defp read_body(pid, stream, opts, acc \\ "") do case Gun.read_chunk(pid, stream, opts) do {:fin, body} -> diff --git a/test/tesla/middleware/json_test.exs b/test/tesla/middleware/json_test.exs index c7cec6ec..e23b3dd1 100644 --- a/test/tesla/middleware/json_test.exs +++ b/test/tesla/middleware/json_test.exs @@ -167,6 +167,32 @@ defmodule Tesla.Middleware.JsonTest do end end + describe "Streams" do + test "encode stream" do + adapter = fn env -> + assert IO.iodata_to_binary(Enum.to_list(env.body)) == ~s|{"id":1}\n{"id":2}\n{"id":3}\n| + end + + stream = Stream.map(1..3, fn i -> %{id: i} end) + Tesla.Middleware.JSON.call(%Tesla.Env{body: stream}, [{:fn, adapter}], []) + end + + test "decode stream" do + adapter = fn _env -> + stream = Stream.map(1..3, fn i -> ~s|{"id": #{i}}\n| end) + + {:ok, + %Tesla.Env{ + headers: [{"content-type", "application/json"}], + body: stream + }} + end + + assert {:ok, env} = Tesla.Middleware.JSON.call(%Tesla.Env{}, [{:fn, adapter}], []) + assert Enum.to_list(env.body) == [%{"id" => 1}, %{"id" => 2}, %{"id" => 3}] + end + end + describe "Multipart" do defmodule MultipartClient do use Tesla diff --git a/test/tesla/middleware/path_params_test.exs b/test/tesla/middleware/path_params_test.exs index abdcaa73..6f0938b2 100644 --- a/test/tesla/middleware/path_params_test.exs +++ b/test/tesla/middleware/path_params_test.exs @@ -1,86 +1,303 @@ defmodule Tesla.Middleware.PathParamsTest do use ExUnit.Case, async: true + alias Tesla.Env @middleware Tesla.Middleware.PathParams - test "no params" do - assert {:ok, env} = @middleware.call(%Env{url: "/users/:id"}, [], nil) - assert env.url == "/users/:id" + defmodule TestUser do + defstruct [:id] end - test "passed params" do - opts = [path_params: [id: 42]] - assert {:ok, env} = @middleware.call(%Env{url: "/users/:id", opts: opts}, [], nil) - assert env.url == "/users/42" - end + describe "Phoenix-style params (:id)" do + test "leaves the identifier with no parameters" do + assert {:ok, env} = @middleware.call(%Env{url: "/users/:id"}, [], nil) + assert env.url == "/users/:id" + end - test "value is not given" do - opts = [path_params: [y: 42]] - assert {:ok, env} = @middleware.call(%Env{url: "/users/:x", opts: opts}, [], nil) - assert env.url == "/users/:x" - end + test "replaces the identifier with passed params" do + opts = [path_params: [id: 42]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/:id", opts: opts}, [], nil) + assert env.url == "/users/42" + end - test "value is nil" do - opts = [path_params: [id: nil]] - assert {:ok, env} = @middleware.call(%Env{url: "/users/:id", opts: opts}, [], nil) - assert env.url == "/users/:id" - end + test "replaces the identifier with empty passed params" do + opts = [path_params: [id: ""]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/:id", opts: opts}, [], nil) + assert env.url == "/users/" + end - test "placeholder contains another placeholder" do - opts = [path_params: [id: 1, id_post: 2]] + test "leaves the identifier if no value is given" do + opts = [path_params: [y: 42]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/:x", opts: opts}, [], nil) + assert env.url == "/users/:x" + end - assert {:ok, env} = @middleware.call(%Env{url: "/users/:id/p/:id_post", opts: opts}, [], nil) + test "leaves the identifier if the value is nil" do + opts = [path_params: [id: nil]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/:id", opts: opts}, [], nil) + assert env.url == "/users/:id" + end - assert env.url == "/users/1/p/2" - end + test "correctly handles shorter identifiers in longer identifiers" do + opts = [path_params: [id: 1, id_post: 2]] - test "placeholder starts by number" do - opts = [path_params: ["1id": 1, id_post: 2]] + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:id/p/:id_post", opts: opts}, [], nil) - assert {:ok, env} = @middleware.call(%Env{url: "/users/:1id/p/:id_post", opts: opts}, [], nil) + assert env.url == "/users/1/p/2" + end - assert env.url == "/users/:1id/p/2" - end + test "correctly handles shorter identifiers in longer identifiers (not provided)" do + opts = [path_params: [id: 1]] - test "placeholder with only 1 number" do - opts = [path_params: ["1": 1, id_post: 2]] + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:id/p/:id_post", opts: opts}, [], nil) - assert {:ok, env} = @middleware.call(%Env{url: "/users/:1/p/:id_post", opts: opts}, [], nil) + assert env.url == "/users/1/p/:id_post" + end - assert env.url == "/users/:1/p/2" - end + test "leaves identifiers that start with a number" do + opts = [path_params: ["1id": 1, id_post: 2]] - test "placeholder with only 1 character" do - opts = [path_params: [i: 1, id_post: 2]] + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:1id/p/:id_post", opts: opts}, [], nil) - assert {:ok, env} = @middleware.call(%Env{url: "/users/:i/p/:id_post", opts: opts}, [], nil) + assert env.url == "/users/:1id/p/2" + end - assert env.url == "/users/1/p/2" - end + test "leaves identifiers that are a single digit" do + opts = [path_params: ["1": 1, id_post: 2]] + + assert {:ok, env} = @middleware.call(%Env{url: "/users/:1/p/:id_post", opts: opts}, [], nil) + + assert env.url == "/users/:1/p/2" + end + + test "replaces identifiers one character long" do + opts = [path_params: [i: 1, id_post: 2]] + + assert {:ok, env} = @middleware.call(%Env{url: "/users/:i/p/:id_post", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "leaves identifiers that are only numbers" do + opts = [path_params: ["123": 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:123/p/:id_post", opts: opts}, [], nil) + + assert env.url == "/users/:123/p/2" + end + + test "leaves identifiers that start with underscore" do + opts = [path_params: [_id: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:_id/p/:id_post", opts: opts}, [], nil) - test "placeholder with multiple numbers" do - opts = [path_params: ["123": 1, id_post: 2]] + assert env.url == "/users/:_id/p/2" + end - assert {:ok, env} = @middleware.call(%Env{url: "/users/:123/p/:id_post", opts: opts}, [], nil) + test "replaces any valid identifier" do + opts = [path_params: [id_1_a: 1, id_post: 2]] - assert env.url == "/users/:123/p/2" + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:id_1_a/p/:id_post", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces identifiers that start with a capital letter" do + opts = [path_params: [id_1_a: 1, IdPost: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:id_1_a/p/:IdPost", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces identifiers where the path params is a struct" do + opts = [path_params: %TestUser{id: 1}] + + assert {:ok, env} = @middleware.call(%Env{url: "/users/:id", opts: opts}, [], nil) + assert env.url == "/users/1" + end + + test "URI-encodes path parameters with reserved characters" do + opts = [path_params: [id: "user#1", post_id: "post#2"]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:id/p/:post_id", opts: opts}, [], nil) + + assert env.url == "/users/user%231/p/post%232" + end end - test "placeholder starts by underscore" do - opts = [path_params: [_id: 1, id_post: 2]] + describe "OpenAPI-style params ({id})" do + test "leaves the identifier with no parameters" do + assert {:ok, env} = @middleware.call(%Env{url: "/users/{id}"}, [], nil) + assert env.url == "/users/{id}" + end + + test "replaces the identifier with passed params" do + opts = [path_params: [id: 42]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/{id}", opts: opts}, [], nil) + assert env.url == "/users/42" + end + + test "replaces the identifier with empty passed params" do + opts = [path_params: [id: ""]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/{id}", opts: opts}, [], nil) + assert env.url == "/users/" + end + + test "leaves the identifier if no value is given" do + opts = [path_params: [y: 42]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/{x}", opts: opts}, [], nil) + assert env.url == "/users/{x}" + end + + test "leaves the identifier if the value is nil" do + opts = [path_params: [id: nil]] + assert {:ok, env} = @middleware.call(%Env{url: "/users/{id}", opts: opts}, [], nil) + assert env.url == "/users/{id}" + end + + test "leaves identifiers that start with a number" do + opts = [path_params: ["1id": 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{1id}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/{1id}/p/2" + end + + test "leaves identifiers that are a single digit" do + opts = [path_params: ["1": 1, id_post: 2]] - assert {:ok, env} = @middleware.call(%Env{url: "/users/:_id/p/:id_post", opts: opts}, [], nil) + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{1}/p/{id_post}", opts: opts}, [], nil) - assert env.url == "/users/:_id/p/2" + assert env.url == "/users/{1}/p/2" + end + + test "replaces identifiers one character long" do + opts = [path_params: [i: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{i}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "leaves identifiers that are only numbers" do + opts = [path_params: ["123": 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{123}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/{123}/p/2" + end + + test "leaves identifiers that start with underscore" do + opts = [path_params: [_id: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{_id}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/{_id}/p/2" + end + + test "leaves identifiers that start with dash" do + opts = [path_params: ["-id": 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{-id}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/{-id}/p/2" + end + + test "replaces any valid identifier" do + opts = [path_params: [id_1_a: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id_1_a}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces any valid identifier with hyphens" do + opts = [path_params: [id_1_a: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id_1_a}/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces identifiers that start with a capital letter" do + opts = [path_params: [id_1_a: 1, IdPost: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id_1_a}/p/{IdPost}", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces identifiers where the path params is a struct" do + opts = [path_params: %TestUser{id: 1}] + + assert {:ok, env} = @middleware.call(%Env{url: "/users/{id}", opts: opts}, [], nil) + assert env.url == "/users/1" + end + + test "URI-encodes path parameters with reserved characters" do + opts = [path_params: [id: "user#1", post_id: "post#2"]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id}/p/{post_id}", opts: opts}, [], nil) + + assert env.url == "/users/user%231/p/post%232" + end end - test "placeholder with numbers, underscore and characters" do - opts = [path_params: [id_1_a: 1, id_post: 2]] + describe "Mixed params (not recommended, {id} and :id)" do + test "replaces identifiers one character long" do + opts = [path_params: [i: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/:i/p/{id_post}", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces any valid identifier" do + opts = [path_params: [id_1_a: 1, id_post: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id_1_a}/p/:id_post", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "replaces identifiers that start with a capital letter" do + opts = [path_params: [id_1_a: 1, IdPost: 2]] + + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id_1_a}/p/:IdPost", opts: opts}, [], nil) + + assert env.url == "/users/1/p/2" + end + + test "URI-encodes path parameters with reserved characters" do + opts = [path_params: [id: "user#1", id_post: "post#2"]] - assert {:ok, env} = - @middleware.call(%Env{url: "/users/:id_1_a/p/:id_post", opts: opts}, [], nil) + assert {:ok, env} = + @middleware.call(%Env{url: "/users/{id}/p/:id_post", opts: opts}, [], nil) - assert env.url == "/users/1/p/2" + assert env.url == "/users/user%231/p/post%232" + end end end diff --git a/test/tesla/middleware/retry_test.exs b/test/tesla/middleware/retry_test.exs index 4b9be4a8..351d9d3a 100644 --- a/test/tesla/middleware/retry_test.exs +++ b/test/tesla/middleware/retry_test.exs @@ -3,6 +3,7 @@ defmodule Tesla.Middleware.RetryTest do defmodule LaggyAdapter do def start_link, do: Agent.start_link(fn -> 0 end, name: __MODULE__) + def reset(), do: Agent.update(__MODULE__, fn _ -> 0 end) def call(env, _opts) do Agent.get_and_update(__MODULE__, fn retries -> @@ -59,8 +60,13 @@ defmodule Tesla.Middleware.RetryTest do adapter LaggyAdapter end + setup_all do + {:ok, _pid} = LaggyAdapter.start_link() + :ok + end + setup do - {:ok, _} = LaggyAdapter.start_link() + LaggyAdapter.reset() :ok end diff --git a/test/tesla/middleware/sse_test.exs b/test/tesla/middleware/sse_test.exs new file mode 100644 index 00000000..9f0de25a --- /dev/null +++ b/test/tesla/middleware/sse_test.exs @@ -0,0 +1,124 @@ +defmodule Tesla.Middleware.SSETest do + use ExUnit.Case + + @env %Tesla.Env{ + status: 200, + headers: [{"content-type", "text/event-stream"}] + } + + describe "Basics" do + test "ignore not matching content-type" do + adapter = fn _env -> + {:ok, %Tesla.Env{headers: [{"content-type", "text/plain"}], body: "test"}} + end + + assert {:ok, env} = Tesla.Middleware.SSE.call(%Tesla.Env{}, [{:fn, adapter}], []) + assert env.body == "test" + end + + test "decode comment" do + adapter = fn _env -> + {:ok, %{@env | body: ": comment"}} + end + + assert {:ok, env} = Tesla.Middleware.SSE.call(%Tesla.Env{}, [{:fn, adapter}], []) + assert env.body == [%{comment: "comment"}] + end + + test "decode multiple messages" do + body = """ + : this is a test stream + + data: some text + + data: another message + data: with two lines + """ + + adapter = fn _env -> + {:ok, %{@env | body: body}} + end + + assert {:ok, env} = Tesla.Middleware.SSE.call(%Tesla.Env{}, [{:fn, adapter}], []) + + assert env.body == [ + %{comment: "this is a test stream"}, + %{data: "some text"}, + %{data: "another message\nwith two lines"} + ] + end + + test "decode named events" do + body = """ + event: userconnect + data: {"username": "bobby", "time": "02:33:48"} + + data: Here's a system message of some kind that will get used + data: to accomplish some task. + + event: usermessage + data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."} + """ + + adapter = fn _env -> + {:ok, %{@env | body: body}} + end + + assert {:ok, env} = Tesla.Middleware.SSE.call(%Tesla.Env{}, [{:fn, adapter}], []) + + assert env.body == [ + %{event: "userconnect", data: ~s|{"username": "bobby", "time": "02:33:48"}|}, + %{ + data: + "Here's a system message of some kind that will get used\nto accomplish some task." + }, + %{ + event: "usermessage", + data: ~s|{"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}| + } + ] + end + + test "output only data" do + body = """ + : comment1 + + event: userconnect + data: data1 + + data: data2 + data: data3 + + event: usermessage + data: data4 + """ + + adapter = fn _env -> + {:ok, %{@env | body: body}} + end + + assert {:ok, env} = Tesla.Middleware.SSE.call(%Tesla.Env{}, [{:fn, adapter}], only: :data) + + assert env.body == ["data1", "data2\ndata3", "data4"] + end + + test "handle stream data" do + adapter = fn _env -> + chunks = [ + ~s|dat|, + ~s|a: dat|, + ~s|a1\n\ndata: data2\n\ndata: d|, + ~s|ata3\n\n| + ] + + stream = Stream.map(chunks, & &1) + + {:ok, %{@env | body: stream}} + end + + assert {:ok, env} = Tesla.Middleware.SSE.call(%Tesla.Env{}, [{:fn, adapter}], []) + + assert Enum.to_list(env.body) == [%{data: "data1"}, %{data: "data2"}, %{data: "data3"}] + end + end +end