├── .formatter.exs ├── .gitignore ├── README.md ├── config └── config.exs ├── lib └── enum_type.ex ├── license.md ├── mix.exs ├── mix.lock └── test ├── changeset_test.exs ├── enum_test.exs ├── new_syntax_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [ 5 | defenum: :*, 6 | value: :* 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | enum_type-*.tar 24 | 25 | .elixir_ls 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EnumType 2 | 3 | Generates Enumerated type modules that can be used as values and matched in code. Creates proper types so Dialyzer will be able to check bad calls. 4 | 5 | ## Why? 6 | 7 | Something we have often wanted in Elixir was an enumerated type. The benefits we wanted are: 8 | 9 | - compile time type checking 10 | - easy pattern matching 11 | - not Ecto specific, but has Ecto support 12 | 13 | ## Installation 14 | 15 | The package can be installed 16 | by adding `enum_type` to your list of dependencies in `mix.exs`: 17 | 18 | ```elixir 19 | def deps do 20 | [ 21 | {:enum_type, "~> 1.1.0"} 22 | ] 23 | end 24 | ``` 25 | 26 | ## Changelog 27 | 28 | - `v1.1.3` Replace `use Ecto.Type` with default implementation of `equal?/2` 29 | and `embed_as/1`. 30 | - `v1.1.2` Replace `@behaviour Ecto.Type` with `use Ecto.Type` 31 | - `v1.1.1` Fix warning for Elixir 1.10 32 | - `v1.1.0` Adds types to the enum. Dialyzer might start complaining. 33 | 34 | ## Creating and using an Enum 35 | 36 | An enum type is created as its own module with each value of an an enum being a child module. The enum reference can then be used 37 | as you would use any module name in Elixir. Since actual modules are created, this also means the module names are valid references 38 | that can be called. Enum types can be defined anywhere `defmodule` can be used. 39 | 40 | ```elixir 41 | defmodule MyApp do 42 | use EnumType 43 | 44 | defenum Color do 45 | value Red, "red" 46 | value Blue, "blue" 47 | value Green, "green" 48 | 49 | default Blue 50 | end 51 | 52 | @spec do_something(color :: Color.t) :: String.t 53 | def do_something(Color.Red), do: "got red" 54 | def do_something(Color.Blue), do: "got blue" 55 | def do_something(Color.Green), do: "got green" 56 | end 57 | 58 | MyApp.Color.Blue == MyApp.Color.default 59 | "green" == MyApp.Color.Green.value 60 | "got red" == MyApp.do_something(MyApp.Color.Red) 61 | ``` 62 | 63 | ## Enum Type Functions 64 | 65 | * `values` - List of all enum values in the order they are defined. `["red", "blue"]` 66 | * `enums` - List of all enum value modules that are defined in the order defined. `[MyApp.Color.Red, MyApp.Color.Blue]` 67 | * `options` - List of tuples with the module name and the value. `[{MyApp.Color.Red, "red}, {MyApp.Color.Blue, "blue"}]` 68 | * `from` - Converts a value to an option module name. `MyApp.Color.Red == MyApp.Color.from("red")` 69 | * `value` - Converts a option module into the value. `"red" == MyApp.Color.value(MyApp.Color.Red)` 70 | 71 | ## Enum Option Functions 72 | 73 | * `value` - The value of the enum option. `MyApp.Color.Red.value` 74 | 75 | ## Custom Functions 76 | 77 | Since both the enum type and options are both modules, any custom code that can be added to a module can also be added to these code blocks. 78 | 79 | ```elixir 80 | import EnumType 81 | 82 | defenum Color do 83 | value Red, "red" do 84 | def statement, do: "I'm red" 85 | end 86 | 87 | value Blue, "blue" do 88 | def statement, do: "I'm blue" 89 | end 90 | 91 | value Green, "green" do 92 | def statement, do: "I'm green" 93 | end 94 | 95 | default Blue 96 | 97 | @spec do_something(color :: Color.t) 98 | def do_something(Color.Red), do: "got red" 99 | def do_something(Color.Blue), do: "got blue" 100 | def do_something(Color.Green), do: "got green" 101 | 102 | def statement(color), do: "I'm #{color.value}" 103 | end 104 | 105 | "got blue" == Color.do_something(Color.Blue) 106 | "I'm green" == Color.Green.statement 107 | "I'm red" == Color.statement(Color.Red) 108 | ``` 109 | 110 | ## Ecto Type Support 111 | 112 | If `Ecto` is included in your project, additional helpers functions will be compiled in that implement the `Ecto.Type` behaviour callbacks. 113 | When using Ecto, a type must be specified for the enum values that is supported by Ecto. All values provided by the Enum Type must be the same 114 | Ecto basic type defined. By default, the type is `:string`. 115 | 116 | ```elixir 117 | defmodule Subscriber do 118 | use Ecto.Schema 119 | use EnumType 120 | 121 | import Ecto.Changeset 122 | 123 | # For database field defined as a string. 124 | defenum Level do 125 | value Basic, "basic" 126 | value Premium, "premium" 127 | 128 | default Basic 129 | end 130 | 131 | # For database field defined as an integer. 132 | defenum AgeGroup, :integer do 133 | value Minor, 0 134 | value Adult, 1 135 | value NotSpecified, 2 136 | 137 | default NotSpecified 138 | end 139 | 140 | schema "subscribers" do 141 | field :name, :string 142 | field :level, Level, default: Level.default 143 | field :age_group, AgeGroup, default: AgeGroup.default 144 | end 145 | 146 | changeset(schema, params) do 147 | schema 148 | |> cast(params, [:name, :level, :age_group]) 149 | |> Level.validate(:level, message: "Invalid subscriber type") 150 | |> AgeGroup.validate(:age_group) 151 | end 152 | end 153 | ``` 154 | 155 | When working with an enum type in an Ecto schema, always use the module name of the value you wish to use. The name can 156 | be included in any query or changeset params. 157 | 158 | ## Using with Absinthe 159 | 160 | Absinthe provides a means to define enums and map to other values. When using Absinthe with an Ecto schema that also uses `EnumType`, 161 | you will need to map to the enum option module name and not the underlying value that will be stored in the database. 162 | 163 | ```elixir 164 | enum :subscriber_level do 165 | value :basic, as: Subscriber.Level.Basic 166 | value :premium, as: Subscriber.Level.Premium 167 | end 168 | 169 | object :subscriber do 170 | field :level, :subscriber_level 171 | end 172 | ``` 173 | 174 | Absinthe will produce an upper case value based upon its own enum through the graphql interface. "BASIC" or "PREMIUM". 175 | 176 | Outbound or inbound will be mapped correctly to and from the EnumType module name value. 177 | 178 | ## Dialyzer 179 | 180 | Enum types and values are modules under the hood. Since module names are just atoms in Elixir, we don't get compile-time type checks. However, Dialyzer will be able to spot type errors. For example, code like the following: 181 | 182 | ```elixir 183 | defenum MyEnum do 184 | value One, "one" 185 | value Two, "two" 186 | value Three, "three" 187 | end 188 | 189 | @spec foo(thing :: MyEnum.t()) :: atom() 190 | def foo(MyEnum.One), do: :one_here 191 | def foo(MyEnum.Two), do: :two_here 192 | def foo(MyEnum.Three), do: :three_here 193 | 194 | def bad_func() do 195 | foo(MyEnum.NotASubtype) 196 | end 197 | ``` 198 | 199 | Dialyzer will complain that `bad_func()` doesn't return because `foo(MyEnum.NotASubtype)` breaks the contract. 200 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :enum_type, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:enum_type, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/enum_type.ex: -------------------------------------------------------------------------------- 1 | defmodule EnumType do 2 | @moduledoc """ 3 | Generates Enumerated type modules that can be used as values and matched in 4 | code. Creates proper types so Dialyzer will be able to check bad calls. 5 | """ 6 | 7 | defmacro __using__(_opts) do 8 | quote do 9 | import EnumType 10 | end 11 | end 12 | 13 | defmacro defenum(name, do: block) do 14 | quote generated: true, location: :keep do 15 | defenum(unquote(name), :string, do: unquote(block)) 16 | end 17 | end 18 | 19 | # Makes an alias: `build_module(MyModule, [:One])` -> `MyModule.One` 20 | defp build_module({:__aliases__, meta, prefix}, submodules), 21 | do: {:__aliases__, meta, prefix ++ submodules} 22 | 23 | defp build_module(prefix, submodules) when is_atom(prefix), 24 | do: {:__aliases__, [], [prefix | submodules]} 25 | 26 | # Given a prefix like `:MyModule` and some subtypes like `[:One, 27 | # :Two]`, constructs a pipe list like `MyModule.One | MyModule.Two`. 28 | # Returns AST. 29 | defp build_type_pipe(prefix, [subtype]) do 30 | build_module(prefix, [subtype]) 31 | end 32 | 33 | defp build_type_pipe(prefix, [subtype | other_types]) do 34 | quote generated: true, location: :keep do 35 | unquote(build_module(prefix, [subtype])) | unquote(build_type_pipe(prefix, other_types)) 36 | end 37 | end 38 | 39 | defmacro defenum(name, ecto_type, do: block) do 40 | {:__block__, _, block_body} = block 41 | 42 | values = 43 | Enum.reduce(block_body, [], fn 44 | {:value, _, [{_, _, [sym]} | _]}, acc -> [sym | acc] 45 | _, acc -> acc 46 | end) 47 | 48 | type_pipe = build_type_pipe(name, values) 49 | 50 | # type_pipe_string = Macro.to_string(type_pipe, __ENV__) 51 | 52 | syn = 53 | quote generated: true, location: :keep do 54 | defmodule unquote(name) do 55 | @type t :: unquote(type_pipe) 56 | 57 | Module.register_attribute(__MODULE__, :possible_options, accumulate: true) 58 | 59 | with {:module, _module} <- Code.ensure_compiled(Ecto.Type) do 60 | @behaviour Ecto.Type 61 | def embed_as(_), do: :self 62 | def equal?(term1, term2), do: term1 == term2 63 | 64 | def type, do: unquote(ecto_type) 65 | end 66 | 67 | def default, do: nil 68 | 69 | defoverridable default: 0 70 | 71 | unquote(block) 72 | 73 | with {:module, _module} <- Code.ensure_compiled(Ecto.Type) do 74 | # Default fallback ecto conversion options. 75 | def cast(_), do: :error 76 | def load(_), do: :error 77 | def value(nil), do: nil 78 | def value(_), do: :error 79 | def dump(_), do: :error 80 | 81 | def validate(changeset, field, opts \\ []) do 82 | Ecto.Changeset.validate_inclusion(changeset, field, enums(), opts) 83 | end 84 | end 85 | 86 | def enums, do: Enum.reverse(Enum.map(@possible_options, fn {key, _value} -> key end)) 87 | def values, do: Enum.reverse(Enum.map(@possible_options, fn {_key, value} -> value end)) 88 | def options, do: Enum.reverse(@possible_options) 89 | end 90 | end 91 | 92 | syn 93 | end 94 | 95 | defmacro default(option) do 96 | quote generated: true, location: :keep do 97 | def default, do: __MODULE__.unquote(option) 98 | end 99 | end 100 | 101 | defmacro value(option, value, [do: block] \\ [do: nil]) do 102 | quote generated: true, location: :keep do 103 | @possible_options {__MODULE__.unquote(option), unquote(value)} 104 | 105 | defmodule unquote(option) do 106 | @type t :: __MODULE__ 107 | 108 | def value, do: unquote(value) 109 | def upcase_value, do: String.upcase(value() |> to_string()) 110 | def downcase_value, do: String.downcase(value() |> to_string()) 111 | unquote(block) 112 | end 113 | 114 | def from(unquote(value)), do: unquote(option) 115 | 116 | def value(unquote(option)), do: unquote(option).value 117 | 118 | with {:module, _module} <- Code.ensure_compiled(Ecto.Type) do 119 | # Support querying by both the Enum module and the specific value. 120 | # Error will occur if an invalid value is attempted to be used. 121 | def cast(unquote(option)), do: {:ok, unquote(option)} 122 | def cast(unquote(value)), do: {:ok, unquote(option)} 123 | 124 | def load(unquote(value)), do: {:ok, unquote(option)} 125 | 126 | # Allow both querying by Module and setting a value to the Module when updating or inserting. 127 | def dump(unquote(option)), do: {:ok, unquote(option).value} 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # Apache 2 License 2 | 3 | Copyright (c) 2016 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EnumType.MixProject do 2 | use Mix.Project 3 | 4 | @version "1.1.3" 5 | 6 | def project do 7 | [ 8 | app: :enum_type, 9 | version: @version, 10 | elixir: "~> 1.5", 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | description: 14 | "An Elixir friendly Enum module generator that can be used by itself or with Ecto.", 15 | name: "EnumType", 16 | package: %{ 17 | licenses: ["Apache 2.0"], 18 | maintainers: ["Joseph Lindley"], 19 | links: %{"GitHub" => "https://github.com/onboardingsystems/enum_type"}, 20 | files: ~w(mix.exs README.md lib) 21 | }, 22 | docs: [ 23 | source_ref: "v#{@version}", 24 | main: "readme", 25 | canonical: "http://hexdocs.pm/enum_type", 26 | source_url: "https://github.com/onboardingsystems/enum_type", 27 | extras: ["README.md"] 28 | ] 29 | ] 30 | end 31 | 32 | # Run "mix help compile.app" to learn about applications. 33 | def application do 34 | [ 35 | extra_applications: [:logger] 36 | ] 37 | end 38 | 39 | # Run "mix help deps" to learn about dependencies. 40 | defp deps do 41 | [ 42 | {:ex_doc, "~> 0.0", only: [:docs, :dev]}, 43 | {:ecto, "~> 3.0", only: [:test]} 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm", "bbd124e240e3ff40f407d50fced3736049e72a73d547f69201484d3a624ab569"}, 3 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, 4 | "ecto": {:hex, :ecto, "3.0.5", "bf9329b56f781a67fdb19e92e6d9ed79c5c8b31d41653b79dafb7ceddfbe87e0", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "cdfd200bb6ae9e9b66d6231bec6a82137506df0bb71e752692c2858ed7f20408"}, 5 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "dc87f778d8260da0189a622f62790f6202af72f2f3dee6e78d91a18dd2fcd137"}, 6 | "makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "259748a45dfcf5f49765a7c29c9594791c82de23e22d7a3e6e59533fe8e8935b"}, 7 | "makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "393d17c5a648e3b30522b2a4743bd1dc3533e1227c8c2823ebe8c3a8e5be5913"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm", "4ababf5c44164f161872704e1cfbecab3935fdebec66c72905abaad0e6e5cef6"}, 9 | } 10 | -------------------------------------------------------------------------------- /test/changeset_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoTest do 2 | use ExUnit.Case 3 | 4 | defmodule EctoSchema do 5 | use Ecto.Schema 6 | use EnumType 7 | import Ecto.Changeset 8 | 9 | defenum Sample do 10 | value Red, "red" 11 | value Blue, "blue" 12 | value Green, "green" 13 | 14 | default Blue 15 | end 16 | 17 | embedded_schema do 18 | field :color, Sample, default: Sample.default() 19 | end 20 | 21 | def changeset(struct, params) do 22 | struct 23 | |> cast(params, [:color]) 24 | |> Sample.validate(:color) 25 | end 26 | end 27 | 28 | 29 | test "ecto type type/0" do 30 | assert EctoSchema.Sample.type == :string 31 | end 32 | 33 | describe "ecto type cast" do 34 | # called with using changeset cast as well as when running queries. So we 35 | # should be able to accept both the extenal string representation of the 36 | # enum value as well as the module name representation. 37 | test "with module value" do 38 | assert EctoSchema.Sample.cast(EctoSchema.Sample.Red) == {:ok, EctoSchema.Sample.Red} 39 | end 40 | 41 | test "with string value" do 42 | assert EctoSchema.Sample.cast(EctoSchema.Sample.Red.value) == {:ok, EctoSchema.Sample.Red} 43 | end 44 | 45 | test "with invalid module value" do 46 | assert EctoSchema.Sample.cast(EctoSchema.Sample.Puke) == :error 47 | end 48 | 49 | test "with invalid string value" do 50 | assert EctoSchema.Sample.cast("puke") == :error 51 | end 52 | end 53 | 54 | test "ecto type load" do 55 | # when loading from the DB, we are guaranteed to have a string version of the atom 56 | assert EctoSchema.Sample.load(EctoSchema.Sample.Red.value) == {:ok, EctoSchema.Sample.Red} 57 | end 58 | 59 | describe "ecto type dump" do 60 | # when dumping data to the DB we expect a module name or atom. But we 61 | # should still guard against invalid values since any value could be 62 | # inserted into the schema struct outside of the changeset and cast 63 | # functions at runtime. 64 | test "with valid value" do 65 | assert EctoSchema.Sample.dump(EctoSchema.Sample.Red) == {:ok, EctoSchema.Sample.Red.value} 66 | end 67 | 68 | test "with invalid value" do 69 | assert EctoSchema.Sample.dump(EctoSchema.Sample.Puke) == :error 70 | end 71 | end 72 | 73 | 74 | describe "changesets" do 75 | test "with module value" do 76 | changeset = EctoSchema.changeset(%EctoSchema{}, %{color: EctoSchema.Sample.Red}) 77 | assert changeset.valid? 78 | assert changeset.changes.color == EctoSchema.Sample.Red 79 | end 80 | 81 | test "with string value" do 82 | changeset = EctoSchema.changeset(%EctoSchema{}, %{color: EctoSchema.Sample.Red.value()}) 83 | assert changeset.valid? 84 | assert changeset.changes.color == EctoSchema.Sample.Red 85 | end 86 | 87 | test "with invalid module value" do 88 | changeset = EctoSchema.changeset(%EctoSchema{}, %{color: EctoSchema.Sample.Puke}) 89 | refute changeset.valid? 90 | assert changeset.errors == [color: {"is invalid", [type: EctoTest.EctoSchema.Sample, validation: :cast]}] 91 | end 92 | 93 | test "with invalid string value" do 94 | changeset = EctoSchema.changeset(%EctoSchema{}, %{color: "puke"}) 95 | refute changeset.valid? 96 | assert changeset.errors == [color: {"is invalid", [type: EctoTest.EctoSchema.Sample, validation: :cast]}] 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/enum_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EnumTypeTest do 2 | use ExUnit.Case 3 | 4 | use EnumType 5 | 6 | defenum Sample do 7 | value Red, "red" 8 | value Blue, "blue" 9 | value Green, "green" 10 | 11 | default(Blue) 12 | end 13 | 14 | defenum IntegerSample do 15 | value Yes, 1 16 | value No, 0 17 | value Maybe, 2 18 | 19 | default(Maybe) 20 | end 21 | 22 | defenum BooleanSample do 23 | value Yes, true 24 | value No, false 25 | 26 | default(No) 27 | end 28 | 29 | defenum CustomSample do 30 | value One, "Test 1" 31 | 32 | value Two, "Test 2" do 33 | def custom, do: "Custom #{value()}" 34 | end 35 | 36 | default(One) 37 | 38 | @spec hello(CustomSample.t()) :: String.t() 39 | def hello(enum), do: "Hello #{enum.value}" 40 | end 41 | 42 | describe "enum values" do 43 | test "value" do 44 | assert "red" == Sample.Red.value() 45 | end 46 | 47 | test "default value" do 48 | assert "blue" == Sample.default().value 49 | end 50 | 51 | test "integer value" do 52 | assert 1 == IntegerSample.Yes.value() 53 | end 54 | 55 | test "integer default value" do 56 | assert 2 == IntegerSample.default().value 57 | end 58 | 59 | test "boolean value" do 60 | assert true == BooleanSample.Yes.value() 61 | end 62 | 63 | test "boolean default value" do 64 | assert false == BooleanSample.default().value 65 | end 66 | end 67 | 68 | describe "enum options" do 69 | test "values are in order defined in enum" do 70 | assert ["red", "blue", "green"] == Sample.values() 71 | end 72 | 73 | test "enums are in order defined in enum" do 74 | assert [Sample.Red, Sample.Blue, Sample.Green] == Sample.enums() 75 | end 76 | 77 | test "options" do 78 | assert [{Sample.Red, "red"}, {Sample.Blue, "blue"}, {Sample.Green, "green"}] 79 | end 80 | end 81 | 82 | describe "custom functions" do 83 | test "type function" do 84 | assert "Hello Test 1" == CustomSample.hello(CustomSample.One) 85 | end 86 | 87 | test "value function" do 88 | assert "Custom Test 2" == CustomSample.Two.custom() 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/new_syntax_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NewSyntaxTest do 2 | use ExUnit.Case 3 | 4 | use EnumType 5 | 6 | defenum(MyType) do 7 | value One, "one" 8 | value Two, "two" 9 | end 10 | 11 | @spec foo(thing :: MyEnum.t()) :: atom() 12 | def foo(MyEnum.One), do: :one_here 13 | def foo(MyEnum.Two), do: :two_here 14 | def foo(MyEnum.Three), do: :three_here 15 | 16 | def bar_two() do 17 | foo(MyEnum.Two) 18 | end 19 | 20 | def bar_bad() do 21 | foo(MyEnum.NotASubtype) 22 | end 23 | 24 | test "just tinkering" do 25 | assert foo(MyEnum.One) == :one_here 26 | assert foo(MyEnum.Two) == :two_here 27 | assert foo(MyEnum.Three) == :three_here 28 | assert_raise FunctionClauseError, fn -> foo(MyEnum.Boom) end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------