diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 1df7743..2191a10 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,2 +1,3 @@ [{"lib/wuunder_utils/maps.ex", :contract_supertype}, -{"lib/wuunder_utils/strings.ex", :contract_supertype},] +{"lib/wuunder_utils/strings.ex", :contract_supertype}, +{"lib/wuunder_utils/numbers.ex", :contract_supertype}] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6642354..10136d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,17 @@ name: Test and build on: + push: + branches: ["main"] pull_request: - branches: - - master + branches: ["main"] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: @@ -35,8 +43,8 @@ jobs: restore-keys: ${{ runner.os }}-mixv2- - name: Setup deps - run: | - mix deps.get + run: | + mix deps.get mix deps.unlock --check-unused - name: Compile @@ -46,14 +54,11 @@ jobs: run: mix format --check-formatted - name: Run Tests - run: mix coveralls.github --warnings-as-errors - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: mix test - name: Cleanup run: mix clean - dialyzer: name: "Run Dialyzer" runs-on: ubuntu-latest @@ -110,8 +115,8 @@ jobs: priv/plts - name: Setup deps - run: | - mix deps.get + run: | + mix deps.get mix deps.unlock --check-unused - name: Compile diff --git a/.tool-versions b/.tool-versions index b7c4c5b..9c71c0d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.17.2-otp-27 -erlang 27.0.1 +elixir 1.17.3-otp-27 +erlang 27.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..88c4e7d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +## v0.9 (2024-10-24) + +### Backwards incompatible changes + + * Renamed `WuunderUtils.Maps.delete_field` to `WuunderUtils.Maps.delete` + * Renamed `WuunderUtils.Maps.delete_field_in` to `WuunderUtils.Maps.deep_delete` + * Renamed `WuunderUtils.Maps.get_field` to `WuunderUtils.Maps.get` + * Renamed `WuunderUtils.Maps.get_field_in` to `WuunderUtils.Maps.deep_get` + * Renamed `WuunderUtils.Maps.get_fields_in` to `WuunderUtils.Maps.deep_get_values` + * Renamed `WuunderUtils.Maps.put_field` to `WuunderUtils.Maps.put` + * Renamed `WuunderUtils.Maps.put_field_in` to `WuunderUtils.Maps.deep_put` + * Renamed `WuunderUtils.Maps.has_only_atom_keys?` to `WuunderUtils.Maps.only_atom_keys?` + * Renamed `WuunderUtils.Maps.alias_fields` to `WuunderUtils.Maps.rename_keys` + * Renamed `WuunderUtils.Maps.map_all` to `WuunderUtils.Maps.map` + +### Enhancements + + * Added `strinigy_keys` helper in `WuunderUtils.Maps` + * Added `atomize_keys` helper in `WuunderUtils.Maps` diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1a6b396 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 Wuunder + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index bbc597e..9d15e2c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Contains a set of modules that makes Elixir development more fun and productive. +![Build status](https://github.com/wuunder/wuunder_utils/actions/workflows/ci.yml/badge.svg) +[![Hex.pm](https://img.shields.io/hexpm/v/wuunder_utils.svg)](https://hex.pm/packages/wuunder_utils) + ## Installation If [available in Hex](https://hex.pm/docs/publish), the package can be installed @@ -10,7 +13,7 @@ by adding `wuunder_utils` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:wuunder_utils, "~> 0.1.0"} + {:wuunder_utils, "~> 0.9.0"} ] end ``` @@ -18,4 +21,3 @@ end Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) and published on [HexDocs](https://hexdocs.pm). Once published, the docs can be found at . - diff --git a/lib/wuunder_utils/maps.ex b/lib/wuunder_utils/maps.ex index ca05145..6296a77 100644 --- a/lib/wuunder_utils/maps.ex +++ b/lib/wuunder_utils/maps.ex @@ -16,51 +16,51 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.get_field(%{value: 20}, :value) + iex> WuunderUtils.Maps.get(%{value: 20}, :value) 20 - iex> WuunderUtils.Maps.get_field(%{"value" => 20}, :value) + iex> WuunderUtils.Maps.get(%{"value" => 20}, :value) 20 - iex> WuunderUtils.Maps.get_field(%{value: 20}, "value") + iex> WuunderUtils.Maps.get(%{value: 20}, "value") 20 - iex> WuunderUtils.Maps.get_field(%{value: 20}, "non-existent") + iex> WuunderUtils.Maps.get(%{value: 20}, "non-existent") nil - iex> WuunderUtils.Maps.get_field(%{value: 20}, :weight) + iex> WuunderUtils.Maps.get(%{value: 20}, :weight) nil - iex> WuunderUtils.Maps.get_field(%{value: 20}, :weight, 350) + iex> WuunderUtils.Maps.get(%{value: 20}, :weight, 350) 350 - iex> WuunderUtils.Maps.get_field(%{value: 20}, "currency", "EUR") + iex> WuunderUtils.Maps.get(%{value: 20}, "currency", "EUR") "EUR" - iex> WuunderUtils.Maps.get_field([name: "Henk", name: "Kees", last_name: "Jansen"], "name") + iex> WuunderUtils.Maps.get([name: "Henk", name: "Kees", last_name: "Jansen"], "name") "Henk" - iex> WuunderUtils.Maps.get_field(["a", "b", "c"], 1) + iex> WuunderUtils.Maps.get(["a", "b", "c"], 1) "b" - iex> WuunderUtils.Maps.get_field(["a", "b", "c"], 3, "d") + iex> WuunderUtils.Maps.get(["a", "b", "c"], 3, "d") "d" - iex> WuunderUtils.Maps.get_field({"a", "b", "c"}, 1) + iex> WuunderUtils.Maps.get({"a", "b", "c"}, 1) "b" - iex> WuunderUtils.Maps.get_field({"a", "b", "c"}, 3, "d") + iex> WuunderUtils.Maps.get({"a", "b", "c"}, 3, "d") "d" """ - @spec get_field(any(), map_key() | non_neg_integer(), any()) :: any() - def get_field(map, key, default \\ nil) + @spec get(any(), map_key() | non_neg_integer(), any()) :: any() + def get(map, key, default \\ nil) - def get_field(list, index, default) when is_list(list) and is_number(index), + def get(list, index, default) when is_list(list) and is_number(index), do: Enum.at(list, index, default) - def get_field(list, key, default) when is_list(list) and is_binary(key) do + def get(list, key, default) when is_list(list) and is_binary(key) do atom_key = get_safe_key(key) if is_atom(atom_key) && Keyword.keyword?(list) do @@ -70,15 +70,15 @@ defmodule WuunderUtils.Maps do end end - def get_field(tuple, index, _default) + def get(tuple, index, _default) when is_tuple(tuple) and is_number(index) and index < tuple_size(tuple), do: elem(tuple, index) - def get_field(tuple, index, default) + def get(tuple, index, default) when is_tuple(tuple) and is_number(index) and index >= tuple_size(tuple), do: default - def get_field(map, key, default) + def get(map, key, default) when is_map(map) and is_valid_map_atom_key(key) do if Map.has_key?(map, key) do Map.get(map, key) @@ -87,7 +87,7 @@ defmodule WuunderUtils.Maps do end end - def get_field(map, key, default) when is_map(map) and is_valid_map_binary_key(key) do + def get(map, key, default) when is_map(map) and is_valid_map_binary_key(key) do atom_key = get_safe_key(key) if is_atom(atom_key) && Map.has_key?(map, atom_key) do @@ -121,31 +121,31 @@ defmodule WuunderUtils.Maps do ...> } ...> } ...> - ...> WuunderUtils.Maps.get_field_in(person, [:country, :code]) + ...> WuunderUtils.Maps.deep_get(person, [:country, :code]) "NL" - iex> WuunderUtils.Maps.get_field_in(person, "country.code") + iex> WuunderUtils.Maps.deep_get(person, "country.code") "NL" - iex> WuunderUtils.Maps.get_field_in(person, [:address, :company]) + iex> WuunderUtils.Maps.deep_get(person, [:address, :company]) %Company{name: "Wuunder"} - iex> WuunderUtils.Maps.get_field_in(person, [:address, :company, :name]) + iex> WuunderUtils.Maps.deep_get(person, [:address, :company, :name]) "Wuunder" - iex> WuunderUtils.Maps.get_field_in(person, [:meta, :skills]) + iex> WuunderUtils.Maps.deep_get(person, [:meta, :skills]) ["programmer", "manager", %{name: "painting", type: "hobby", grades: {"A+", "C"}}] - iex> WuunderUtils.Maps.get_field_in(person, [:meta, :skills, 1]) + iex> WuunderUtils.Maps.deep_get(person, [:meta, :skills, 1]) "manager" - iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.1") + iex> WuunderUtils.Maps.deep_get(person, "meta.skills.1") "manager" - iex> WuunderUtils.Maps.get_field_in(person, [:meta, :skills, 2, :type]) + iex> WuunderUtils.Maps.deep_get(person, [:meta, :skills, 2, :type]) "hobby" - iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.2.type") + iex> WuunderUtils.Maps.deep_get(person, "meta.skills.2.type") "hobby" - iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.2.non_existent") + iex> WuunderUtils.Maps.deep_get(person, "meta.skills.2.non_existent") nil - iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.2.non_existent", "default") + iex> WuunderUtils.Maps.deep_get(person, "meta.skills.2.non_existent", "default") "default" - iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.2.grades.0") + iex> WuunderUtils.Maps.deep_get(person, "meta.skills.2.grades.0") "A+" - iex> WuunderUtils.Maps.get_field_in(person, "meta.skills.2.grades.2", "none") + iex> WuunderUtils.Maps.deep_get(person, "meta.skills.2.grades.2", "none") "none" iex> keyword_list = [ @@ -157,45 +157,45 @@ defmodule WuunderUtils.Maps do ...> ] ...> ] ...> - iex> WuunderUtils.Maps.get_field_in(keyword_list, "name") + iex> WuunderUtils.Maps.deep_get(keyword_list, "name") "Henk" - iex> WuunderUtils.Maps.get_field_in(keyword_list, "addresses") + iex> WuunderUtils.Maps.deep_get(keyword_list, "addresses") [%{"number" => 1, "street" => "Laan"}, %{"number" => 1337, "street" => "Straat"}] - iex> WuunderUtils.Maps.get_field_in(keyword_list, "addresses.0") + iex> WuunderUtils.Maps.deep_get(keyword_list, "addresses.0") %{"number" => 1, "street" => "Laan"} - iex> WuunderUtils.Maps.get_field_in(keyword_list, "addresses.1.street") + iex> WuunderUtils.Maps.deep_get(keyword_list, "addresses.1.street") "Straat" - iex> WuunderUtils.Maps.get_field_in(keyword_list, "addresses.1.other_field", "none") + iex> WuunderUtils.Maps.deep_get(keyword_list, "addresses.1.other_field", "none") "none" - iex> WuunderUtils.Maps.get_field_in(keyword_list, "addresses.2.other_field", "none") + iex> WuunderUtils.Maps.deep_get(keyword_list, "addresses.2.other_field", "none") nil """ - @spec get_field_in(any(), list(atom()) | String.t()) :: any() - def get_field_in(value, path, default \\ nil) + @spec deep_get(any(), list(atom()) | String.t()) :: any() + def deep_get(value, path, default \\ nil) - def get_field_in(value, path, default) when is_binary(path) do + def deep_get(value, path, default) when is_binary(path) do keys = keys_from_path(path) - get_field_in(value, keys, default) + deep_get(value, keys, default) end - def get_field_in(nil, _keys, _default), do: nil + def deep_get(nil, _keys, _default), do: nil - def get_field_in(value, [], _default), do: value + def deep_get(value, [], _default), do: value - def get_field_in(value, _keys, _default) + def deep_get(value, _keys, _default) when not is_map(value) and not is_list(value) and not is_tuple(value), do: nil - def get_field_in(map_list_or_tuple, [key | rest], default) + def deep_get(map_list_or_tuple, [key | rest], default) when is_map(map_list_or_tuple) or is_list(map_list_or_tuple) or is_tuple(map_list_or_tuple) do map_list_or_tuple - |> get_field(key, default) - |> get_field_in(rest, default) + |> get(key, default) + |> deep_get(rest, default) end - def get_field_in(nil, keys, _default) when is_list(keys), do: nil + def deep_get(nil, keys, _default) when is_list(keys), do: nil @doc """ Creates a map from a given set of fields. The output will always be a string. @@ -217,7 +217,7 @@ defmodule WuunderUtils.Maps do ...> } ...> } ...> - ...> WuunderUtils.Maps.get_fields_in( + ...> WuunderUtils.Maps.deep_get_values( ...> person, ...> [ ...> [:country, :code], @@ -236,8 +236,8 @@ defmodule WuunderUtils.Maps do } """ - @spec get_fields_in(map() | struct() | list(), list()) :: map() - def get_fields_in(value, fields) do + @spec deep_get_values(map() | struct() | list(), list()) :: map() + def deep_get_values(value, fields) do initial_map = Enum.reduce(fields, %{}, fn field, initial_map -> keys = @@ -245,18 +245,18 @@ defmodule WuunderUtils.Maps do |> get_keys() |> ensure_zero_index() - Map.merge(initial_map, empty_map(keys)) + Map.merge(initial_map, new(keys)) end) Enum.reduce(fields, initial_map, fn field, final_map -> - value = get_field_in(value, field) + value = deep_get(value, field) keys = field |> get_keys() |> ensure_zero_index() - put_field_in(final_map, keys, value) + deep_put(final_map, keys, value) end) end @@ -265,40 +265,40 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.empty_map([:person, :name, :meta, 0, :hobby, :type]) + iex> WuunderUtils.Maps.new([:person, :name, :meta, 0, :hobby, :type]) %{"person" => %{"name" => %{"meta" => [%{"hobby" => %{"type" => %{}}}]}}} - iex> WuunderUtils.Maps.empty_map([:person, :name, :meta, 0, :hobbies, 0, :type]) + iex> WuunderUtils.Maps.new([:person, :name, :meta, 0, :hobbies, 0, :type]) %{"person" => %{"name" => %{"meta" => [%{"hobbies" => [%{"type" => %{}}]}]}}} """ - @spec empty_map(String.t() | list()) :: map() - def empty_map(path) when is_binary(path) do + @spec new(String.t() | list()) :: map() + def new(path) when is_binary(path) do path |> keys_from_path() - |> empty_map() + |> new() end - def empty_map(keys) when is_list(keys), do: empty_map(%{}, keys) + def new(keys) when is_list(keys), do: new(%{}, keys) - @spec empty_map(map() | list(), list()) :: map() | list() - def empty_map(list, [key | rest]) when is_list(list) do + @spec new(map() | list(), list()) :: map() | list() + def new(list, [key | rest]) when is_list(list) do if is_integer(key) do - [empty_map(rest)] + [new(rest)] else - [%{"key" => empty_map(rest)}] + [%{"key" => new(rest)}] end end - def empty_map(map, [key | rest]) when is_map(map) do + def new(map, [key | rest]) when is_map(map) do if is_integer(key) do - [empty_map(rest)] + [new(rest)] else - Map.put(map, "#{key}", empty_map(rest)) + Map.put(map, "#{key}", new(rest)) end end - def empty_map(map_or_list, []), do: map_or_list + def new(map_or_list, []), do: map_or_list @doc """ Acts as an IndifferentMap. Put a key/value regardless of the key type. If the map @@ -307,43 +307,43 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.put_field(%{value: 20}, :weight, 350) + iex> WuunderUtils.Maps.put(%{value: 20}, :weight, 350) %{value: 20, weight: 350} - iex> WuunderUtils.Maps.put_field(["a", "b", "c"], 1, "d") + iex> WuunderUtils.Maps.put(["a", "b", "c"], 1, "d") ["a", "d", "c"] - iex> WuunderUtils.Maps.put_field(["a", "b", "c"], 4, "d") + iex> WuunderUtils.Maps.put(["a", "b", "c"], 4, "d") ["a", "b", "c"] - iex> WuunderUtils.Maps.put_field(%{value: 20, weight: 200}, "weight", 350) + iex> WuunderUtils.Maps.put(%{value: 20, weight: 200}, "weight", 350) %{value: 20, weight: 350} - iex> WuunderUtils.Maps.put_field(%{value: 20}, "weight", 350) + iex> WuunderUtils.Maps.put(%{value: 20}, "weight", 350) %{:value => 20, "weight" => 350} - iex> WuunderUtils.Maps.put_field(%{"weight" => 350}, :value, 25) + iex> WuunderUtils.Maps.put(%{"weight" => 350}, :value, 25) %{"weight" => 350, "value" => 25} - iex> WuunderUtils.Maps.put_field(%{"weight" => 350}, "value", 25) + iex> WuunderUtils.Maps.put(%{"weight" => 350}, "value", 25) %{"weight" => 350, "value" => 25} """ - @spec put_field(map() | struct() | list() | nil, String.t() | atom(), any()) :: + @spec put(map() | struct() | list() | nil, String.t() | atom(), any()) :: map() | struct() | list() - def put_field(map, key, value) + def put(map, key, value) when is_map(map) and is_valid_map_atom_key(key) do - if Map.has_key?(map, key) || has_only_atom_keys?(map) do + if Map.has_key?(map, key) || only_atom_keys?(map) do Map.put(map, key, value) else Map.put(map, "#{key}", value) end end - def put_field(list, index, value) when is_list(list) and is_integer(index), + def put(list, index, value) when is_list(list) and is_integer(index), do: List.replace_at(list, index, value) - def put_field(map, key, value) when is_map(map) and is_valid_map_binary_key(key) do + def put(map, key, value) when is_map(map) and is_valid_map_binary_key(key) do atom_key = get_safe_key(key) if Map.has_key?(map, atom_key) do @@ -376,122 +376,122 @@ defmodule WuunderUtils.Maps do ...> ] ...> } ...> } - iex> WuunderUtils.Maps.put_field_in(person, [:first_name], "Piet") + iex> WuunderUtils.Maps.deep_put(person, [:first_name], "Piet") %Person{person | first_name: "Piet"} - iex> WuunderUtils.Maps.put_field_in(person, [:country, :code], "US") + iex> WuunderUtils.Maps.deep_put(person, [:country, :code], "US") %Person{person | country: %Country{code: "US"}} - iex> WuunderUtils.Maps.put_field_in(person, [:meta, :skills, 1], "vaultdweller") + iex> WuunderUtils.Maps.deep_put(person, [:meta, :skills, 1], "vaultdweller") %Person{person | meta: %{skills: ["programmer", "vaultdweller", %{name: "painting", type: "hobby"}]}} - iex> WuunderUtils.Maps.put_field_in(person, [:meta, :skills, 2, :name], "walking") + iex> WuunderUtils.Maps.deep_put(person, [:meta, :skills, 2, :name], "walking") %Person{person | meta: %{skills: ["programmer", "manager", %{name: "walking", type: "hobby"}]}} - iex> WuunderUtils.Maps.put_field_in(person, "meta.skills.2.name", "walking") + iex> WuunderUtils.Maps.deep_put(person, "meta.skills.2.name", "walking") %Person{person | meta: %{skills: ["programmer", "manager", %{name: "walking", type: "hobby"}]}} """ - @spec put_field_in( + @spec deep_put( map() | struct() | list() | nil, list(atom() | String.t()) | String.t(), any() ) :: any() - def put_field_in(value, path, value_to_set) + def deep_put(value, path, value_to_set) when (is_map(value) or is_list(value)) and is_binary(path) do keys = keys_from_path(path) - put_field_in(value, keys, value_to_set) + deep_put(value, keys, value_to_set) end - def put_field_in(map_or_list, [key | rest], value_to_set) + def deep_put(map_or_list, [key | rest], value_to_set) when is_map(map_or_list) or is_list(map_or_list) do - current = get_field(map_or_list, key) - put_field(map_or_list, key, put_field_in(current, rest, value_to_set)) + current = get(map_or_list, key) + put(map_or_list, key, deep_put(current, rest, value_to_set)) end - def put_field_in(_map_or_list, [], value_to_set), do: value_to_set + def deep_put(_map_or_list, [], value_to_set), do: value_to_set @doc """ Removes a key from a map. Doesn't matter if the key is an atom or string ## Examples - iex> WuunderUtils.Maps.delete_field(%{length: 255, weight: 100}, :length) + iex> WuunderUtils.Maps.delete(%{length: 255, weight: 100}, :length) %{weight: 100} - iex> WuunderUtils.Maps.delete_field(%{length: 255, weight: 100}, "length") + iex> WuunderUtils.Maps.delete(%{length: 255, weight: 100}, "length") %{weight: 100} - iex> WuunderUtils.Maps.delete_field(%{"value" => 50, "currency" => "EUR"}, "currency") + iex> WuunderUtils.Maps.delete(%{"value" => 50, "currency" => "EUR"}, "currency") %{"value" => 50} - iex> WuunderUtils.Maps.delete_field(%{"value" => 50, "currency" => "EUR"}, :currency) + iex> WuunderUtils.Maps.delete(%{"value" => 50, "currency" => "EUR"}, :currency) %{"value" => 50} - iex> WuunderUtils.Maps.delete_field(["a", "b", "c"], 1) + iex> WuunderUtils.Maps.delete(["a", "b", "c"], 1) ["a", "c"] - iex> WuunderUtils.Maps.delete_field({"a", "b", "c"}, 1) + iex> WuunderUtils.Maps.delete({"a", "b", "c"}, 1) {"a", "c"} iex> country = %Country{code: "NL"} ...> - ...> WuunderUtils.Maps.delete_field(country, :code) + ...> WuunderUtils.Maps.delete(country, :code) %Country{code: ""} iex> country = %Country{code: "NL"} ...> - ...> WuunderUtils.Maps.delete_field(country, "code") + ...> WuunderUtils.Maps.delete(country, "code") %Country{code: ""} iex> country = %Country{code: "NL"} ...> - ...> WuunderUtils.Maps.delete_field(country, "does_not_exist") + ...> WuunderUtils.Maps.delete(country, "does_not_exist") %Country{code: "NL"} """ - @spec delete_field(any(), map_key() | non_neg_integer()) :: any() - def delete_field(%module{} = struct, key) + @spec delete(any(), map_key() | non_neg_integer()) :: any() + def delete(%module{} = struct, key) when is_struct(struct) and is_valid_map_atom_key(key) do default = struct(module, %{}) if Map.has_key?(default, key) do - put_field(struct, key, Map.get(default, key)) + put(struct, key, Map.get(default, key)) else struct end end - def delete_field(struct, key) when is_struct(struct) and is_valid_map_binary_key(key) do + def delete(struct, key) when is_struct(struct) and is_valid_map_binary_key(key) do atom_key = get_safe_key(key) if Map.has_key?(struct, atom_key) do - delete_field(struct, atom_key) + delete(struct, atom_key) else struct end end - def delete_field(map, key) when is_map(map) and is_valid_map_atom_key(key) do - if has_only_atom_keys?(map) do + def delete(map, key) when is_map(map) and is_valid_map_atom_key(key) do + if only_atom_keys?(map) do Map.delete(map, key) else Map.delete(map, "#{key}") end end - def delete_field(tuple, index) + def delete(tuple, index) when is_tuple(tuple) and is_number(index) and index < tuple_size(tuple), do: Tuple.delete_at(tuple, index) - def delete_field(tuple, index) + def delete(tuple, index) when is_tuple(tuple) and is_number(index) and index >= tuple_size(tuple), do: tuple - def delete_field(list, index) when is_list(list) and is_number(index) and index < length(list), + def delete(list, index) when is_list(list) and is_number(index) and index < length(list), do: List.delete_at(list, index) - def delete_field(list, index) when is_list(list) and is_number(index) and index >= length(list), + def delete(list, index) when is_list(list) and is_number(index) and index >= length(list), do: list - def delete_field(map, key) when is_map(map) and is_valid_map_binary_key(key) do + def delete(map, key) when is_map(map) and is_valid_map_binary_key(key) do atom_key = get_safe_key(key) if Map.has_key?(map, atom_key) do @@ -506,7 +506,7 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.delete_field_in(%{"data" => [%{"name" => "Piet"}, %{"name" => "Henk"}]}, "data.0.name") + iex> WuunderUtils.Maps.deep_delete(%{"data" => [%{"name" => "Piet"}, %{"name" => "Henk"}]}, "data.0.name") %{"data" => [%{}, %{"name" => "Henk"}]} iex> person = %Person{ @@ -524,7 +524,7 @@ defmodule WuunderUtils.Maps do ...> } ...> } ...> - ...> WuunderUtils.Maps.delete_field_in(person, [:country, :code]) + ...> WuunderUtils.Maps.deep_delete(person, [:country, :code]) %Person{ country: %Country{code: ""}, address: %Address{ @@ -539,7 +539,7 @@ defmodule WuunderUtils.Maps do ] } } - iex> WuunderUtils.Maps.delete_field_in(person, "meta.skills.1") + iex> WuunderUtils.Maps.deep_delete(person, "meta.skills.1") %Person{ country: %Country{code: "NL"}, address: %Address{ @@ -554,58 +554,58 @@ defmodule WuunderUtils.Maps do } } """ - @spec delete_field_in(any(), list(atom()) | String.t()) :: any() - def delete_field_in(value, path) when is_binary(path) do + @spec deep_delete(any(), list(atom()) | String.t()) :: any() + def deep_delete(value, path) when is_binary(path) do keys = keys_from_path(path) - delete_field_in(value, keys) + deep_delete(value, keys) end - def delete_field_in(nil, _keys), do: nil + def deep_delete(nil, _keys), do: nil - def delete_field_in(value, []), do: value + def deep_delete(value, []), do: value - def delete_field_in(value, _keys) + def deep_delete(value, _keys) when not is_map(value) and not is_list(value) and not is_tuple(value), do: nil - def delete_field_in(map_list_or_tuple, [key]) + def deep_delete(map_list_or_tuple, [key]) when is_map(map_list_or_tuple) or is_list(map_list_or_tuple) or is_tuple(map_list_or_tuple), - do: delete_field(map_list_or_tuple, key) + do: delete(map_list_or_tuple, key) - def delete_field_in(map_list_or_tuple, [_head | _tail] = keys) + def deep_delete(map_list_or_tuple, [_head | _tail] = keys) when is_map(map_list_or_tuple) or is_list(map_list_or_tuple) or is_tuple(map_list_or_tuple) do before_last_key = Enum.slice(keys, 0..-2//1) last_key = List.last(keys) new_value = map_list_or_tuple - |> get_field_in(before_last_key) - |> delete_field(last_key) + |> deep_get(before_last_key) + |> delete(last_key) - put_field_in(map_list_or_tuple, before_last_key, new_value) + deep_put(map_list_or_tuple, before_last_key, new_value) end - def delete_field_in(nil, keys) when is_list(keys), do: nil + def deep_delete(nil, keys) when is_list(keys), do: nil @doc """ Tests if the given map only consists of atom keys ## Examples - iex> WuunderUtils.Maps.has_only_atom_keys?(%{a: 1, b: 2}) + iex> WuunderUtils.Maps.only_atom_keys?(%{a: 1, b: 2}) true - iex> WuunderUtils.Maps.has_only_atom_keys?(%{:a => 1, "b" => 2}) + iex> WuunderUtils.Maps.only_atom_keys?(%{:a => 1, "b" => 2}) false - iex> WuunderUtils.Maps.has_only_atom_keys?(%{"a" => 1, "b" => 2}) + iex> WuunderUtils.Maps.only_atom_keys?(%{"a" => 1, "b" => 2}) false """ - @spec has_only_atom_keys?(map() | struct()) :: boolean() - def has_only_atom_keys?(struct) when is_struct(struct), do: true + @spec only_atom_keys?(map() | struct()) :: boolean() + def only_atom_keys?(struct) when is_struct(struct), do: true - def has_only_atom_keys?(map) when is_map(map) do + def only_atom_keys?(map) when is_map(map) do map |> Map.keys() |> Enum.all?(&is_atom/1) @@ -616,21 +616,21 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.alias_field(%{country: "NL"}, :country, :country_code) + iex> WuunderUtils.Maps.rename_key(%{country: "NL"}, :country, :country_code) %{country_code: "NL"} - iex> WuunderUtils.Maps.alias_field(%{"country" => "NL"}, :country, :country_code) + iex> WuunderUtils.Maps.rename_key(%{"country" => "NL"}, :country, :country_code) %{"country_code" => "NL"} - iex> WuunderUtils.Maps.alias_field(%{street_name: "Straatnaam"}, :street, :street_address) + iex> WuunderUtils.Maps.rename_key(%{street_name: "Straatnaam"}, :street, :street_address) %{street_name: "Straatnaam"} """ - @spec alias_field(map(), atom(), atom()) :: map() - def alias_field(map, from, to) + @spec rename_key(map(), atom(), atom()) :: map() + def rename_key(map, from, to) when is_map(map) and is_valid_map_atom_key(from) and is_valid_map_atom_key(to) do - from_key = if Enum.empty?(map) || has_only_atom_keys?(map), do: from, else: "#{from}" - to_key = if Enum.empty?(map) || has_only_atom_keys?(map), do: to, else: "#{to}" + from_key = if Enum.empty?(map) || only_atom_keys?(map), do: from, else: "#{from}" + to_key = if Enum.empty?(map) || only_atom_keys?(map), do: to, else: "#{to}" if is_nil(Map.get(map, from_key)) == false && is_nil(Map.get(map, to_key)) do map @@ -646,15 +646,15 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.alias_fields(%{country: "NL", street: "Straat", number: 666}, %{country: :country_code, street: :street_name, number: :house_number}) + iex> WuunderUtils.Maps.rename_keys(%{country: "NL", street: "Straat", number: 666}, %{country: :country_code, street: :street_name, number: :house_number}) %{country_code: "NL", house_number: 666, street_name: "Straat"} """ - @spec alias_fields(map(), map()) :: map() - def alias_fields(map, aliasses), + @spec rename_keys(map(), map()) :: map() + def rename_keys(map, aliasses), do: Enum.reduce(Map.keys(aliasses), map, fn key, alias_params -> - alias_field(alias_params, key, Map.get(aliasses, key)) + rename_key(alias_params, key, Map.get(aliasses, key)) end) @doc """ @@ -788,7 +788,7 @@ defmodule WuunderUtils.Maps do do: put_when(map, !!condition.(), key, value) def put_when(map, true, key, value) when is_map(map) and is_valid_map_key(key), - do: put_field(map, key, value) + do: put(map, key, value) def put_when(map, false, key, _value) when is_map(map) and is_valid_map_key(key), do: map @@ -1080,9 +1080,9 @@ defmodule WuunderUtils.Maps do new_value = delete_empty(value) if is_nil(new_value) do - delete_field(new_map, key) + delete(new_map, key) else - put_field(new_map, key, new_value) + put(new_map, key, new_value) end end) @@ -1127,32 +1127,155 @@ defmodule WuunderUtils.Maps do ## Examples - iex> WuunderUtils.Maps.map_all(%{name: " test ", data: ["some item", "other item ", %{x: " value"}]}, &WuunderUtils.Presence.trim/1) + iex> WuunderUtils.Maps.map(%{name: " test ", data: ["some item", "other item ", %{x: " value"}]}, &WuunderUtils.Presence.trim/1) %{data: ["some item", "other item", %{x: "value"}], name: "test"} """ - @spec map_all(any(), function()) :: any() - def map_all(value, map_fn) when is_map(value) and is_function(map_fn) do + @spec map(any(), function()) :: any() + def map(value, map_fn) when is_map(value) and is_function(map_fn) do value |> Map.keys() |> Enum.reduce(value, fn key, new_map -> value = Map.get(new_map, key) - new_value = map_all(value, map_fn) + new_value = map(value, map_fn) - put_field(new_map, key, new_value) + put(new_map, key, new_value) end) end - def map_all(values, map_fn) when is_list(values) and is_function(map_fn), - do: Enum.map(values, &map_all(&1, map_fn)) + def map(values, map_fn) when is_list(values) and is_function(map_fn), + do: Enum.map(values, &map(&1, map_fn)) - def map_all(values, map_fn) when is_tuple(values) and is_function(map_fn) do + def map(values, map_fn) when is_tuple(values) and is_function(map_fn) do values |> Tuple.to_list() - |> map_all(map_fn) + |> map(map_fn) |> List.to_tuple() end - def map_all(value, map_fn), do: map_fn.(value) + def map(value, map_fn), do: map_fn.(value) + + @doc """ + Converts keys in maps to strings. Note: skips structs. So you need to convert these to maps first. + + ## Examples + + iex> WuunderUtils.Maps.stringify_keys(%{ + ...> first_name: "Peter", + ...> last_name: "Griffin", + ...> skills: [%{code: "A", title: "Talking"}, %{code: "B", title: "Moving"}] + ...> }) + %{ + "first_name" => "Peter", + "last_name" => "Griffin", + "skills" => [%{"code" => "A", "title" => "Talking"}, %{"code" => "B", "title" => "Moving"}] + } + + iex> WuunderUtils.Maps.stringify_keys(%Person{ + ...> first_name: "Peter", + ...> last_name: "Pan", + ...> date_of_birth: ~D[1980-01-02], + ...> weight: Decimal.new("81.5"), + ...> country: %Country{code: "UK"}, + ...> time_of_death: ~T[13:37:37], + ...> meta: %{skills: [%{code: "A", title: "Talking"}, %{code: "B", title: "Moving"}]} + ...> }) + %Person{ + first_name: "Peter", + last_name: "Pan", + date_of_birth: ~D[1980-01-02], + weight: Decimal.new("81.5"), + country: %Country{code: "UK"}, + time_of_death: ~T[13:37:37], + meta: %{"skills" => [%{"code" => "A", "title" => "Talking"}, %{"code" => "B", "title" => "Moving"}]} + } + """ + @spec stringify_keys(any()) :: any() + def stringify_keys(struct) when is_struct(struct) do + struct + |> Map.keys() + |> Enum.reduce(struct, fn key, new_struct -> + value = Map.get(struct, key) + Map.put(new_struct, key, stringify_keys(value)) + end) + end + + def stringify_keys(map) when is_map(map) do + map + |> Enum.reduce(%{}, fn {key, value}, new_map -> + Map.put(new_map, "#{key}", stringify_keys(value)) + end) + end + + def stringify_keys(list) when is_list(list), do: Enum.map(list, &stringify_keys/1) + + def stringify_keys(value), do: value + + @doc """ + Converts keys in maps to atoms. By default, this function will only atomize strings that already exist. + Note: skips structs. + + ## Examples + + iex> WuunderUtils.Maps.atomize_keys(%{ + ...> "first_name" => "Peter", + ...> "last_name" => "Griffin", + ...> "skills" => [%{"code" => "A", "title" => "Talking"}, %{"code" => "B", "title" => "Moving"}] + ...> }) + %{ + first_name: "Peter", + last_name: "Griffin", + skills: [%{code: "A", title: "Talking"}, %{code: "B", title: "Moving"}] + } + + iex> WuunderUtils.Maps.atomize_keys(%Person{ + ...> first_name: "Peter", + ...> last_name: "Pan", + ...> date_of_birth: ~D[1980-01-02], + ...> weight: Decimal.new("81.5"), + ...> country: %Country{code: "UK"}, + ...> time_of_death: ~T[13:37:37], + ...> meta: %{"skills" => [%{"code" => "A", "title" => "Talking"}, %{"code" => "B", "title" => "Moving"}]} + ...> }) + %Person{ + first_name: "Peter", + last_name: "Pan", + date_of_birth: ~D[1980-01-02], + weight: Decimal.new("81.5"), + country: %Country{code: "UK"}, + time_of_death: ~T[13:37:37], + meta: %{skills: [%{code: "A", title: "Talking"}, %{code: "B", title: "Moving"}]} + } + """ + @spec atomize_keys(any(), Keyword.t()) :: any() + def atomize_keys(value, options \\ [existing_atoms: true]) + + def atomize_keys(struct, options) when is_struct(struct) and is_list(options) do + struct + |> Map.keys() + |> Enum.reduce(struct, fn key, new_struct -> + value = Map.get(struct, key) + Map.put(new_struct, key, atomize_keys(value, options)) + end) + end + + def atomize_keys(map, options) when is_map(map) and is_list(options) do + map + |> Enum.reduce(%{}, fn {key, value}, new_map -> + new_key = + cond do + is_binary(key) and options[:existing_atoms] -> get_safe_key(key) + is_binary(key) -> String.to_atom(key) + is_atom(key) -> key + end + + Map.put(new_map, new_key, atomize_keys(value, options)) + end) + end + + def atomize_keys(list, options) when is_list(list) and is_list(options), + do: Enum.map(list, &atomize_keys(&1, options)) + + def atomize_keys(value, _options), do: value defp transform_struct(module, struct, transform) do if has_ecto_schema?(module) do diff --git a/lib/wuunder_utils/numbers.ex b/lib/wuunder_utils/numbers.ex index 54040c7..ed8e590 100644 --- a/lib/wuunder_utils/numbers.ex +++ b/lib/wuunder_utils/numbers.ex @@ -122,7 +122,9 @@ defmodule WuunderUtils.Numbers do "1.0" """ - @spec as_string(any()) :: String.t() | nil + @spec as_string(term()) :: nil | String.t() + def as_string(nil), do: nil + def as_string(value) when is_decimal(value), do: Decimal.to_string(value) def as_string(value) when is_float(value), do: Float.to_string(value) def as_string(value) when is_integer(value), do: Integer.to_string(value) @@ -133,8 +135,6 @@ defmodule WuunderUtils.Numbers do |> as_string() end - def as_string(nil), do: nil - @doc """ Adds two Decimal's together. Defaults back to 0. diff --git a/mix.exs b/mix.exs index 6f04afe..1c07761 100644 --- a/mix.exs +++ b/mix.exs @@ -16,7 +16,7 @@ defmodule WuunderUtils.MixProject do start_permanent: Mix.env() == :prod, test_coverage: [tool: ExCoveralls], preferred_cli_env: [coveralls: :test, "coveralls.html": :test], - version: "0.8.1" + version: "0.9.0" ] end @@ -41,9 +41,10 @@ defmodule WuunderUtils.MixProject do defp package do [ name: "wuunder_utils", - files: ~w(lib .formatter.exs mix.exs README*), - licenses: ["Apache-2.0"], + files: ~w(lib .formatter.exs mix.exs CHANGELOG.md LICENSE.md README*), + licenses: ["MIT"], links: %{ + "Changelog" => "https://hexdocs.pm/wuunder_utils/changelog.html", "GitHub" => "https://github.com/wuunder/wuunder_utils", "Docs" => "https://hexdocs.pm/wuunder_utils" } @@ -53,7 +54,7 @@ defmodule WuunderUtils.MixProject do defp docs do [ main: "WuunderUtils", - extras: ["README.md"] + extras: ["README.md", "CHANGELOG.md"] ] end diff --git a/mix.lock b/mix.lock index b70ad65..3ac0e92 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,18 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, + "credo": {:hex, :credo, "1.7.8", "9722ba1681e973025908d542ec3d95db5f9c549251ba5b028e251ad8c24ab8c5", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cb9e87cc64f152f3ed1c6e325e7b894dea8f5ef2e41123bd864e3cd5ceb44968"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, - "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "dialyxir": {:hex, :dialyxir, "1.4.4", "fb3ce8741edeaea59c9ae84d5cec75da00fa89fe401c72d6e047d11a61f65f70", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cd6111e8017ccd563e65621a4d9a4a1c5cd333df30cebc7face8029cacb4eff6"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, }