Skip to content

Commit 75536d8

Browse files
authored
Add support for keyword lists in Access.key/2 and Access.key!/1 (#15470)
1 parent 0395301 commit 75536d8

2 files changed

Lines changed: 123 additions & 14 deletions

File tree

lib/elixir/lib/access.ex

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ defmodule Access do
226226
end
227227
end
228228

229+
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
230+
229231
@doc """
230232
Fetches the value for the given key in a container (a map, keyword
231233
list, or struct that implements the `Access` behaviour).
@@ -485,7 +487,7 @@ defmodule Access do
485487
## Accessors
486488

487489
@doc """
488-
Returns a function that accesses the given key in a map/struct.
490+
Returns a function that accesses the given key in a map/struct/keyword list.
489491
490492
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
491493
`Kernel.get_and_update_in/3`, and friends.
@@ -514,30 +516,56 @@ defmodule Access do
514516
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
515517
{"john", %{user: %{}}}
516518
517-
An error is raised if the accessed structure is not a map or a struct:
519+
iex> keyword = [user: [name: "john"]]
520+
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
521+
"john"
522+
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
523+
...> {prev, String.upcase(prev)}
524+
...> end)
525+
{"john", [user: [name: "JOHN"]]}
526+
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
527+
{"john", [user: []]}
518528
519-
iex> get_in([], [Access.key(:foo)])
520-
** (BadMapError) expected a map, got:
521-
...
529+
An error is raised if the accessed structure is not a map, struct, or keyword list:
530+
531+
iex> get_in(123, [Access.key(:foo)])
532+
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
533+
534+
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
535+
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
522536
"""
523-
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
537+
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
524538
def key(key, default \\ nil) do
525539
fn
526-
:get, data, next ->
540+
:get, %{} = data, next ->
527541
next.(Map.get(data, key, default))
528542

529-
:get_and_update, data, next ->
543+
:get_and_update, %{} = data, next ->
530544
value = Map.get(data, key, default)
531545

532546
case next.(value) do
533547
{get, update} -> {get, Map.put(data, key, update)}
534548
:pop -> {value, Map.delete(data, key)}
535549
end
550+
551+
:get, data, next when is_probably_keyword(data) ->
552+
next.(Keyword.get(data, key, default))
553+
554+
:get_and_update, data, next when is_probably_keyword(data) ->
555+
value = Keyword.get(data, key, default)
556+
557+
case next.(value) do
558+
{get, update} -> {get, Keyword.put(data, key, update)}
559+
:pop -> {value, Keyword.delete(data, key)}
560+
end
561+
562+
_op, data, _next ->
563+
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
536564
end
537565
end
538566

539567
@doc """
540-
Returns a function that accesses the given key in a map/struct.
568+
Returns a function that accesses the given key in a map/struct/keyword list.
541569
542570
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
543571
`Kernel.get_and_update_in/3`, and friends.
@@ -546,6 +574,19 @@ defmodule Access do
546574
547575
## Examples
548576
577+
iex> keyword = [user: [name: "john"]]
578+
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
579+
"john"
580+
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
581+
...> {prev, String.upcase(prev)}
582+
...> end)
583+
{"john", [user: [name: "JOHN"]]}
584+
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
585+
{"john", [user: []]}
586+
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
587+
** (KeyError) key :unknown not found in:
588+
...
589+
549590
iex> map = %{user: %{name: "john"}}
550591
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
551592
"john"
@@ -574,13 +615,15 @@ defmodule Access do
574615
`Access.key!/1` is useful when the key is not known in advance
575616
and must be accessed dynamically.
576617
577-
An error is raised if the accessed structure is not a map/struct:
618+
An error is raised if the accessed structure is not a map/struct/keyword list:
578619
579-
iex> get_in([], [Access.key!(:foo)])
580-
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
620+
iex> get_in(123, [Access.key!(:foo)])
621+
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
581622
623+
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
624+
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
582625
"""
583-
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
626+
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
584627
def key!(key) do
585628
fn
586629
:get, %{} = data, next ->
@@ -594,8 +637,19 @@ defmodule Access do
594637
:pop -> {value, Map.delete(data, key)}
595638
end
596639

640+
:get, data, next when is_probably_keyword(data) ->
641+
next.(Keyword.fetch!(data, key))
642+
643+
:get_and_update, data, next when is_probably_keyword(data) ->
644+
value = Keyword.fetch!(data, key)
645+
646+
case next.(value) do
647+
{get, update} -> {get, Keyword.put(data, key, update)}
648+
:pop -> {value, Keyword.delete(data, key)}
649+
end
650+
597651
_op, data, _next ->
598-
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
652+
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
599653
end
600654
end
601655

lib/elixir/test/elixir/access_test.exs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,61 @@ defmodule AccessTest do
219219
end
220220
end
221221

222+
describe "key/2 and key!/1" do
223+
@test_map %{foo: :bar, baz: :qux}
224+
225+
test "finds key in map" do
226+
assert get_in(@test_map, [Access.key(:foo)]) == :bar
227+
assert get_in(@test_map, [Access.key(:missing)]) == nil
228+
229+
assert get_in(@test_map, [Access.key!(:foo)]) == :bar
230+
231+
assert_raise KeyError, fn ->
232+
get_in(@test_map, [Access.key!(:missing)])
233+
end
234+
end
235+
236+
test "finds key in struct" do
237+
defmodule KeySample do
238+
defstruct foo: :bar, baz: :qux
239+
end
240+
241+
assert get_in(struct(KeySample), [Access.key(:foo)]) == :bar
242+
assert get_in(struct(KeySample), [Access.key(:missing)]) == nil
243+
244+
assert get_in(struct(KeySample), [Access.key!(:foo)]) == :bar
245+
246+
assert_raise KeyError, fn ->
247+
get_in(struct(KeySample), [Access.key!(:missing)])
248+
end
249+
end
250+
251+
@test_keyword [foo: :bar, baz: :qux]
252+
253+
test "finds key in keyword list" do
254+
assert get_in(@test_keyword, [Access.key(:foo)]) == :bar
255+
assert get_in(@test_keyword, [Access.key(:missing)]) == nil
256+
257+
assert get_in(@test_keyword, [Access.key!(:foo)]) == :bar
258+
259+
assert_raise KeyError, fn ->
260+
get_in(@test_keyword, [Access.key!(:missing)])
261+
end
262+
end
263+
264+
test "raises when key/2 access is attempted on [1,2,3]" do
265+
assert_raise RuntimeError, ~r"Access.key/2 expected a map/struct/keyword list", fn ->
266+
put_in([1, 2, 3], [Access.key(:foo)], :bar)
267+
end
268+
end
269+
270+
test "raises when key!/1 access is attempted on [1,2,3]" do
271+
assert_raise RuntimeError, ~r"Access.key!/1 expected a map/struct/keyword list", fn ->
272+
put_in([1, 2, 3], [Access.key!(:foo)], :bar)
273+
end
274+
end
275+
end
276+
222277
describe "at/1" do
223278
@test_list [1, 2, 3, 4, 5, 6]
224279

0 commit comments

Comments
 (0)