Current section

Files

Jump to
ash_authentication lib ash_authentication utils.ex
Raw

lib/ash_authentication/utils.ex

# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Utils do
@moduledoc false
alias Ash.{Domain, Resource}
alias Spark.{Dsl, Dsl.Transformer}
@doc """
Returns `true` if `falsy` is either `nil` or `false`.
"""
@spec is_falsy(any) :: Macro.t()
defguard is_falsy(falsy) when falsy in [nil, false]
@doc """
Returns `false` if `truthy` is either `nil` or `false`.
"""
@spec is_truthy(any) :: Macro.t()
defguard is_truthy(truthy) when truthy not in [nil, false]
@doc """
Convert a list of `String.Chars.t` into a sentence.
## Example
iex> ~w[Marty Doc Einstein] |> to_sentence()
"Marty, Doc and Einstein"
"""
@spec to_sentence(Enum.t(), [
{:separator, String.t()} | {:final, String.t()} | {:whitespace, boolean}
]) :: String.t()
def to_sentence(elements, opts \\ []) do
opts =
[separator: ",", final: "and", whitespace: true]
|> Keyword.merge(opts)
|> Map.new()
elements
|> Enum.to_list()
|> case do
[] ->
""
[element] ->
to_string(element)
[_ | _] = elements ->
elements
|> Enum.reverse()
|> convert_to_sentence("", opts.separator, opts.final, opts.whitespace)
end
end
defp convert_to_sentence([last], result, _, final, true), do: "#{result} #{final} #{last}"
defp convert_to_sentence([last], result, _, final, false), do: "#{result}#{final}#{last}"
defp convert_to_sentence([next | rest], "", sep, final, ws),
do: convert_to_sentence(rest, to_string(next), sep, final, ws)
defp convert_to_sentence([next | rest], result, sep, final, true),
do: convert_to_sentence(rest, "#{result}#{sep} #{next}", sep, final, true)
defp convert_to_sentence([next | rest], result, sep, final, false),
do: convert_to_sentence(rest, "#{result}#{sep}#{next}", sep, final, false)
@doc """
Optionally append an element to a collection.
When `test` is truthy, append `element` to the collection.
"""
@spec maybe_append(Enum.t(), test :: any, element :: any) :: Enum.t()
def maybe_append(collection, test, _element) when is_falsy(test), do: collection
def maybe_append(collection, _test, element), do: Enum.concat(collection, [element])
@doc """
Optionally concat a collection to another collection.
When `test` is truthy, concat the collections together.
"""
@spec maybe_concat(Enum.t(), test :: any, Enum.t()) :: Enum.t()
def maybe_concat(collection, test, _new_elements) when is_falsy(test), do: collection
def maybe_concat(collection, _test, new_elements), do: Enum.concat(collection, new_elements)
@doc """
Used within transformers to infer `domain` from a resource if the option is not set.
"""
def maybe_set_domain(dsl_state, section) do
domain = Transformer.get_persisted(dsl_state, :domain)
if domain && !Transformer.get_option(dsl_state, [section], :domain) do
{:ok, Transformer.set_option(dsl_state, [section], :domain, domain)}
else
{:ok, dsl_state}
end
end
@doc """
Used within transformers to optionally build actions as needed.
"""
@spec maybe_build_action(Dsl.t(), atom, (map -> map)) :: {:ok, atom | map} | {:error, any}
def maybe_build_action(dsl_state, action_name, builder) when is_function(builder, 1) do
with nil <- Resource.Info.action(dsl_state, action_name),
{:ok, action} <- builder.(dsl_state) do
{:ok, Transformer.add_entity(dsl_state, [:actions], action)}
else
action when is_map(action) -> {:ok, dsl_state}
{:error, reason} -> {:error, reason}
end
end
@doc """
Used within transformers to optionally build attributes as needed.
"""
@spec maybe_build_attribute(Dsl.t(), atom, atom | module, keyword) :: {:ok, Dsl.t()}
def maybe_build_attribute(dsl_state, name, type, options) do
if Resource.Info.attribute(dsl_state, name) do
{:ok, dsl_state}
else
options =
options
|> Keyword.put(:name, name)
|> Keyword.put(:type, type)
attribute = Transformer.build_entity!(Resource.Dsl, [:attributes], :attribute, options)
{:ok, Transformer.add_entity(dsl_state, [:attributes], attribute)}
end
end
@doc """
Used within transformers to optionally build relationships as needed.
"""
@spec maybe_build_relationship(
Dsl.t(),
relationship_name :: atom,
(Dsl.t() -> {:ok, Resource.Relationships.relationship()})
) :: {:ok, Dsl.t()} | {:error, Exception.t()}
def maybe_build_relationship(dsl_state, relationship_name, builder)
when is_function(builder, 1) do
with :error <- find_relationship(dsl_state, relationship_name),
{:ok, relationship} <- builder.(dsl_state) do
{:ok, Transformer.add_entity(dsl_state, [:relationships], relationship)}
else
{:ok, _relationship} -> {:ok, dsl_state}
{:error, reason} -> {:error, reason}
end
end
@doc """
Find a relationship from a resource.
"""
@spec find_relationship(Dsl.t(), relationship_name :: atom) ::
{:ok, Resource.Relationships.relationship()} | :error
def find_relationship(dsl_state, relationship_name) do
dsl_state
|> Resource.Info.relationships()
|> Enum.find(&(&1.name == relationship_name))
|> case do
nil -> :error
relationship -> {:ok, relationship}
end
end
@doc """
Optionally set a field in a map.
Like `Map.put_new/3` except that it overwrites fields if their contents are
falsy.
"""
@spec maybe_set_field(map, any, any) :: map
def maybe_set_field(map, field, value) when is_falsy(:erlang.map_get(field, map)),
do: Map.put(map, field, value)
def maybe_set_field(map, _field, _value), do: map
@doc """
Like `maybe_set_field/3` except that the value is lazily generated.
"""
@spec maybe_set_field_lazy(input, any, (input -> value)) :: map when input: map, value: any
def maybe_set_field_lazy(map, field, generator)
when is_falsy(:erlang.map_get(field, map)) and is_function(generator, 1),
do: Map.put(map, field, generator.(map))
def maybe_set_field_lazy(map, _field, _generator), do: map
@doc """
Asserts that `resource` is an Ash resource and `extension` is a Spark DSL
extension.
"""
@spec assert_resource_has_extension(Resource.t(), Spark.Dsl.Extension.t()) ::
:ok | {:error, term}
def assert_resource_has_extension(resource, extension) do
with :ok <- assert_is_resource(resource) do
assert_has_extension(resource, extension)
end
end
@doc """
Asserts that `module` is actually an Ash resource.
"""
@spec assert_is_resource(Resource.t()) :: :ok | {:error, term}
def assert_is_resource(module) do
with :ok <- assert_is_module(module),
true <- function_exported?(module, :spark_is, 0),
Resource <- module.spark_is() do
:ok
else
_ ->
{:error, "Module `#{inspect(module)}` is not an Ash resource"}
end
end
@doc """
Asserts that `module` is actually an Ash domain.
"""
@spec assert_is_domain(Domain.t()) :: :ok | {:error, term}
def assert_is_domain(module) do
with :ok <- assert_is_module(module),
true <- function_exported?(module, :spark_is, 0),
Domain <- module.spark_is() do
:ok
else
_ -> {:error, "Module `#{inspect(module)}` is not an Ash domain"}
end
end
@doc """
Asserts that `module` is a Spark DSL extension.
"""
@spec assert_is_extension(Spark.Dsl.Extension.t()) :: :ok | {:error, term}
def assert_is_extension(extension) do
with :ok <- assert_is_module(extension) do
assert_has_behaviour(extension, Spark.Dsl.Extension)
end
end
@doc """
Asserts that `module` is actually a module.
"""
@spec assert_is_module(module) :: :ok | {:error, term}
def assert_is_module(module) when is_atom(module) do
case Code.ensure_compiled(module) do
{:module, _} -> :ok
_ -> {:error, "Argument `#{inspect(module)}` is not a valid module"}
end
end
def assert_is_module(module),
do: {:error, "Argument `#{inspect(module)}` is not a valid module"}
@doc """
Asserts that `module` is extended by `extension`.
"""
@spec assert_has_extension(Resource.t(), Spark.Dsl.Extension.t()) :: :ok | {:error, term}
def assert_has_extension(module, extension) do
if extension in Spark.extensions(module) do
:ok
else
{:error, "Module `#{inspect(module)}` is not extended by `#{inspect(extension)}`"}
end
end
@doc """
Asserts that `module` implements `behaviour`.
"""
@spec assert_has_behaviour(module, module) :: :ok | {:error, term}
def assert_has_behaviour(module, behaviour) do
if Spark.implements_behaviour?(module, behaviour) do
:ok
else
{:error,
"Module `#{inspect(module)}` does not implement the `#{inspect(behaviour)}` behaviour"}
end
end
@doc """
Convert a lifetime to seconds.
"""
@spec lifetime_to_seconds(
lifetime ::
integer
| {integer, :seconds}
| {integer, :minutes}
| {integer, :hours}
| {integer, :days}
) :: integer
def lifetime_to_seconds(seconds) when is_integer(seconds), do: seconds
def lifetime_to_seconds({seconds, :seconds}), do: seconds
def lifetime_to_seconds({minutes, :minutes}), do: minutes * 60
def lifetime_to_seconds({hours, :hours}), do: hours * 60 * 60
def lifetime_to_seconds({days, :days}), do: days * 60 * 60 * 24
@doc """
Normalize claim tenant :null atom to nil.
> To support JOSE 1.11.11+ and Erlang's built-in JSON module, it sends :null instead of nil.
"""
@spec normalize_null(term()) :: term()
def normalize_null(:null), do: nil
def normalize_null(map) when is_map(map),
do: Map.new(map, fn {k, v} -> {k, normalize_null(v)} end)
def normalize_null(value), do: value
@doc """
Apply `Ash.Query.lock/2` only when the resource's data layer supports the
given lock type. On data layers that don't support locking (e.g. ETS),
returns the query unchanged.
This is intentionally a soft fallback: data layers without locking are
typically single-process and serialise writes by other means, so the
atomicity that the lock provides on SQL-backed data layers is already
guaranteed there. Resources that genuinely need lock-based safety should
surface a compile-time warning via the relevant verifier.
"""
@spec maybe_lock(Ash.Query.t(), Ash.DataLayer.lock_type()) :: Ash.Query.t()
def maybe_lock(query, lock_type) do
if Ash.DataLayer.data_layer_can?(query.resource, {:lock, lock_type}) do
Ash.Query.lock(query, lock_type)
else
query
end
end
@doc """
Add a `:lock` keyword option to `opts` only when the resource's data
layer supports the given lock type.
Counterpart to `maybe_lock/2` for code paths that pass options directly
to `Ash.get/3` and friends rather than building a query.
"""
@spec maybe_lock_opt(keyword, Ash.Resource.t(), Ash.DataLayer.lock_type()) :: keyword
def maybe_lock_opt(opts, resource, lock_type) do
if Ash.DataLayer.data_layer_can?(resource, {:lock, lock_type}) do
Keyword.put(opts, :lock, lock_type)
else
opts
end
end
end