From 9a248b3f8695a1c72393434b8cd8b4b1a334dcd0 Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Mon, 25 May 2020 10:16:51 -0500 Subject: [PATCH] fix: pass dialyzer (#170) * chore: update deps * fix: pass dialyzer * fix: more specific apns notification payload typespec * fix: proper APNS notification payload spec --- .tool-versions | 1 + CHANGELOG.md | 3 + README.md | 2 +- lib/pigeon/adm/config.ex | 2 +- lib/pigeon/apns/config.ex | 87 ++++++++++------------ lib/pigeon/apns/config_parser.ex | 15 ++++ lib/pigeon/apns/jwt_config.ex | 123 ++++++++++++++----------------- lib/pigeon/apns/notification.ex | 12 +-- lib/pigeon/apns/shared.ex | 8 +- lib/pigeon/config_error.ex | 1 + lib/pigeon/configurable.ex | 2 +- lib/pigeon/http2/client.ex | 3 +- mix.exs | 53 ++++++------- mix.lock | 52 ++++++------- 14 files changed, 183 insertions(+), 181 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..3a771604 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +elixir 1.10.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index cce64c3d..6b1bc852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +# v1.5.1 +* Fixed various typespecs. + ## v1.5.0 * Bumped minimum Elixir version to 1.6 * Raise `Pigeon.ConfigError` when booting invalid config structs. diff --git a/README.md b/README.md index 83fde76d..f3e0ebde 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Add pigeon and kadabra as `mix.exs` dependencies: ```elixir def deps do [ - {:pigeon, "~> 1.5.0"}, + {:pigeon, "~> 1.5.1"}, {:kadabra, "~> 0.4.4"} ] end diff --git a/lib/pigeon/adm/config.ex b/lib/pigeon/adm/config.ex index aeb13147..fdda3505 100644 --- a/lib/pigeon/adm/config.ex +++ b/lib/pigeon/adm/config.ex @@ -56,7 +56,7 @@ defmodule Pigeon.ADM.Config do defp valid_item?(item), do: is_binary(item) and String.length(item) > 0 - @spec validate!(any) :: :ok + @spec validate!(any) :: :ok | no_return def validate!(config) do if valid?(config) do :ok diff --git a/lib/pigeon/apns/config.ex b/lib/pigeon/apns/config.ex index 4d4719b0..90b21008 100644 --- a/lib/pigeon/apns/config.ex +++ b/lib/pigeon/apns/config.ex @@ -3,15 +3,15 @@ defmodule Pigeon.APNS.Config do Configuration for APNS Workers using certificates. """ - defstruct name: nil, - reconnect: true, - cert: nil, + defstruct cert: nil, certfile: nil, key: nil, keyfile: nil, - uri: nil, + name: nil, port: 443, - ping_period: 600_000 + ping_period: 600_000, + reconnect: true, + uri: nil @typedoc ~S""" Certificate APNS configuration struct @@ -152,9 +152,14 @@ end defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do @moduledoc false - alias Pigeon.APNS.Shared + alias Pigeon.APNS.{ + Config, + ConfigParser, + Shared + } @type sock :: {:sslsocket, any, pid | {any, any}} + @type socket_opts :: maybe_improper_list(atom, integer | boolean) # Configurable Callbacks @@ -166,13 +171,8 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do def connect(%{uri: uri} = config) do uri = to_charlist(uri) - case connect_socket_options(config) do - {:ok, options} -> - Pigeon.Http2.Client.default().connect(uri, :https, options) - - error -> - error - end + options = connect_socket_options(config) + Pigeon.Http2.Client.default().connect(uri, :https, options) end defdelegate push_headers(config, notification, opts), to: Shared @@ -186,53 +186,42 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do defdelegate close(config), to: Shared - def validate!(config) do - case config do - %{cert: {:error, _}, certfile: {:error, _}} -> - raise Pigeon.ConfigError, - reason: "attempted to start without valid certificate", - config: redact(config) - - %{key: {:error, _}, keyfile: {:error, _}} -> - raise Pigeon.ConfigError, - reason: "attempted to start without valid key", - config: redact(config) + @spec validate!(Config.t()) :: :ok | no_return + def validate!(%Config{cert: {:error, _}, certfile: {:error, _}} = config) do + raise Pigeon.ConfigError, + reason: "attempted to start without valid certificate", + config: ConfigParser.redact(config) + end - _ -> - :ok - end + def validate!(%Config{key: {:error, _}, keyfile: {:error, _}} = config) do + raise Pigeon.ConfigError, + reason: "attempted to start without valid key", + config: ConfigParser.redact(config) end - defp redact(config) do - [:cert, :key] - |> Enum.reduce(config, fn key, acc -> - case Map.get(acc, key) do - bin when is_binary(bin) -> Map.put(acc, key, "[FILTERED]") - {:RSAPrivateKey, _bin} -> Map.put(acc, key, "[FILTERED]") - _ -> acc - end - end) + def validate!(%Config{}) do + :ok end + @spec connect_socket_options(Config.t()) :: socket_opts def connect_socket_options(config) do - options = - [ - cert_option(config), - key_option(config), - {:password, ''}, - {:packet, 0}, - {:reuseaddr, true}, - {:active, true}, - :binary - ] - |> Shared.add_port(config) - - {:ok, options} + [ + cert_option(config), + key_option(config), + {:password, ''}, + {:packet, 0}, + {:reuseaddr, true}, + {:active, true}, + :binary + ] + |> Shared.add_port(config) end + @spec cert_option(Config.t()) :: {:cert, binary} | {:certfile, binary} def cert_option(%{cert: cert, certfile: nil}), do: {:cert, cert} def cert_option(%{cert: nil, certfile: file}), do: {:certfile, file} + @spec key_option(Config.t()) :: {:key, binary} | {:keyfile, binary} def key_option(%{key: key, keyfile: nil}), do: {:key, key} def key_option(%{key: nil, keyfile: file}), do: {:keyfile, file} end diff --git a/lib/pigeon/apns/config_parser.ex b/lib/pigeon/apns/config_parser.ex index 50d8a3ff..7e70dda1 100644 --- a/lib/pigeon/apns/config_parser.ex +++ b/lib/pigeon/apns/config_parser.ex @@ -36,12 +36,14 @@ defmodule Pigeon.APNS.ConfigParser do |> parse() end + @spec config_type(any) :: module | :error defp config_type(%{cert: _cert, key_identifier: _key_id}), do: :error defp config_type(%{cert: _cert}), do: Config defp config_type(%{key_identifier: _jwt_key}), do: JWTConfig defp config_type(_else), do: :error @doc false + @spec file_path(binary) :: binary | {:error, {:nofile, binary}} def file_path(path) when is_binary(path) do if :filelib.is_file(path) do Path.expand(path) @@ -58,6 +60,18 @@ defmodule Pigeon.APNS.ConfigParser do def file_path(other), do: {:error, {:nofile, other}} + @doc false + def redact(config) when is_map(config) do + [:cert, :key, :keyfile] + |> Enum.reduce(config, fn key, acc -> + case Map.get(acc, key) do + bin when is_binary(bin) -> Map.put(acc, key, "[FILTERED]") + {:RSAPrivateKey, _bin} -> Map.put(acc, key, "[FILTERED]") + _ -> acc + end + end) + end + @doc false def strip_errors(config, key1, key2) do case {Map.get(config, key1), Map.get(config, key2)} do @@ -67,6 +81,7 @@ defmodule Pigeon.APNS.ConfigParser do end end + @spec uri_for_mode(atom) :: binary | nil def uri_for_mode(:dev), do: @apns_development_api_uri def uri_for_mode(:prod), do: @apns_production_api_uri def uri_for_mode(_else), do: nil diff --git a/lib/pigeon/apns/jwt_config.ex b/lib/pigeon/apns/jwt_config.ex index 05d9d75e..bf5c6e11 100644 --- a/lib/pigeon/apns/jwt_config.ex +++ b/lib/pigeon/apns/jwt_config.ex @@ -3,17 +3,19 @@ defmodule Pigeon.APNS.JWTConfig do Configuration for APNS Workers using JWT. """ - defstruct name: nil, - reconnect: true, - uri: nil, - port: 443, - ping_period: 600_000, - key: nil, + defstruct key: nil, keyfile: nil, key_identifier: nil, - team_id: nil + name: nil, + ping_period: 600_000, + port: 443, + reconnect: true, + team_id: nil, + uri: nil - alias Pigeon.APNS.{Config, ConfigParser} + alias Pigeon.APNS.ConfigParser + + @type headers :: [{binary, binary}] @typedoc ~S""" JWT APNS configuration struct @@ -41,8 +43,8 @@ defmodule Pigeon.APNS.JWTConfig do uri: binary | nil, port: pos_integer, ping_period: pos_integer, - key: binary | nil, - keyfile: binary | nil, + key: binary | nil | {:error, term}, + keyfile: binary | nil | {:error, term}, key_identifier: binary | nil, team_id: binary | nil } @@ -142,9 +144,16 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do import Joken.Config - alias Pigeon.APNS.{Config, JWTConfig, Notification, Shared} + alias Pigeon.APNS.{ + ConfigParser, + JWTConfig, + Notification, + Shared + } + @type headers :: [{binary, binary}] @type sock :: {:sslsocket, any, pid | {any, any}} + @type socket_opts :: maybe_improper_list(atom, integer | boolean) # Seconds @token_max_age 3_590 @@ -155,22 +164,16 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do defdelegate max_demand(any), to: Shared - @spec connect(any) :: {:ok, sock} | {:error, String.t()} + @spec connect(any) :: {:ok, sock} | {:error, binary} def connect(%{uri: uri} = config) do uri = to_charlist(uri) - case connect_socket_options(config) do - {:ok, options} -> - Pigeon.Http2.Client.default().connect(uri, :https, options) - - error -> - error - end + options = connect_socket_options(config) + Pigeon.Http2.Client.default().connect(uri, :https, options) end - @spec push_headers(JWTConfig.t(), Notification.t(), Keyword.t()) :: - Shared.headers() - def push_headers(config, notification, opts) do + @spec push_headers(JWTConfig.t(), Notification.t(), Keyword.t()) :: headers | no_return + def push_headers(%JWTConfig{} = config, notification, opts) do config |> Shared.push_headers(notification, opts) |> put_bearer_token(config) @@ -185,54 +188,42 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do defdelegate close(config), to: Shared - def validate!(config) do - case config do - %{team_id: nil} -> - raise Pigeon.ConfigError, - reason: "attempted to start without valid team_id", - config: redact(config) - - %{key_identifier: nil} -> - raise Pigeon.ConfigError, - reason: "attempted to start without valid key_identifier", - config: redact(config) - - %{key: {:error, _}, keyfile: {:error, _}} -> - raise Pigeon.ConfigError, - reason: "attempted to start without valid key", - config: redact(config) - - _ -> - :ok - end + @spec validate!(JWTConfig.t()) :: :ok | no_return + def validate!(%JWTConfig{team_id: nil} = config) do + raise Pigeon.ConfigError, + reason: "attempted to start without valid team_id", + config: ConfigParser.redact(config) end - defp redact(config) do - [:key, :keyfile] - |> Enum.reduce(config, fn key, acc -> - case Map.get(acc, key) do - bin when is_binary(bin) -> Map.put(acc, key, "[FILTERED]") - {:RSAPrivateKey, _bin} -> Map.put(acc, key, "[FILTERED]") - _ -> acc - end - end) + def validate!(%JWTConfig{key_identifier: nil} = config) do + raise Pigeon.ConfigError, + reason: "attempted to start without valid key_identifier", + config: ConfigParser.redact(config) + end + + def validate!(%JWTConfig{key: {:error, _}, keyfile: {:error, _}} = config) do + raise Pigeon.ConfigError, + reason: "attempted to start without valid key", + config: ConfigParser.redact(config) + end + + def validate!(%JWTConfig{}) do + :ok end + @spec connect_socket_options(JWTConfig.t()) :: socket_opts def connect_socket_options(%{key: _jwt_key} = config) do - options = - [ - {:packet, 0}, - {:reuseaddr, true}, - {:active, true}, - :binary - ] - |> Shared.add_port(config) - - {:ok, options} + [ + {:packet, 0}, + {:reuseaddr, true}, + {:active, true}, + :binary + ] + |> Shared.add_port(config) end - @spec put_bearer_token(Config.headers(), JWTConfig.t()) :: Config.headers() - defp put_bearer_token(headers, config) do + @spec put_bearer_token(headers, JWTConfig.t()) :: headers + defp put_bearer_token(headers, %JWTConfig{} = config) when is_list(headers) do token_storage_key = config.key_identifier <> ":" <> config.team_id {timestamp, saved_token} = Pigeon.APNS.Token.get(token_storage_key) now = :os.system_time(:seconds) @@ -246,7 +237,7 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do [{"authorization", "bearer " <> token} | headers] end - @spec generate_apns_jwt(JWTConfig.t(), String.t()) :: String.t() + @spec generate_apns_jwt(JWTConfig.t(), binary) :: binary defp generate_apns_jwt(config, token_storage_key) do key = get_token_key(config) now = :os.system_time(:seconds) @@ -255,14 +246,14 @@ defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do {:ok, token, _claims} = default_claims(iss: config.team_id, iat: now) - |> Joken.generate_and_sign(nil, signer) + |> Joken.generate_and_sign(%{}, signer) :ok = Pigeon.APNS.Token.update(token_storage_key, {now, token}) token end - @spec get_token_key(JWTConfig.t()) :: JOSE.JWK.t() + @spec get_token_key(JWTConfig.t()) :: %{binary => binary} defp get_token_key(%JWTConfig{keyfile: nil} = config) do %{"pem" => config.key} end diff --git a/lib/pigeon/apns/notification.ex b/lib/pigeon/apns/notification.ex index 9cefa2d1..fbcc8342 100644 --- a/lib/pigeon/apns/notification.ex +++ b/lib/pigeon/apns/notification.ex @@ -39,7 +39,7 @@ defmodule Pigeon.APNS.Notification do priority: non_neg_integer | nil, push_type: String.t() | nil, id: String.t() | nil, - payload: %{String.t() => String.t()}, + payload: %{String.t() => term}, response: response, topic: String.t() | nil } @@ -47,11 +47,11 @@ defmodule Pigeon.APNS.Notification do @typedoc ~S""" APNS push response - - nil - Push has not been sent yet - - `:success` - Push was successfully sent + - nil - Push has not been sent yet. + - `:success` - Push was successfully sent. - `t:Pigeon.APNS.Error.error_response/0` - Push attempted but - server responded with error - - `:timeout` - Internal error. Push did not reach APNS servers + server responded with error. + - `:timeout` - Internal error. Push did not reach APNS servers. """ @type response :: nil | :success | Error.error_response() | :timeout @@ -279,7 +279,7 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_custom(t, %{String.t() => String.t()}) :: t + @spec put_custom(t, %{String.t() => term}) :: t def put_custom(notification, data) do new_payload = Map.merge(notification.payload, data) %{notification | payload: new_payload} diff --git a/lib/pigeon/apns/shared.ex b/lib/pigeon/apns/shared.ex index c33b4b6c..bdd3da80 100644 --- a/lib/pigeon/apns/shared.ex +++ b/lib/pigeon/apns/shared.ex @@ -7,7 +7,8 @@ defmodule Pigeon.APNS.Shared do @type config :: Config.t() | JWTConfig.t() - @type headers :: [{binary(), any()}] + @type headers :: [{String.t(), String.t()}] + @type opts :: Keyword.t() @apns_id "apns-id" @apns_topic "apns-topic" @@ -22,7 +23,7 @@ defmodule Pigeon.APNS.Shared do @spec max_demand(any) :: non_neg_integer def max_demand(_config), do: 1_000 - @spec push_headers(Config.t(), Notification.t(), Keyword.t()) :: headers() + @spec push_headers(config, Notification.t(), opts) :: headers() def push_headers(_config, notification, _opts) do json = Poison.encode!(notification.payload) @@ -39,8 +40,7 @@ defmodule Pigeon.APNS.Shared do |> put_header(@apns_collapse_id, notification.collapse_id) end - @spec push_payload(config, Notification.t(), Keyword.t()) :: - iodata | no_return + @spec push_payload(config, Notification.t(), opts) :: iodata | no_return def push_payload(_config, notification, _opts) do Poison.encode!(notification.payload) end diff --git a/lib/pigeon/config_error.ex b/lib/pigeon/config_error.ex index e99a7e7e..159da4c1 100644 --- a/lib/pigeon/config_error.ex +++ b/lib/pigeon/config_error.ex @@ -2,6 +2,7 @@ defmodule Pigeon.ConfigError do defexception reason: nil, config: nil @impl true + @spec message(map) :: binary def message(%{config: config, reason: reason}) do """ #{reason} diff --git a/lib/pigeon/configurable.ex b/lib/pigeon/configurable.ex index 22a399d6..0f238745 100644 --- a/lib/pigeon/configurable.ex +++ b/lib/pigeon/configurable.ex @@ -53,6 +53,6 @@ defprotocol Pigeon.Configurable do def close(config) - @spec validate!(any) :: :ok + @spec validate!(any) :: :ok | no_return def validate!(config) end diff --git a/lib/pigeon/http2/client.ex b/lib/pigeon/http2/client.ex index ef941f40..9f579f78 100644 --- a/lib/pigeon/http2/client.ex +++ b/lib/pigeon/http2/client.ex @@ -100,7 +100,8 @@ defmodule Pigeon.Http2.Client do @callback send_ping(pid) :: :ok - @callback send_request(pid, headers :: Keyword.t(), data :: String.t()) :: :ok + @callback send_request(pid, headers :: [{binary, binary}, ...], data :: String.t()) :: + :ok @callback handle_end_stream(msg :: term, state :: term) :: {:ok, %Pigeon.Http2.Stream{}} diff --git a/mix.exs b/mix.exs index ac18ff32..13537854 100644 --- a/mix.exs +++ b/mix.exs @@ -1,27 +1,14 @@ defmodule Pigeon.Mixfile do use Mix.Project - @version "1.5.0" + @version "1.5.1" def project do [ app: :pigeon, - name: "Pigeon", - version: @version, - elixir: "~> 1.6", - elixirc_paths: elixirc_paths(Mix.env()), - source_url: "https://github.com/codedge-llc/pigeon", - description: description(), - package: package(), build_embedded: Mix.env() == :prod, - start_permanent: Mix.env() == :prod, - test_coverage: [tool: ExCoveralls], - preferred_cli_env: [ - coveralls: :test, - "coveralls.detail": :test, - "coveralls.post": :test, - "coveralls.html": :test - ], + deps: deps(), + description: description(), dialyzer: [ plt_add_apps: [:kadabra, :poison], ignore_warnings: "config/dialyzer.ignore-warnings" @@ -36,7 +23,21 @@ defmodule Pigeon.Mixfile do "CHANGELOG.md" ] ], - deps: deps() + elixir: "~> 1.6", + elixirc_options: [warnings_as_errors: true], + elixirc_paths: elixirc_paths(Mix.env()), + name: "Pigeon", + package: package(), + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test + ], + source_url: "https://github.com/codedge-llc/pigeon", + start_permanent: Mix.env() == :prod, + test_coverage: [tool: ExCoveralls], + version: @version ] end @@ -49,16 +50,16 @@ defmodule Pigeon.Mixfile do defp deps do [ - {:poison, "~> 2.0 or ~> 3.0 or ~> 4.0"}, - {:httpoison, "~> 0.7 or ~> 1.0"}, + {:credo, "~> 1.0", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, + {:earmark, "~> 1.0", only: :dev}, + {:excoveralls, "~> 0.5", only: :test}, + {:ex_doc, "~> 0.18", only: :dev}, {:gen_stage, "~> 0.12 or ~> 1.0"}, + {:httpoison, "~> 0.7 or ~> 1.0"}, {:joken, "~> 2.1"}, {:kadabra, "~> 0.4.3", optional: true}, - {:earmark, "~> 1.0", only: :dev}, - {:ex_doc, "~> 0.18", only: :dev}, - {:excoveralls, "~> 0.5", only: :test}, - {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, - {:credo, "~> 1.0", only: [:dev, :test], runtime: false} + {:poison, "~> 2.0 or ~> 3.0 or ~> 4.0"} ] end @@ -72,9 +73,9 @@ defmodule Pigeon.Mixfile do defp package do [ files: ["lib", "mix.exs", "README*", "LICENSE*"], - maintainers: ["Henry Popp", "Tyler Hurst"], licenses: ["MIT"], - links: %{"GitHub" => "https://github.com/codedge-llc/pigeon"} + links: %{"GitHub" => "https://github.com/codedge-llc/pigeon"}, + maintainers: ["Henry Popp", "Tyler Hurst"] ] end end diff --git a/mix.lock b/mix.lock index c95da718..3909cfb5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,28 +1,28 @@ %{ - "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, - "credo": {:hex, :credo, "1.2.2", "f57faf60e0a12b0ba9fd4bad07966057fde162b33496c509b95b027993494aab", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.12.2", "a513defac45c59e310ac42fcf2b8ae96f1f85746410f30b1ff2b710a4b6cd44b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm"}, - "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, - "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm"}, - "kadabra": {:hex, :kadabra, "0.4.4", "29d7f4c231d44a59d99b550fa75489a80a572140b3316809f326bf608bce33d8", [:mix], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, - "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, + "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, + "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, + "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, + "excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "568a3e616c264283f5dea5b020783ae40eef3f7ee2163f7a67cbd7b35bcadada"}, + "gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm", "1d9fc978db5305ac54e6f5fec7adf80cd893b1000cf78271564c516aa2af7706"}, + "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, + "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"}, + "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, + "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, + "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"}, + "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, + "kadabra": {:hex, :kadabra, "0.4.5", "dff2ac761751df1d5e746c5bb32bccf30f26371b153ff4d99d620a172ad466aa", [:mix], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "8656a78f48e89ecb21769fdd15fd7562d317b234f602664089ce10124e9ab959"}, + "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, }