Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #175

Merged
merged 3 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
* `Assent.Strategy.OAuth2` now supports PKCE
* `Assent.Strategy.OAuth2.Base.authorize_url/2` incomplete typespec fixed
* `Assent.Strategy.decode_response/2` deprecated accepting result tuples and now accepts `Assent.HTTPAdapter.HTTPResponse` structs
* `Assent.Strategy.request/5` deprecated in favor of `Assent.Strategy.http_request/5`
* `Assent.Strategy.decode_response/2` deprecated in favor of `Assent.HTTPAdapter.decode_response/2`
* `Assent.Config.get/3` deprecated in favor of `Keyword.get/3`
* `Assent.Config.put/3` deprecated in favor of `Keyword.put/3`
* `Assent.Config.merge/2` deprecated in favor of `Keyword.merge/2`
* `Assent.Config.t()` type deprecated in favor of `Keyword.t()` type
* `Assent.Config.fetch/2` deprecated in favor of `Assent.fetch_config/2`

## v0.2.10 (2024-04-11)

Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ defmodule ProviderAuth do

@config
# Session params should be added to the config so the strategy can use them
|> Config.put(:session_params, session_params)
|> Keyword.put(:session_params, session_params)
|> Github.callback(params)
|> case do
{:ok, %{user: user, token: token}} ->
Expand Down Expand Up @@ -166,8 +166,6 @@ config :my_app, :strategies,

```elixir
defmodule MultiProviderAuth do
alias Assent.Config

@spec request(atom()) :: {:ok, map()} | {:error, term()}
def request(provider) do
config = config!(provider)
Expand All @@ -180,7 +178,7 @@ defmodule MultiProviderAuth do
config = config!(provider)

config
|> Assent.Config.put(:session_params, session_params)
|> Keyword.put(:session_params, session_params)
|> config[:strategy].callback(params)
end

Expand All @@ -189,7 +187,7 @@ defmodule MultiProviderAuth do
Application.get_env(:my_app, :strategies)[provider] ||
raise "No provider configuration for #{provider}"

Config.put(config, :redirect_uri, "http://localhost:4000/oauth/#{provider}/callback")
Keyword.put(config, :redirect_uri, "http://localhost:4000/oauth/#{provider}/callback")
end
end
```
Expand Down
2 changes: 1 addition & 1 deletion integration/lib/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ defmodule IntegrationServer.Router do

unquote(path)
|> config!()
|> Assent.Config.put(:session_params, get_session(conn, :session_params))
|> Keyword.put(:session_params, get_session(conn, :session_params))
|> unquote(module).callback(conn.params)
|> case do
{:ok, %{user: user, token: token}} ->
Expand Down
84 changes: 79 additions & 5 deletions lib/assent.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
defmodule Assent do
@moduledoc false

defmodule MissingConfigError do
defexception [:key, :config]

@type t :: %__MODULE__{
key: atom(),
config: Keyword.t()
}

def message(exception) do
key = inspect(exception.key)
config_keys = inspect(Keyword.keys(exception.config))

"Expected #{key} in config, got: #{config_keys}"
end
end

defmodule CallbackError do
defexception [:message, :error, :error_uri]
end
Expand All @@ -18,18 +34,33 @@ defmodule Assent do
end

defmodule MissingParamError do
defexception [:expected_key, :params]
defexception [:key, :params]

@type t :: %__MODULE__{
expected_key: binary(),
key: binary(),
params: map()
}

# TODO: Deprecated, remove in 0.3
def exception(opts) do
opts =
case Keyword.fetch(opts, :expected_key) do
{:ok, key} ->
IO.warn("The `expected_key` option is deprecated. Please use `key` instead.")
[key: key, params: opts[:params]]

:error ->
opts
end

struct!(__MODULE__, opts)
end

def message(exception) do
expected_key = inspect(exception.expected_key)
params = inspect(Map.keys(exception.params))
key = inspect(exception.key)
param_keys = exception.params |> Map.keys() |> Enum.sort() |> inspect()

"Expected #{expected_key} in params, got: #{params}"
"Expected #{key} in params, got: #{param_keys}"
end
end

Expand Down Expand Up @@ -112,6 +143,49 @@ defmodule Assent do
end
end

@doc """
Fetches the key value from the configuration.

Returns a `Assent.MissingConfigError` if the key is not found.
"""
@spec fetch_config(Keyword.t(), atom()) :: {:ok, any()} | {:error, MissingConfigError.t()}
def fetch_config(config, key) when is_list(config) and is_atom(key) do
case Keyword.fetch(config, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, MissingConfigError.exception(key: key, config: config)}
end
end

@doc """
Fetches the key value from the params.

Returns a `Assent.MissingParamError` if the key is not found.
"""
@spec fetch_param(map(), binary()) :: {:ok, any()} | {:error, MissingParamError.t()}
def fetch_param(params, key) when is_map(params) and is_binary(key) do
case Map.fetch(params, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, MissingParamError.exception(key: key, params: params)}
end
end

@default_json_library (Code.ensure_loaded?(JSON) && JSON) || Jason

@doc """
Fetches the JSON library in config.

If not found in provided config, this will attempt to load the JSON library
from global application environment for `:assent`. Defaults to
`#{inspect(@default_json_library)}`.
"""
@spec json_library(Keyword.t()) :: module()
def json_library(config) do
case Keyword.fetch(config, :json_library) do
:error -> Application.get_env(:assent, :json_library, @default_json_library)
{:ok, json_library} -> json_library
end
end

import Bitwise

@doc false
Expand Down
49 changes: 13 additions & 36 deletions lib/assent/config.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# TODO: Deprecated, remove in 0.3
defmodule Assent.Config do
@moduledoc """
Methods to handle configurations.
"""
@moduledoc false

defmodule MissingKeyError do
defmodule MissingConfigError do
@type t :: %__MODULE__{}

defexception [:key]
Expand All @@ -15,51 +14,29 @@ defmodule Assent.Config do

@type t :: Keyword.t()

@doc """
Fetches the key value from the configuration.
"""
@spec fetch(t(), atom()) :: {:ok, any()} | {:error, MissingKeyError.t()}
def fetch(config, key) do
case Keyword.fetch(config, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, MissingKeyError.exception(key: key)}
end
end
@doc false
@deprecated "Use Assent.fetch_config/2 instead"
def fetch(config, key), do: Assent.fetch_config(config, key)

@deprecated "Use Keyword.get/3 instead"
defdelegate get(config, key, default), to: Keyword

@deprecated "Use Keyword.put/3 instead"
defdelegate put(config, key, value), to: Keyword

@deprecated "Use Keyword.merge/2 instead"
defdelegate merge(config_a, config_b), to: Keyword

@default_json_library (Code.ensure_loaded?(JSON) && JSON) || Jason

@doc """
Fetches the JSON library in config.

If not found in provided config, this will attempt to load the JSON library
from global application environment for `:assent`. Defaults to
`#{@default_json_library}`.
"""
@spec json_library(t()) :: module()
def json_library(config) do
case get(config, :json_library, nil) do
nil ->
Application.get_env(:assent, :json_library, @default_json_library)

json_library ->
json_library
end
end
@deprecated "Use Assent.json_library/1 instead"
def json_library(config), do: Assent.json_library(config)

# TODO: Remove in next major version
def __base_url__(config) do
case fetch(config, :base_url) do
case Assent.fetch_config(config, :base_url) do
{:ok, base_url} ->
{:ok, base_url}

{:error, error} ->
case fetch(config, :site) do
case Assent.fetch_config(config, :site) do
{:ok, base_url} ->
IO.warn("The `:site` configuration key is deprecated, use `:base_url` instead")
{:ok, base_url}
Expand Down
98 changes: 97 additions & 1 deletion lib/assent/http_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Assent.HTTPAdapter do
end
end
"""
alias Assent.{InvalidResponseError, ServerUnreachableError}

defmodule HTTPResponse do
@moduledoc """
Expand Down Expand Up @@ -69,7 +70,7 @@ defmodule Assent.HTTPAdapter do
{:ok, map()} | {:error, any()}

@doc """
Sets a user agent header
Sets a user agent header.

The header value will be `Assent-VERSION` with VERSION being the `:vsn` of
`:assent` app.
Expand All @@ -80,4 +81,99 @@ defmodule Assent.HTTPAdapter do

{"User-Agent", "Assent-#{version}"}
end

@default_http_client Enum.find_value(
[
{Req, Assent.HTTPAdapter.Req},
{:httpc, Assent.HTTPAdapter.Httpc}
],
fn {dep, module} ->
Code.ensure_loaded?(dep) && {module, []}
end
)

@doc """
Makes a HTTP request.

## Options

- `:http_adapter` - The HTTP adapter to use, defaults to
`#{inspect(elem(@default_http_client, 0))}`.
- `:json_library` - The JSON library to use, see
`Assent.json_library/1`.
"""
@spec request(atom(), binary(), binary() | nil, list(), Keyword.t()) ::
{:ok, HTTPResponse.t()} | {:error, HTTPResponse.t()} | {:error, term()}
def request(method, url, body, headers, opts) do
{http_adapter, http_adapter_opts} = get_adapter(opts)

method
|> http_adapter.request(url, body, headers, http_adapter_opts)
|> case do
{:ok, response} ->
decode_response(response, opts)

{:error, error} ->
{:error,
ServerUnreachableError.exception(
reason: error,
http_adapter: http_adapter,
request_url: url
)}
end
|> case do
{:ok, %{status: status} = resp} when status in 200..399 ->
{:ok, %{resp | http_adapter: http_adapter, request_url: url}}

{:ok, %{status: status} = resp} when status in 400..599 ->
{:error, %{resp | http_adapter: http_adapter, request_url: url}}

{:error, error} ->
{:error, error}
end
end

defp get_adapter(opts) do
default_http_adapter = Application.get_env(:assent, :http_adapter, @default_http_client)

case Keyword.get(opts, :http_adapter, default_http_adapter) do
{http_adapter, opts} -> {http_adapter, opts}
http_adapter when is_atom(http_adapter) -> {http_adapter, nil}
end
end

@doc """
Decodes request response body.

## Options

- `:json_library` - The JSON library to use, see
`Assent.json_library/1`
"""
@spec decode_response(HTTPResponse.t(), Keyword.t()) ::
{:ok, HTTPResponse.t()} | {:error, InvalidResponseError.t()}
def decode_response(%HTTPResponse{} = response, opts) do
case decode(response.headers, response.body, opts) do
{:ok, body} -> {:ok, %{response | body: body}}
{:error, _error} -> {:error, InvalidResponseError.exception(response: response)}
end
end

defp decode(headers, body, opts) when is_binary(body) do
case List.keyfind(headers, "content-type", 0) do
{"content-type", "application/json" <> _rest} ->
Assent.json_library(opts).decode(body)

{"content-type", "text/javascript" <> _rest} ->
Assent.json_library(opts).decode(body)

{"content-type", "application/x-www-form-urlencoded" <> _reset} ->
{:ok, URI.decode_query(body)}

_any ->
{:ok, body}
end
end

defp decode(_headers, body, _opts), do: {:ok, body}
end
Loading
Loading