Repository: dashbitco/nimble_options (elixir)

Issue:
Member type
A pretty common possible option value is a member of a list. An example that comes to mind is Broadway's `:batch_mode` in `Broadway.test_messages/3`, which is `:bulk` or `:flush`.

Do we want to support a "member" type?

```elixir
schema = [
  batch_mode: [type: {:member, [:bulk, :flush]}, default: :flush]
]
```

Do you think it's too soon, too specific, or a good addition?

Pull request description:
Add the {:one_of, choices} type
Closes #15.

Patch:
diff --git a/lib/nimble_options.ex b/lib/nimble_options.ex
index a7df6de..8db793d 100644
--- a/lib/nimble_options.ex
+++ b/lib/nimble_options.ex
@@ -93,6 +93,8 @@ defmodule NimbleOptions do
 
     * `{:fun, arity}` - Any function with the specified arity.
 
+    * `{:one_of, choices}` - A value that is a member of one of the `choices`.
+
     * `{:custom, mod, fun, args}` - A custom type. The related value must be validated
       by `mod.fun(values, ...args)`. The function should return `{:ok, value}` or
       `{:error, message}`.
@@ -380,6 +382,15 @@ defmodule NimbleOptions do
     apply(mod, fun, [value | args])
   end
 
+  defp validate_type({:one_of, choices}, key, value) do
+    if value in choices do
+      :ok
+    else
+      {:error,
+       "expected #{inspect(key)} to be one of #{inspect(choices)}, got: #{inspect(value)}"}
+    end
+  end
+
   defp validate_type(nil, key, value) do
     validate_type(:any, key, value)
   end
@@ -406,7 +417,10 @@ defmodule NimbleOptions do
   end
 
   defp available_types() do
-    types = Enum.map(@basic_types, &inspect/1) ++ ["{:fun, arity}", "{:custom, mod, fun, args}"]
+    types =
+      Enum.map(@basic_types, &inspect/1) ++
+        ["{:fun, arity}", "{:one_of, choices}", "{:custom, mod, fun, args}"]
+
     Enum.join(types, ", ")
   end
 
@@ -419,6 +433,10 @@ defmodule NimbleOptions do
     {:ok, value}
   end
 
+  def type({:one_of, choices} = value) when is_list(choices) do
+    {:ok, value}
+  end
+
   def type({:custom, mod, fun, args} = value)
       when is_atom(mod) and is_atom(fun) and is_list(args) do
     {:ok, value}


Test patch:
diff --git a/test/nimble_options_test.exs b/test/nimble_options_test.exs
index 41b4785..3d10a54 100644
--- a/test/nimble_options_test.exs
+++ b/test/nimble_options_test.exs
@@ -30,7 +30,7 @@ defmodule NimbleOptionsTest do
 
       Available types: :any, :keyword_list, :non_empty_keyword_list, :atom, \
       :non_neg_integer, :pos_integer, :mfa, :mod_arg, :string, :boolean, :timeout, \
-      {:fun, arity}, {:custom, mod, fun, args}\
+      {:fun, arity}, {:one_of, choices}, {:custom, mod, fun, args}\
       """
 
       assert_raise ArgumentError, message, fn ->
@@ -343,6 +343,25 @@ defmodule NimbleOptionsTest do
              }
     end
 
+    test "valid {:one_of, choices}" do
+      schema = [batch_mode: [type: {:one_of, [:flush, :bulk]}]]
+
+      opts = [batch_mode: :flush]
+      assert NimbleOptions.validate(opts, schema) == {:ok, opts}
+
+      opts = [batch_mode: :bulk]
+      assert NimbleOptions.validate(opts, schema) == {:ok, opts}
+    end
+
+    test "invalid {:one_of, choices}" do
+      schema = [batch_mode: [type: {:one_of, [:flush, :bulk]}]]
+
+      opts = [batch_mode: :invalid]
+
+      assert NimbleOptions.validate(opts, schema) ==
+               {:error, "expected :batch_mode to be one of [:flush, :bulk], got: :invalid"}
+    end
+
     test "{:custom, mod, fun, args} with empty args" do
       schema = [buffer_keep: [type: {:custom, __MODULE__, :buffer_keep, []}]]