Skip to content

Commit a9b2a95

Browse files
committed
Allow records without field default values
1 parent 86bafe8 commit a9b2a95

File tree

4 files changed

+35
-17
lines changed

4 files changed

+35
-17
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3299,15 +3299,11 @@ defmodule Kernel do
32993299
In case a struct does not declare a field type, it defaults to `term`.
33003300
"""
33013301
defmacro defstruct(kv) do
3302-
{fields, types} = Record.Backend.split_fields_and_types(:defstruct, kv)
3302+
{fields, types} = Record.Backend.split_fields_and_types(kv)
33033303

33043304
fields =
33053305
quote bind_quoted: [fields: fields] do
3306-
fields = Enum.map(fields, fn
3307-
{ key, _ } = pair when is_atom(key) -> pair
3308-
key when is_atom(key) -> { key, nil }
3309-
other -> raise ArgumentError, message: "struct fields must be atoms, got: #{inspect other}"
3310-
end)
3306+
fields = Record.Backend.default_fields(:struct, fields)
33113307

33123308
def __struct__() do
33133309
%{unquote_splicing(Macro.escape(fields)), __struct__: __MODULE__}

lib/elixir/lib/record.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ defmodule Record do
157157
defmacro defrecord(name, tag \\ nil, kv) do
158158
quote bind_quoted: [name: name, tag: tag, kv: kv] do
159159
tag = tag || name
160-
fields = Macro.escape(kv)
160+
fields = Macro.escape Record.Backend.default_fields(:record, kv)
161161

162162
defmacro(unquote(name)(args \\ [])) do
163163
Record.Backend.access(unquote(tag), unquote(fields), args, __CALLER__)
@@ -175,7 +175,7 @@ defmodule Record do
175175
defmacro defrecordp(name, tag \\ nil, kv) do
176176
quote bind_quoted: [name: name, tag: tag, kv: kv] do
177177
tag = tag || name
178-
fields = Macro.escape(kv)
178+
fields = Macro.escape Record.Backend.default_fields(:record, kv)
179179

180180
defmacrop(unquote(name)(args \\ [])) do
181181
Record.Backend.access(unquote(tag), unquote(fields), args, __CALLER__)

lib/elixir/lib/record/backend.ex

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,43 @@ defmodule Record.Backend do
22
# Callback functions invoked by defrecord, defrecordp and friends.
33
@moduledoc false
44

5+
@doc """
6+
Normalizes a list of record or struct fields to have default values.
7+
"""
8+
def default_fields(type, fields) do
9+
Enum.map(fields, fn
10+
{ key, _ } = pair when is_atom(key) -> pair
11+
key when is_atom(key) -> { key, nil }
12+
other -> raise ArgumentError, message: "#{type} fields must be atoms, got: #{inspect other}"
13+
end)
14+
end
15+
516
@doc """
617
Splits a keywords list into fields and types.
718
819
This logic is shared by records and structs.
920
"""
10-
def split_fields_and_types(tag, kv) do
21+
def split_fields_and_types(kv) do
1122
if Keyword.keyword?(kv) do
12-
split_fields_and_types(tag, kv, [], [])
23+
split_fields_and_types(kv, [], [])
1324
else
1425
{kv, []}
1526
end
1627
end
1728

18-
defp split_fields_and_types(tag, [{field, {:::, _, [default, type]}}|t], fields, types) do
19-
split_fields_and_types(tag, t, [{field, default}|fields], [{field, type}|types])
29+
defp split_fields_and_types([{field, {:::, _, [default, type]}}|t], fields, types) do
30+
split_fields_and_types(t, [{field, default}|fields], [{field, type}|types])
2031
end
2132

22-
defp split_fields_and_types(tag, [{field, default}|t], fields, types) do
23-
split_fields_and_types(tag, t, [{field, default}|fields], [{field, quote(do: term)}|types])
33+
defp split_fields_and_types([{field, default}|t], fields, types) do
34+
split_fields_and_types(t, [{field, default}|fields], [{field, quote(do: term)}|types])
2435
end
2536

26-
defp split_fields_and_types(tag, [field|t], fields, types) do
27-
split_fields_and_types(tag, t, [{field, nil}|fields], [{field, quote(do: term)}|types])
37+
defp split_fields_and_types([field|t], fields, types) do
38+
split_fields_and_types(t, [field|fields], [{field, quote(do: term)}|types])
2839
end
2940

30-
defp split_fields_and_types(_tag, [], fields, types) do
41+
defp split_fields_and_types([], fields, types) do
3142
{:lists.reverse(fields), :lists.reverse(types)}
3243
end
3344

lib/elixir/test/elixir/record_test.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ defmodule RecordTest do
5050
refute record?(13)
5151
end
5252

53+
Record.defrecord :timestamp, [:date, :time]
5354
Record.defrecord :user, __MODULE__, name: "José", age: 25
5455
Record.defrecordp :file_info, Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
5556

@@ -81,4 +82,14 @@ defmodule RecordTest do
8182
assert macro_exported?(__MODULE__, :user, 0)
8283
refute macro_exported?(__MODULE__, :file_info, 1)
8384
end
85+
86+
test "records with no defaults" do
87+
record = timestamp()
88+
assert timestamp(record, :date) == nil
89+
assert timestamp(record, :time) == nil
90+
91+
record = timestamp(date: :foo, time: :bar)
92+
assert timestamp(record, :date) == :foo
93+
assert timestamp(record, :time) == :bar
94+
end
8495
end

0 commit comments

Comments
 (0)