mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
improvement: add first class support for enum types
This commit is contained in:
parent
64876c6e19
commit
dd82fcc53b
3 changed files with 185 additions and 2 deletions
|
@ -746,8 +746,6 @@ defmodule Ash.Query do
|
|||
|
||||
@doc """
|
||||
Merge a map of values into the query context
|
||||
|
||||
Not much uses this currently.
|
||||
"""
|
||||
@spec set_context(t() | Ash.Resource.t(), map | nil) :: t()
|
||||
def set_context(query, nil), do: to_query(query)
|
||||
|
|
111
lib/ash/type/enum.ex
Normal file
111
lib/ash/type/enum.ex
Normal file
|
@ -0,0 +1,111 @@
|
|||
defmodule Ash.Type.Enum do
|
||||
@moduledoc """
|
||||
A type for abstracting enums into a single type.
|
||||
|
||||
For example, you might have:
|
||||
```elixir
|
||||
attribute :status, :atom, constraints: [:open, :closed]
|
||||
```
|
||||
|
||||
But as that starts to spread around your system you may find that you want
|
||||
to centralize that logic. To do that, use this module to define an Ash type
|
||||
easily.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.TicketStatus do
|
||||
use Ash.Type.Enum, values: [:open, :closed]
|
||||
end
|
||||
```
|
||||
|
||||
Valid values are:
|
||||
|
||||
* The atom itself, e.g `:open`
|
||||
* A string that matches the atom, e.g `"open"`
|
||||
* A string that matches the atom after being downcased, e.g `"OPEN"` or `"oPeN"`
|
||||
* A string that matches the stringified, downcased atom, after itself being downcased.
|
||||
This allows for enum values like `:Open`, `:SomeState` and `:Some_State`
|
||||
"""
|
||||
@doc "The list of valid values (not all input types that match them)"
|
||||
@callback values() :: [atom]
|
||||
@doc "true if a given term matches a value"
|
||||
@callback match?(term) :: boolean
|
||||
@doc "finds the valid value that matches a given input term"
|
||||
@callback match(term) :: {:ok, atom} | :error
|
||||
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, generated: true do
|
||||
use Ash.Type
|
||||
|
||||
@behaviour unquote(__MODULE__)
|
||||
|
||||
@values unquote(opts[:values]) ||
|
||||
raise("Must provide `values` option for `use #{inspect(unquote(__MODULE__))}`")
|
||||
@string_values @values |> Enum.map(&to_string/1)
|
||||
|
||||
@impl unquote(__MODULE__)
|
||||
def values, do: @values
|
||||
|
||||
@impl Ash.Type
|
||||
def storage_type, do: :string
|
||||
|
||||
@impl Ash.Type
|
||||
def cast_input(value, _) do
|
||||
match(value)
|
||||
end
|
||||
|
||||
@impl Ash.Type
|
||||
def cast_stored(value, _) do
|
||||
match(value)
|
||||
end
|
||||
|
||||
@impl Ash.Type
|
||||
def dump_to_native(value, _) do
|
||||
{:ok, to_string(value)}
|
||||
end
|
||||
|
||||
@impl unquote(__MODULE__)
|
||||
@spec match?(term) :: boolean
|
||||
def match?(term) do
|
||||
case match(term) do
|
||||
{:ok, _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@impl unquote(__MODULE__)
|
||||
@spec match(term) :: {:ok, term} | :error
|
||||
def match(value) when value in @values, do: {:ok, value}
|
||||
def match(value) when value in @string_values, do: {:ok, String.to_existing_atom(value)}
|
||||
|
||||
def match(value) do
|
||||
value =
|
||||
value
|
||||
|> to_string()
|
||||
|> String.downcase()
|
||||
|
||||
match =
|
||||
Enum.find_value(@values, fn valid_value ->
|
||||
sanitized_valid_value =
|
||||
valid_value
|
||||
|> to_string()
|
||||
|> String.downcase()
|
||||
|
||||
if sanitized_valid_value == value do
|
||||
valid_value
|
||||
end
|
||||
end)
|
||||
|
||||
if match do
|
||||
{:ok, match}
|
||||
else
|
||||
:error
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
|
||||
defoverridable storage_type: 0
|
||||
end
|
||||
end
|
||||
end
|
74
test/type/enum_test.exs
Normal file
74
test/type/enum_test.exs
Normal file
|
@ -0,0 +1,74 @@
|
|||
defmodule Ash.Test.Type.EnumTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
require Ash.Query
|
||||
|
||||
defmodule Status do
|
||||
use Ash.Type.Enum, values: [:open, :Closed, :NeverHappened, :Always_Was]
|
||||
end
|
||||
|
||||
defmodule Post do
|
||||
@moduledoc false
|
||||
use Ash.Resource, data_layer: Ash.DataLayer.Ets
|
||||
|
||||
ets do
|
||||
private?(true)
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :status, Status
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Api do
|
||||
@moduledoc false
|
||||
use Ash.Api
|
||||
|
||||
resources do
|
||||
resource(Post)
|
||||
end
|
||||
end
|
||||
|
||||
import Ash.Changeset
|
||||
|
||||
test "it handles exact matches" do
|
||||
Post
|
||||
|> new(%{status: :open})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "it handles string matches" do
|
||||
Post
|
||||
|> new(%{status: "open"})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "it handles mixed case string matches" do
|
||||
Post
|
||||
|> new(%{status: "OpEn"})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "it handles mixed case string matches against mixed case atoms" do
|
||||
Post
|
||||
|> new(%{status: "nEveRHAppened"})
|
||||
|> Api.create!()
|
||||
end
|
||||
|
||||
test "it fails on mismatches" do
|
||||
assert_raise Ash.Error.Invalid, fn ->
|
||||
Post
|
||||
|> new(%{status: "what"})
|
||||
|> Api.create!()
|
||||
end
|
||||
end
|
||||
|
||||
test "the values are returned in the introspection function" do
|
||||
assert Status.values() == [:open, :Closed, :NeverHappened, :Always_Was]
|
||||
assert Status.match("OPEN") == {:ok, :open}
|
||||
assert Status.match?(:always_was)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue