Skip to content

Commit

Permalink
Merge pull request #175 from pow-auth/refactor
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
danschultzer authored Dec 29, 2024
2 parents 7948f23 + 95c65d4 commit ad82697
Show file tree
Hide file tree
Showing 34 changed files with 851 additions and 676 deletions.
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

0 comments on commit ad82697

Please sign in to comment.