I’m really not sure how Dialyzer has come up with the success typing specs reported below, but it feels completely wrong. (If someone wants to try this, it requires inflex. We will be open sourcing this soon as MIT, so I have no issues posting it here.)
lib/kinetic_lib/remap.ex:53:call
The function call will not succeed.
KineticLib.Remap.atomize_keys(_struct_or_map :: any(), [{atom(), _}, ...])
will never return since it differs in arguments with
positions 2nd from the success typing arguments:
(map(), [])
The specs clearly indicate keyword
, and I am able to call the function with the parameters as I expect them, so either there’s a bug with dialyzer (possible, but unlikely), or I’ve specified the specs incorrectly (much more likely), or there’s a bug in other parts of my code (less likely on the surface than incorrect specs, but I haven’t written unit tests for this yet, so…still likely).
The code is below, hidden.
Help?
Thanks.
Remap code
defmodule Remap do
@moduledoc """
Remap allows maps or structs to be rekeyed.
"""
@type key_transforms :: :camel | :pascal | :underscore | nil
@type atomize_permit :: :safe | list(String.t()) | :unsafe | nil
@type stringify_permit :: :all | list(atom) | nil | false
@type atomize_options :: [
as: key_transforms,
permit: atomize_permit,
keep: boolean,
nested: boolean
]
@type stringify_options :: [
as: key_transforms,
permit: stringify_permit,
keep: boolean,
nested: boolean
]
@typep recurse_function :: (struct | map, keyword -> struct | map)
@doc """
Convert the keys of the struct or map to atoms. By default, atomize_keys/2
will perform an unsafe conversion. The following options may be used
to control the conversion:
- `as`: `:camel` (convert `camel_value` to `CamelValue`), `:pascal` (convert
`pascal_value` to `pascalValue`), or `:underscore` (convert `CamelValue`
to `camel_value`). If not specified, there is no conversion.
- `permit`: `:safe` (only convert strings to atoms that are already
existing) or a list of string values that are permitted.
- `keep`: a boolean value indicating whether non-atom keys should be kept.
If not specified, atom values are not kept (same as `keep: false`).
- `nested`: a boolean value indicating whether nested maps or structures, and
nested maps or structures in lists should be considered. If not specified,
nested maps and structures are processed as well (same as `nested: true`).
"""
@spec safe_atomize_keys(struct | map, keyword) :: struct | map
def safe_atomize_keys(struct_or_map, options \\ []) do
permit =
options
|> Keyword.get(:permit)
|> case do
[] -> []
[_ | _] = list -> list
_ -> :safe
end
atomize_keys(struct_or_map, Keyword.put(options, :permit, permit))
end
@doc """
Convert the keys of the struct or map to atoms. By default, atomize_keys/2
will perform an unsafe conversion. The following options may be used
to control the conversion:
- `as`: `:camel` (convert `camel_value` to `CamelValue`), `:pascal` (convert
`pascal_value` to `pascalValue`), or `:underscore` (convert `CamelValue`
to `camel_value`). If not specified, there is no conversion.
- `permit`: `:safe` (only convert strings to atoms that are already
existing), a list of string values that are permitted. If not specified,
unsafe atom conversion is performed.
- `keep`: a boolean value indicating whether non-atom keys should be kept.
If not specified, atom values are not kept (same as `keep: false`).
- `nested`: a boolean value indicating whether nested maps or structures, and
nested maps or structures in lists should be considered. If not specified,
nested maps and structures are processed as well (same as `nested: true`).
"""
@spec atomize_keys(struct | map, keyword) :: struct | map
def atomize_keys(struct_or_map, options \\ [])
def atomize_keys(%mod{} = struct, options) do
map =
struct
|> Map.from_struct()
|> do_atomize_keys(options)
struct(mod, map)
end
def atomize_keys(map, options) when is_map(map) do
map
|> do_atomize_keys(options)
|> Map.new()
end
@spec stringify_keys(struct | map) :: map
def stringify_keys(struct_or_map), do: stringify_keys(struct_or_map, [])
@spec stringify_keys(struct | map, keyword) :: map
def stringify_keys(%_{} = struct, options) do
struct
|> Map.from_struct()
|> stringify_keys(options)
end
def stringify_keys(map, options) when is_map(map) do
map
|> do_stringify_keys(options)
|> Map.new()
end
@spec do_atomize_keys(map, keyword) :: Enumerable.t()
defp do_atomize_keys(map, options) do
map
|> transform_keys_as(Keyword.get(options, :as))
|> transform_keys_to_atom(Keyword.get(options, :permit))
|> purge_non_atom_keys(Keyword.get(options, :keep, false))
|> recurse(&atomize_keys/2, options)
end
@spec do_stringify_keys(map, keyword) :: Enumerable.t()
defp do_stringify_keys(map, options) do
map
|> transform_keys_as(Keyword.get(options, :as))
|> transform_keys_to_string(Keyword.get(options, :permit))
|> purge_non_string_keys(Keyword.get(options, :keep, false))
|> recurse(&stringify_keys/2, options)
end
@spec transform_keys_as(Enumerable.t(), key_transforms) :: Enumerable.t()
defp transform_keys_as(enumerable, mode) when mode in ~w(camel pascal underscore)a do
Stream.map(enumerable, &transform_key_as(&1, mode))
end
defp transform_keys_as(enumerable, _mode), do: enumerable
@spec transform_key_as({any, any}, :camel | :pascal | :underscore) :: {any, any}
defp transform_key_as({k, _v} = item, _) when not is_binary(k) and not is_atom(k), do: item
defp transform_key_as({k, v}, :camel), do: {Inflex.camelize(k), v}
defp transform_key_as({k, v}, :pascal), do: {Inflex.camelize(k, :lower), v}
defp transform_key_as({k, v}, :underscore), do: {Inflex.underscore(k), v}
@spec transform_keys_to_atom(Enumerable.t(), atomize_permit) :: Enumerable.t(0)
defp transform_keys_to_atom(enumerable, :safe) do
Stream.map(enumerable, &transform_key_to_atom(&1, :safe))
end
defp transform_keys_to_atom(enumerable, [_ | _] = allowed) do
Stream.map(enumerable, &transform_key_to_atom(&1, allowed))
end
defp transform_keys_to_atom(enumerable, []), do: enumerable
defp transform_keys_to_atom(enumerable, _) do
Stream.map(enumerable, &transform_key_to_atom(&1, :unsafe))
end
@spec transform_key_to_atom({any, any}, :safe | :unsafe | list(String.t())) :: {any, any}
defp transform_key_to_atom({k, v}, :safe) when is_binary(k) do
try do
{String.to_existing_atom(k), v}
rescue
_ -> {k, v}
end
end
defp transform_key_to_atom({k, v}, :unsafe) when is_binary(k) do
{String.to_atom(k), v}
end
defp transform_key_to_atom({k, v}, allowed) when is_binary(k) do
if k in allowed do
{String.to_atom(k), v}
else
{k, v}
end
end
defp transform_key_to_atom({_k, _v} = entry, _), do: entry
@spec transform_keys_to_string(Enumerable.t(), stringify_permit) :: Enumerable.t()
defp transform_keys_to_string(enumerable, []), do: enumerable
defp transform_keys_to_string(enumerable, false), do: enumerable
defp transform_keys_to_string(enumerable, [_ | _] = allowed) do
Stream.map(enumerable, &transform_key_to_string(&1, allowed))
end
defp transform_keys_to_string(enumerable, _) do
Stream.map(enumerable, &transform_key_to_string(&1, :all))
end
@spec transform_key_to_string({any, any}, :all | list(atom)) :: {any, any}
defp transform_key_to_string({k, v}, :all) when is_atom(k) do
{Atom.to_string(k), v}
end
defp transform_key_to_string({k, v}, allowed) when is_atom(k) do
if k in allowed do
{Atom.to_string(k), v}
else
{k, v}
end
end
defp transform_key_to_string({_k, _v} = entry, _), do: entry
@spec purge_non_atom_keys(Enumerable.t(), boolean) :: Enumerable.t()
defp purge_non_atom_keys(enumerable, keep?) do
if keep? do
enumerable
else
Stream.filter(enumerable, fn {k, _} -> is_atom(k) end)
end
end
@spec purge_non_string_keys(Enumerable.t(), boolean) :: Enumerable.t()
defp purge_non_string_keys(enumerable, keep?) do
if keep? do
enumerable
else
Stream.filter(enumerable, fn {k, _} -> is_binary(k) end)
end
end
@spec recurse(Enumerable.t(), recurse_function, atomize_permit | stringify_permit) ::
Enumerable.t()
defp recurse(enumerable, function, options) do
if Keyword.get(options, :nested, true) do
Stream.map(enumerable, &do_recurse(&1, function, options))
else
enumerable
end
end
@spec do_recurse({any, any}, recurse_function, keyword) :: {any, any}
defp do_recurse({k, %_{} = v}, function, options) do
{k, function.(v, options)}
end
defp do_recurse({k, v}, function, options) when is_map(v) do
{k, function.(v, options)}
end
defp do_recurse({k, v}, function, options) when is_list(v) do
if Keyword.keyword?(v) do
{k, function.(Map.new(v), options)}
else
{k, Enum.map(v, &process_list_item(&1, function, options))}
end
end
defp do_recurse({_k, _v} = entry, _function, _options), do: entry
@spec process_list_item(any, recurse_function, keyword) :: any
defp process_list_item(item, function, options) when is_map(item) do
function.(item, options)
end
defp process_list_item(items, function, options) when is_list(items) do
if Keyword.keyword?(items) do
function.(Map.new(items), options)
else
Enum.map(items, &process_list_item(&1, function, options))
end
end
defp process_list_item(item, _function, _options), do: item
end