Repository: dashbitco/nimble_options (elixir)

Issue:
option_schema doesn't support :map options
Given this module:

```elixir
defmodule Test do
  @options_schema NimbleOptions.new!(
    map: [type: :map]
  )
  @type option() :: unquote(NimbleOptions.option_typespec(@options_schema))
end
```

Loading this up in Visual Studio Code with ElixirLS produces a Dialyzer error

```none
(CaseClauseError) no case clause matching: :map

Stacktrace:
  │ (nimble_options 0.5.0) lib/nimble_options/docs.ex:163: NimbleOptions.Docs.type_to_spec/1
  │ (nimble_options 0.5.0) lib/nimble_options/docs.ex:156: anonymous fn/1 in NimbleOptions.Docs.schema_to_spec/1
  │ (elixir 1.14.1) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
  │ (nimble_options 0.5.0) lib/nimble_options/docs.ex:155: NimbleOptions.Docs.schema_to_spec/1
  │ test.ex:5: (module)
```

It looks like `NimbleOptions.Docs.type_to_spec/1` [doesn't support](https://github.com/dashbitco/nimble_options/blob/v0.5.0/lib/nimble_options/docs.ex#L162-L187) `:map` or `{:map, key_type, value_type}`.

Pull request description:
Support `:map` types in `option_typespec/1`.
If an option has `type: :map`, generate a corresponding typespec `map()`.  If it has `type: {:map, k, v}`, then generate a typespec `%{optional(k) => v}`.

Fixes #95.

Patch:
diff --git a/lib/nimble_options/docs.ex b/lib/nimble_options/docs.ex
index d1f4070..3c8e04a 100644
--- a/lib/nimble_options/docs.ex
+++ b/lib/nimble_options/docs.ex
@@ -170,28 +170,79 @@ defmodule NimbleOptions.Docs do
 
   defp type_to_spec(type) do
     case type do
-      :any -> quote(do: term())
-      :integer -> quote(do: integer())
-      :non_neg_integer -> quote(do: non_neg_integer())
-      :pos_integer -> quote(do: pos_integer())
-      :atom -> quote(do: atom())
-      :float -> quote(do: float())
-      :boolean -> quote(do: boolean())
-      :pid -> quote(do: pid())
-      :reference -> quote(do: reference())
-      :timeout -> quote(do: timeout())
-      :string -> quote(do: binary())
-      :mfa -> quote(do: {module(), atom(), [term()]})
-      :mod_arg -> quote(do: {module(), [term()]})
-      :keyword_list -> quote(do: keyword())
-      {:keyword_list, _keys} -> quote(do: keyword())
-      :non_empty_keyword_list -> quote(do: keyword())
-      {:non_empty_keyword_list, _keys} -> quote(do: keyword())
-      {:fun, arity} -> function_spec(arity)
-      {:in, _choices} -> quote(do: term())
-      {:custom, _mod, _fun, _args} -> quote(do: term())
-      {:list, subtype} -> quote(do: [unquote(type_to_spec(subtype))])
-      {:or, subtypes} -> subtypes |> Enum.map(&type_to_spec/1) |> unionize_quoted()
+      :any ->
+        quote(do: term())
+
+      :integer ->
+        quote(do: integer())
+
+      :non_neg_integer ->
+        quote(do: non_neg_integer())
+
+      :pos_integer ->
+        quote(do: pos_integer())
+
+      :atom ->
+        quote(do: atom())
+
+      :float ->
+        quote(do: float())
+
+      :boolean ->
+        quote(do: boolean())
+
+      :pid ->
+        quote(do: pid())
+
+      :reference ->
+        quote(do: reference())
+
+      :timeout ->
+        quote(do: timeout())
+
+      :string ->
+        quote(do: binary())
+
+      :mfa ->
+        quote(do: {module(), atom(), [term()]})
+
+      :mod_arg ->
+        quote(do: {module(), [term()]})
+
+      :keyword_list ->
+        quote(do: keyword())
+
+      {:keyword_list, _keys} ->
+        quote(do: keyword())
+
+      :non_empty_keyword_list ->
+        quote(do: keyword())
+
+      {:non_empty_keyword_list, _keys} ->
+        quote(do: keyword())
+
+      :map ->
+        quote(do: map())
+
+      {:map, key_type, value_type} ->
+        quote(
+          do: %{optional(unquote(type_to_spec(key_type))) => unquote(type_to_spec(value_type))}
+        )
+
+      {:fun, arity} ->
+        function_spec(arity)
+
+      {:in, _choices} ->
+        quote(do: term())
+
+      {:custom, _mod, _fun, _args} ->
+        quote(do: term())
+
+      {:list, subtype} ->
+        quote(do: [unquote(type_to_spec(subtype))])
+
+      {:or, subtypes} ->
+        subtypes |> Enum.map(&type_to_spec/1) |> unionize_quoted()
     end
   end
 


Test patch:
diff --git a/test/nimble_options_test.exs b/test/nimble_options_test.exs
index a7aef91..e814cef 100644
--- a/test/nimble_options_test.exs
+++ b/test/nimble_options_test.exs
@@ -1995,6 +1995,8 @@ defmodule NimbleOptionsTest do
         any: [type: :any],
         keyword_list: [type: :keyword_list],
         non_empty_keyword_list: [type: :non_empty_keyword_list],
+        map: [type: :map],
+        map_of_string_to_int: [type: {:map, :string, :integer}],
         atom: [type: :atom],
         integer: [type: :integer],
         non_neg_integer: [type: :non_neg_integer],
@@ -2023,6 +2025,8 @@ defmodule NimbleOptionsTest do
                   {:any, term()}
                   | {:keyword_list, keyword()}
                   | {:non_empty_keyword_list, keyword()}
+                  | {:map, map()}
+                  | {:map_of_string_to_int, %{optional(binary()) => integer()}}
                   | {:atom, atom()}
                   | {:integer, integer()}
                   | {:non_neg_integer, non_neg_integer()}