Skip to content

Commit f9ec6cf

Browse files
author
José Valim
committed
Merge pull request #1740 from yrashk/record-string-fields
Allow for string names when instantiating or updating records
2 parents d221e9a + cc1d7c5 commit f9ec6cf

File tree

2 files changed

+62
-8
lines changed

2 files changed

+62
-8
lines changed

lib/elixir/lib/record.ex

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ defmodule Record do
139139
records flexibility at the cost of performance since
140140
there is more work happening at runtime.
141141
142+
The above calls (new and update) can interchangeably accept both
143+
atom and string keys for field names, however not both at the same time.
144+
Please also note that atom keys are faster. This feature allows to
145+
"sanitize" untrusted dictionaries and initialize/update records without
146+
using `binary_to_existing_atom/1`.
147+
142148
To sum up, `defrecordp` should be used when you don't want
143149
to expose the record information while `defrecord` should be used
144150
whenever you want to share a record within your code or with other
@@ -624,17 +630,27 @@ defmodule Record do
624630
# an ordered dict of options (opts) and it will try to fetch
625631
# the given key from the ordered dict, falling back to the
626632
# default value if one does not exist.
627-
selective = lc { k, v } inlist values do
633+
atom_selective = lc { k, v } inlist values do
628634
quote do: Keyword.get(opts, unquote(k), unquote(v))
629635
end
636+
string_selective = lc { k, v } inlist values do
637+
k = atom_to_binary(k)
638+
quote do
639+
case :lists.keyfind(unquote(k), 1, opts) do
640+
false -> unquote(v)
641+
{_, v} -> v
642+
end
643+
end
644+
end
630645
631646
quote do
632647
@doc false
633648
def new(), do: new([])
634649
635650
@doc false
636651
def new([]), do: { __MODULE__, unquote_splicing(defaults) }
637-
def new(opts) when is_list(opts), do: { __MODULE__, unquote_splicing(selective) }
652+
def new([{key, _}|_] = opts) when is_atom(key), do: { __MODULE__, unquote_splicing(atom_selective) }
653+
def new([{key, _}|_] = opts) when is_binary(key), do: { __MODULE__, unquote_splicing(string_selective) }
638654
end
639655
end
640656
@@ -725,24 +741,38 @@ defmodule Record do
725741
# Define an updater method that receives a
726742
# keyword list and updates the record.
727743
defp updater(values) do
728-
fields =
744+
atom_fields =
745+
lc {key, _default} inlist values do
746+
index = find_index(values, key, 1)
747+
quote do: Keyword.get(keywords, unquote(key), elem(record, unquote(index)))
748+
end
749+
750+
string_fields =
729751
lc {key, _default} inlist values do
730752
index = find_index(values, key, 1)
753+
key = atom_to_binary(key)
731754
quote do
732-
Keyword.get(keywords, unquote(key), elem(record, unquote(index)))
755+
case :lists.keyfind(unquote(key), 1, keywords) do
756+
false -> elem(record, unquote(index))
757+
{_, value} -> value
758+
end
733759
end
734760
end
735761
736-
contents = quote do: { __MODULE__, unquote_splicing(fields) }
762+
atom_contents = quote do: { __MODULE__, unquote_splicing(atom_fields) }
763+
string_contents = quote do: { __MODULE__, unquote_splicing(string_fields) }
737764
738765
quote do
739766
@doc false
740767
def update([], record) do
741768
record
742769
end
743770
744-
def update(keywords, record) do
745-
unquote(contents)
771+
def update([{key, _}|_] = keywords, record) when is_atom(key) do
772+
unquote(atom_contents)
773+
end
774+
def update([{key, _}|_] = keywords, record) when is_binary(key) do
775+
unquote(string_contents)
746776
end
747777
end
748778
end
@@ -765,14 +795,15 @@ defmodule Record do
765795
defp core_specs(values) do
766796
types = lc { _, _, spec } inlist values, do: spec
767797
options = if values == [], do: [], else: [options_specs(values)]
798+
values_specs = if values == [], do: [], else: values_specs(values)
768799
769800
quote do
770801
unless Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
771802
@type t :: { __MODULE__, unquote_splicing(types) }
772803
end
773804
774805
unless Kernel.Typespec.defines_type?(__MODULE__, :options, 0) do
775-
@type options :: unquote(options)
806+
@type options :: unquote(options) | [{String.t, unquote(values_specs)}]
776807
end
777808
778809
@spec new :: t
@@ -790,6 +821,11 @@ defmodule Record do
790821
{ :|, [], [{ k, v }, acc] }
791822
end, { k, v }, t
792823
end
824+
defp values_specs([{ _, _, v }|t]) do
825+
:lists.foldl fn { _, _, v }, acc ->
826+
{ :|, [], [v, acc] }
827+
end, v, t
828+
end
793829
794830
defp accessor_specs([{ :__exception__, _, _ }|t], 1, acc) do
795831
accessor_specs(t, 2, acc)

lib/elixir/test/elixir/record_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ defmodule RecordTest.Macros do
7070
defrecord Record, [a: 1, b: 2]
7171
end
7272

73+
defrecord RecordTest.FooTest, foo: nil, bar: nil
74+
7375
defmodule RecordTest do
7476
use ExUnit.Case, async: true
7577

@@ -162,6 +164,22 @@ defmodule RecordTest do
162164
assert is_record(namespace, :xmlNamespace)
163165
end
164166

167+
test :string_names do
168+
a = RecordTest.FooTest.new([{"foo", 1}, {"bar", 1}])
169+
assert a.foo == 1
170+
assert a.bar == 1
171+
a = a.update([{"foo", 2}, {"bar", 2}])
172+
assert a.foo == 2
173+
assert a.bar == 2
174+
end
175+
176+
test :string_names_import do
177+
record = RecordTest.FileInfo.new([{"type", :regular}, {"access", 100}])
178+
assert record.type == :regular
179+
assert record.access == 100
180+
assert record.update([{"access", 101}]).access == 101
181+
end
182+
165183
defp empty_tuple, do: {}
166184
defp a_tuple, do: { :foo, :bar, :baz }
167185
defp a_list, do: [ :foo, :bar, :baz ]

0 commit comments

Comments
 (0)