Skip to content

Commit 7649a5d

Browse files
committed
Allow for string names when instantiating or updating records
This provides a great added benefit of allowing to import untrusted dictionary-like structures without having to sanitize keys before converting them to atoms, without the risk of running out of atoms. The performance for atom field names remains the same, string names are somewhat slower (as expected)
1 parent d221e9a commit 7649a5d

File tree

2 files changed

+49
-3
lines changed

2 files changed

+49
-3
lines changed

lib/elixir/lib/record.ex

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ 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. Please note, however, that
144+
atom keys are faster.
145+
142146
To sum up, `defrecordp` should be used when you don't want
143147
to expose the record information while `defrecord` should be used
144148
whenever you want to share a record within your code or with other
@@ -625,7 +629,17 @@ defmodule Record do
625629
# the given key from the ordered dict, falling back to the
626630
# default value if one does not exist.
627631
selective = lc { k, v } inlist values do
628-
quote do: Keyword.get(opts, unquote(k), unquote(v))
632+
string_k = atom_to_binary(k)
633+
quote do
634+
case :lists.keyfind(unquote(k), 1, opts) do
635+
false ->
636+
case :lists.keyfind(unquote(string_k), 1, opts) do
637+
false -> unquote(v)
638+
{_, value} -> value
639+
end
640+
{_, value} -> value
641+
end
642+
end
629643
end
630644
631645
quote do
@@ -727,9 +741,17 @@ defmodule Record do
727741
defp updater(values) do
728742
fields =
729743
lc {key, _default} inlist values do
744+
string_key = atom_to_binary(key)
730745
index = find_index(values, key, 1)
731746
quote do
732-
Keyword.get(keywords, unquote(key), elem(record, unquote(index)))
747+
case :lists.keyfind(unquote(key), 1, keywords) do
748+
false ->
749+
case :lists.keyfind(unquote(string_key), 1, keywords) do
750+
false -> elem(record, unquote(index))
751+
{_, value} -> value
752+
end
753+
{_, value} -> value
754+
end
733755
end
734756
end
735757
@@ -765,14 +787,15 @@ defmodule Record do
765787
defp core_specs(values) do
766788
types = lc { _, _, spec } inlist values, do: spec
767789
options = if values == [], do: [], else: [options_specs(values)]
790+
values_specs = if values == [], do: [], else: values_specs(values)
768791
769792
quote do
770793
unless Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
771794
@type t :: { __MODULE__, unquote_splicing(types) }
772795
end
773796
774797
unless Kernel.Typespec.defines_type?(__MODULE__, :options, 0) do
775-
@type options :: unquote(options)
798+
@type options :: unquote(options) | [{String.t, unquote(values_specs)}]
776799
end
777800
778801
@spec new :: t
@@ -790,6 +813,11 @@ defmodule Record do
790813
{ :|, [], [{ k, v }, acc] }
791814
end, { k, v }, t
792815
end
816+
defp values_specs([{ _, _, v }|t]) do
817+
:lists.foldl fn { _, _, v }, acc ->
818+
{ :|, [], [v, acc] }
819+
end, v, t
820+
end
793821
794822
defp accessor_specs([{ :__exception__, _, _ }|t], 1, acc) do
795823
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)