diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..525446d --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/lib/deferred_config.ex b/lib/deferred_config.ex index b0ab201..c28a106 100644 --- a/lib/deferred_config.ex +++ b/lib/deferred_config.ex @@ -1,4 +1,4 @@ -defmodule DeferredConfig do +defmodule DeferredConfig do @moduledoc """ Seamlessly add runtime config to your library, with the "system tuples" or the `{m,f,a}` patterns. @@ -15,27 +15,27 @@ defmodule DeferredConfig do end Where `:mine` is the name of your OTP app. - + Now you and users of your app or lib can configure as follows, and it'll work -- regardless of if they're running it from iex, or a release with env vars set: - - config :mine, - + + config :mine, + # string from env var, or `nil` if missing. port1: {:system, "PORT"}, # string from env var |> integer; `nil` if missing. port2: {:system, "PORT", {String, :to_integer}}, - + # string from env var, or "4000" as default. port3: {:system, "PORT", "4000"}, - + # converts env var to integer, or 4000 as default. port4: {:system, "PORT", 4000, {String, :to_integer}} - + **Accessing config does not change.** - + Since you can use arbitrary transformation functions, you can do advanced transformations if you need to: @@ -50,11 +50,11 @@ defmodule DeferredConfig do end end - # config.exs + # config.exs config :my_app, port: {:system, "MY_IP", {127,0,0,1}, {Mine.Ip, :str2ip} - See `README.md` for explanation of rationale. + See `README.md` for explanation of rationale. **TL;DR:** `REPLACE_OS_VARS` is string-only and release-only, and `{:system, ...}` support among libraries is spotty and easy to get wrong in ways that bite your users @@ -65,19 +65,17 @@ defmodule DeferredConfig do """ require Logger import ReplacingWalk, only: [walk: 3] - + @default_rts [ - {&DeferredConfig.recognize_system_tuple/1, - &DeferredConfig.get_system_tuple/1}, - {&DeferredConfig.recognize_mfa_tuple/1, - &DeferredConfig.transform_mfa_tuple/1} + {&DeferredConfig.recognize_system_tuple/1, &DeferredConfig.get_system_tuple/1}, + {&DeferredConfig.recognize_mfa_tuple/1, &DeferredConfig.transform_mfa_tuple/1} ] - + @doc """ Populate deferred values in an app's config. Best run during `Application.start/2`. - **By default** attempts to populate the common + **By default** attempts to populate the common `{:system, "VAR"}` tuple form for getting values from `System.get_env/1`, and the more general `{:apply, {Mod, fun, [args]}}` form as well. @@ -86,25 +84,26 @@ defmodule DeferredConfig do defaults and conversion functions, see `Peerage.DeferredConfig.get_system_tuple/1`. - Can be extended by passing in a different - enumerable of `{&recognizer/1, &transformer/1}` + Can be extended by passing in a different + enumerable of `{&recognizer/1, &transformer/1}` functions. """ def populate(app, transforms \\ @default_rts) do - :ok = app - |> Application.get_all_env - |> transform_cfg(transforms) - |> apply_transformed_cfg!(app) + :ok = + app + |> Application.get_all_env() + |> transform_cfg(transforms) + |> apply_transformed_cfg!(app) end @doc """ - Given a config kvlist, and an enumerable of + Given a config kvlist, and an enumerable of `{&recognize/1, &transform/1}` functions, returns a kvlist with the values transformed via replacing walk. """ def transform_cfg(cfg, rts \\ @default_rts) when is_list(rts) do - Enum.map(cfg, fn {k,v} -> + Enum.map(cfg, fn {k, v} -> {k, apply_rts(v, rts)} end) end @@ -112,7 +111,7 @@ defmodule DeferredConfig do @doc "`Application.put_env/3` for config kvlist" def apply_transformed_cfg!(kvlist, app) do kvlist - |> Enum.each(fn {k,v} -> + |> Enum.each(fn {k, v} -> Application.put_env(app, k, v) end) end @@ -126,6 +125,7 @@ defmodule DeferredConfig do # apply sequence of replacing walks to a value defp apply_rts(val, []), do: val + defp apply_rts(val, rts) when is_list(rts) do Enum.reduce(rts, val, fn {r, t}, acc_v -> walk(acc_v, r, t) @@ -136,20 +136,20 @@ defmodule DeferredConfig do Recognize mfa tuple, like `{:apply, {File, :read!, ["name"]}}`. Returns `true` on recognition, `false` otherwise. """ - def recognize_mfa_tuple({:apply, {m,f,a}}) - when is_atom(m) and is_atom(f) and is_list(a), - do: true + def recognize_mfa_tuple({:apply, {m, f, a}}) + when is_atom(m) and is_atom(f) and is_list(a), + do: true + def recognize_mfa_tuple({:apply, t}) do - Logger.error "badcfg - :apply needs {:m, :f, lst}. "<> - "given: #{ inspect t }" + Logger.error("badcfg - :apply needs {:m, :f, lst}. " <> "given: #{inspect(t)}") false end + def recognize_mfa_tuple(_), do: false - + @doc "Return evaluated `{:apply, {mod, fun, args}}` tuple." - def transform_mfa_tuple({:apply, {m,f,a}}), do: apply(m,f,a) + def transform_mfa_tuple({:apply, {m, f, a}}), do: apply(m, f, a) - @doc """ Recognizer for system tuples of forms: - `{:system, "VAR"}` @@ -158,28 +158,27 @@ defmodule DeferredConfig do - `{:system, "VAR", default_value, {String, :to_integer}}` Returns `true` when it matches one, `false` otherwise. """ - def recognize_system_tuple({:system, ""<>_k}), do: true - def recognize_system_tuple({:system, ""<>_k, _default}), do: true - def recognize_system_tuple({:system, ""<>_k, _d, _mf}), do: true - def recognize_system_tuple(_), do: false + def recognize_system_tuple({:system, "" <> _k}), do: true + def recognize_system_tuple({:system, "" <> _k, _default}), do: true + def recognize_system_tuple({:system, "" <> _k, _d, _mf}), do: true + def recognize_system_tuple(_), do: false @doc """ Return transformed copy of recognized system tuples: - gets from env, optionally converts it, with + gets from env, optionally converts it, with optional default if env returned nothing. """ def get_system_tuple({:system, k}), do: System.get_env(k) + def get_system_tuple({:system, k, {m, f}}) do - apply m, f, [ System.get_env(k) ] + apply(m, f, [System.get_env(k)]) end + def get_system_tuple({:system, k, d}), do: System.get_env(k) || d + def get_system_tuple({:system, k, d, {m, f}}) do - (val = System.get_env k) && apply(m, f, [val]) || d + ((val = System.get_env(k)) && apply(m, f, [val])) || d end - def get_system_tuple(t), do: throw "Could not fetch: #{inspect t}" - - - + def get_system_tuple(t), do: throw("Could not fetch: #{inspect(t)}") end - diff --git a/lib/replacing_walk.ex b/lib/replacing_walk.ex index 6b04d73..8a4498c 100644 --- a/lib/replacing_walk.ex +++ b/lib/replacing_walk.ex @@ -7,33 +7,33 @@ defmodule ReplacingWalk do """ require Logger - + @doc """ Recursive replacing walk that uses `recognize` and `transform` functions to return a transformed version - of arbitrary data. + of arbitrary data. iex> ReplacingWalk.walk [1, 2, 3], &(&1 == 2), &(&1 * &1) [1,4,3] iex> ReplacingWalk.walk( [1, [2, [3, 2]]], - ...> &(&1 == 2), - ...> &(&1 * &1) + ...> &(&1 == 2), + ...> &(&1 * &1) ...> ) [1,[4, [3, 4]]] It works for Maps: - + iex> ReplacingWalk.walk %{2 => 1, 1 => 2}, &(&1 == 2), &(&1 * &1) %{4 => 1, 1 => 4} - - Structs in general are considered as leaf nodes; we support + + Structs in general are considered as leaf nodes; we support structs that implement Enumerable, but **currently we expect their `Enumerable` implementation to work like a Map. - If you feed this an Enumerable struct that doesn't iterate - like Map -- ie, doesn't iterate over `{k, v}` -- it will die. + If you feed this an Enumerable struct that doesn't iterate + like Map -- ie, doesn't iterate over `{k, v}` -- it will die. (See an example in tests). - + We may change that behavior in the future -- either removing support for arbitrary Enumerables, or provision another protocol that can be implemented to make a data type replacing-walkable. @@ -42,17 +42,21 @@ defmodule ReplacingWalk do `:deferred_config`, so it's probably got some holes; tests that break it are welcome. """ - + # lists def walk(_data = [], _recognize, _transform), do: [] + def walk([item | ls], recognize, transform) do item = item |> maybe_transform_leaf(recognize, transform) - [ walk(item, recognize, transform) | - walk(ls, recognize, transform) ] + + [ + walk(item, recognize, transform) + | walk(ls, recognize, transform) + ] end # structs (enumerable and not; see notes about Enumerable) - def walk(m = %{ :__struct__ => _ }, recognize, transform) do + def walk(m = %{:__struct__ => _}, recognize, transform) do if Enumerable.impl_for(m) do m |> walk_map(recognize, transform) else @@ -64,18 +68,21 @@ defmodule ReplacingWalk do def walk(m, recognize, transform) when is_map(m) do m |> walk_map(recognize, transform) end + def walk(%{}, _, _), do: %{} # kv tuples (very common in config) - def walk(t = {k,v}, recognize, transform) do + def walk(t = {_k, _v}, recognize, transform) do t = maybe_transform_leaf(t, recognize, transform) - if is_tuple t do + + if is_tuple(t) do {k, v} = t - {k |> walk(recognize, transform), - v |> walk(recognize, transform) } - else t end + {k |> walk(recognize, transform), v |> walk(recognize, transform)} + else + t + end end - + # any other data (other tuples; structs; str, atoms, nums..) def walk(other, recognize, transform) do recognize.(other) |> maybe_do(transform, other) @@ -83,7 +90,6 @@ defmodule ReplacingWalk do # -- impl details for map and maplike enum support defp walk_map(m, recognize, transform) do - m = m |> maybe_transform_leaf(recognize, transform) # due to above, may not be enumerable any more. @@ -91,11 +97,12 @@ defmodule ReplacingWalk do # non-map-like iteration, which we *can't* detect without trying. try do Enum.reduce(m, %{}, fn {k, v}, acc -> - k = recognize.(k) |> maybe_do( transform, k ) + k = recognize.(k) |> maybe_do(transform, k) acc |> Map.put(k, walk(v, recognize, transform)) end) - catch _ -> - Logger.error("replacing walk: reduce failed for: #{inspect m}") + catch + _ -> + Logger.error("replacing walk: reduce failed for: #{inspect(m)}") m end end @@ -103,7 +110,7 @@ defmodule ReplacingWalk do defp maybe_transform_leaf(o, recognize, transform) do recognize.(o) |> maybe_do(transform, o) end - defp maybe_do(_should_i = true, op, item), do: op.(item) - defp maybe_do(_shouldnt, _op, item), do: item + defp maybe_do(_should_i = true, op, item), do: op.(item) + defp maybe_do(_shouldnt, _op, item), do: item end diff --git a/mix.exs b/mix.exs index 2e01de9..0634fd3 100644 --- a/mix.exs +++ b/mix.exs @@ -2,26 +2,26 @@ defmodule DeferredConfig.Mixfile do use Mix.Project def project do - [app: :deferred_config, - version: "0.1.1", - elixir: "~> 1.4-rc", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - consolidate_protocols: Mix.env != :test, # proto impl tests - deps: deps(), - - name: "DeferredConfig", - package: package(), - description: description(), - source_url: "https://github.com/mrluc/deferred_config", - homepage_url: "https://github.com/mrluc/deferred_config", - docs: [main: "readme", - extras: ["README.md"]] + [ + app: :deferred_config, + version: "0.1.1", + elixir: "~> 1.4-rc", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + # proto impl tests + consolidate_protocols: Mix.env() != :test, + deps: deps(), + name: "DeferredConfig", + package: package(), + description: description(), + source_url: "https://github.com/mrluc/deferred_config", + homepage_url: "https://github.com/mrluc/deferred_config", + docs: [main: "readme", extras: ["README.md"]] ] end def description do - "Seamless runtime config with one line of code. "<> + "Seamless runtime config with one line of code. " <> "No special accessors or mappings. Full support for " <> "'{:system...} tuple' and '{m,f,a}' runtime config patterns." end @@ -31,17 +31,18 @@ defmodule DeferredConfig.Mixfile do files: ["lib", "mix.exs", "README*", "LICENSE*"], maintainers: ["Luc Fueston"], licenses: ["MIT"], - links: %{"GitHub" => "https://github.com/mrluc/deferred_config", - "Docs" => "https://hexdocs.pm/deferred_config/readme.html"} + links: %{ + "GitHub" => "https://github.com/mrluc/deferred_config", + "Docs" => "https://hexdocs.pm/deferred_config/readme.html" + } ] end - + def application do [extra_applications: [:logger]] end defp deps do - [{:ex_doc, "~> 0.14", only: :dev}, - {:credo, "~> 0.5", only: [:dev, :test]}] + [{:ex_doc, "~> 0.14", only: :dev}, {:credo, "~> 0.5", only: [:dev, :test]}] end end diff --git a/mix.lock b/mix.lock index 0e99ff4..be76ec2 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,10 @@ -%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, - "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, - "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, + "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"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, +} diff --git a/test/deferred_config_test.exs b/test/deferred_config_test.exs index a7eb89f..ece7f37 100644 --- a/test/deferred_config_test.exs +++ b/test/deferred_config_test.exs @@ -3,29 +3,30 @@ defmodule DeferredConfigTest do doctest DeferredConfig @app :lazy_cfg_test_appname - + defmodule MyMod do - def get_my_key(""<>bin), do: "your key is 1234. write it down." + def get_my_key("" <> _bin), do: "your key is 1234. write it down." end - + setup do delete_all_env(@app) # give each test a fake env that looks like this env = %{"PORT" => "4000"} + system_transform = fn - {:system, k} -> Map.get(env, k) - {:system, k, {m, f}} -> apply m, f, [Map.get(env, k)] - {:system, k, d} -> Map.get(env, k, d) - {:system, k, d, {m, f}} -> apply( m, f, [Map.get(env, k)]) || d + {:system, k} -> Map.get(env, k) + {:system, k, {m, f}} -> apply(m, f, [Map.get(env, k)]) + {:system, k, d} -> Map.get(env, k, d) + {:system, k, d, {m, f}} -> apply(m, f, [Map.get(env, k)]) || d end + # our mock stack -- only changes env var retrieval transforms = [ {&DeferredConfig.recognize_system_tuple/1, system_transform}, - {&DeferredConfig.recognize_mfa_tuple/1, - &DeferredConfig.transform_mfa_tuple/1} + {&DeferredConfig.recognize_mfa_tuple/1, &DeferredConfig.transform_mfa_tuple/1} ] - [transforms: transforms, - system_transform: system_transform] + + [transforms: transforms, system_transform: system_transform] end test "system tuples support", %{system_transform: transform} do @@ -34,12 +35,15 @@ defmodule DeferredConfigTest do port2: {:system, "PORT", "1111"}, port3: {:system, "FAIL", "1111"}, port4: {:system, "PORT", {String, :to_integer}}, - port5: [{:system, "PORT", 3000, {String, :to_integer}}], + port5: [{:system, "PORT", 3000, {String, :to_integer}}] ] - actual = cfg - |> DeferredConfig.transform_cfg([ - {&DeferredConfig.recognize_system_tuple/1, transform} - ]) + + actual = + cfg + |> DeferredConfig.transform_cfg([ + {&DeferredConfig.recognize_system_tuple/1, transform} + ]) + assert actual[:port1] == "4000" assert actual[:port2] == "4000" assert actual[:port3] == "1111" @@ -47,7 +51,7 @@ defmodule DeferredConfigTest do assert actual[:port5] == [4000] actual |> DeferredConfig.apply_transformed_cfg!(@app) - actual = Application.get_all_env @app + actual = Application.get_all_env(@app) assert actual[:port1] == "4000" assert actual[:port2] == "4000" assert actual[:port3] == "1111" @@ -56,31 +60,34 @@ defmodule DeferredConfigTest do end test "non-existent tuple values are handled" do - r = DeferredConfig.transform_cfg([key: {:system, "ASDF"}]) + r = DeferredConfig.transform_cfg(key: {:system, "ASDF"}) assert r[:key] == nil end + test "readme sys/mfa example", %{transforms: transforms} do readme_example = [ - http: %{ # even inside nested data + # even inside nested data + http: %{ # the common 'system tuple' pattern is fully supported port: {:system, "PORT", {String, :to_integer}} }, # more general 'mfa tuple' pattern is also supported key: {:apply, {MyMod, :get_my_key, ["arg"]}} ] - actual = readme_example - |> DeferredConfig.transform_cfg(transforms) - - assert "your key is"<>_ = actual[:key] + + actual = + readme_example + |> DeferredConfig.transform_cfg(transforms) + + assert "your key is" <> _ = actual[:key] assert actual[:http][:port] == 4000 end - + defp delete_all_env(app) do app - |> Application.get_all_env - |> Enum.each(fn {k, v} -> - Application.delete_env( app, k ) + |> Application.get_all_env() + |> Enum.each(fn {k, _v} -> + Application.delete_env(app, k) end) end - end diff --git a/test/replacing_walk_test.exs b/test/replacing_walk_test.exs index 272cef2..0c78b22 100644 --- a/test/replacing_walk_test.exs +++ b/test/replacing_walk_test.exs @@ -1,8 +1,18 @@ -defmodule Ham do defstruct( a: 1 ) end -defmodule Spam do defstruct( a: 1, b: 2 ) end +defmodule Ham do + defstruct(a: 1) +end + +defmodule Spam do + defstruct(a: 1, b: 2) +end + defimpl Enumerable, for: Spam do - def count(_), do: {:error, __MODULE__} # default reduce-based - def member?(_,_), do: {:error, __MODULE__} # default reduce-based + def count(_), do: {:error, __MODULE__} + + def member?(_, _), do: {:error, __MODULE__} + + def slice(_), do: {:error, __MODULE__} + def reduce(%{a: a, b: b}, {_, acc}, fun) do {:cont, acc} = fun.({:a, a}, acc) {:cont, acc} = fun.({:b, b}, acc) @@ -16,72 +26,89 @@ defmodule EnvTest do alias ReplacingWalk, as: RW test "basic " do - data = [:a, :b, :c] + data = [:a, :b, :c] expected = [:balls, :b, :c] - actual = data - |> RW.walk( &recognize_atom_a/1, &transform_to_balls/1 ) + + actual = + data + |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) + assert expected == actual end test "maps" do - data = %{ :a => 1, :b => 2, :a => :a } - expected = %{ :balls => 1, :b => 2, :balls => :balls} - actual = data - |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) + data = %{:a => 1, :b => 2} + expected = %{:balls => 1, :b => 2} + + actual = + data + |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) + assert expected == actual end - + test "map - kitchen sink" do assert example_data().kitchen_sink_map_a_to_balls == - example_data().kitchen_sink_map_a - |> RW.walk( &recognize_atom_a/1, &transform_to_balls/1) + example_data().kitchen_sink_map_a + |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) end test "structs won't change if they're not enumerable" do - data = %Ham{ a: 2} - expected = %Ham{ a: 2} - actual = data - |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) + data = %Ham{a: 2} + expected = %Ham{a: 2} + + actual = + data + |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) + assert expected == actual end test "enumerable types are ok tho" do recognize = fn i -> i == 3 end - data = %Spam{ a: 2, b: 3} + data = %Spam{a: 2, b: 3} assert Enumerable.impl_for(data) == Enumerable.Spam - expected = %{ a: 2, b: :balls} - actual = data - |> RW.walk(recognize, &transform_to_balls/1) + expected = %{a: 2, b: :balls} + + actual = + data + |> RW.walk(recognize, &transform_to_balls/1) + assert expected == actual end def recognize_atom_a(:a), do: true - def recognize_atom_a(_), do: false + def recognize_atom_a(_), do: false def transform_to_balls(_), do: :balls def example_data do - %{kitchen_sink_map_a: - %{ - :a => 1, # replaceable key + %{ + kitchen_sink_map_a: %{ + # replaceable key + :a => 1, :b => 2, :c => [ - [ :a, {:a, 3} ], # list - [ a: 2, a: 3 ] # kvlist + # list + [:a, {:a, 3}], + # kvlist + [a: 2, a: 3] ], - :d => :a # replaceable value + # replaceable value + :d => :a }, - kitchen_sink_map_a_to_balls: - %{ - :balls => 1, # replaceable key + kitchen_sink_map_a_to_balls: %{ + # replaceable key + :balls => 1, :b => 2, :c => [ - [ :balls, {:balls, 3} ], # tuples NOT replaced yet - [ balls: 2, balls: 3 ] # kvlist NOT replaced yet + # tuples NOT replaced yet + [:balls, {:balls, 3}], + # kvlist NOT replaced yet + [balls: 2, balls: 3] ], - :d => :balls # replaceable value + # replaceable value + :d => :balls } } end end - -