Skip to content

Commit e3bad83

Browse files
committed
Move __deriving__ callback to the protocol
1 parent 1f98708 commit e3bad83

File tree

11 files changed

+194
-208
lines changed

11 files changed

+194
-208
lines changed

lib/elixir/lib/inspect.ex

Lines changed: 79 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,85 @@ defprotocol Inspect do
139139
# Handle structs in Any
140140
@fallback_to_any true
141141

142+
@impl true
143+
defmacro __deriving__(module, options) do
144+
info = Macro.struct_info!(module, __CALLER__)
145+
fields = Enum.sort(Enum.map(info, & &1.field) -- [:__exception__, :__struct__])
146+
147+
only = Keyword.get(options, :only, fields)
148+
except = Keyword.get(options, :except, [])
149+
optional = Keyword.get(options, :optional, [])
150+
151+
:ok = validate_option(:only, only, fields, module)
152+
:ok = validate_option(:except, except, fields, module)
153+
:ok = validate_option(:optional, optional, fields, module)
154+
155+
inspect_module =
156+
if fields == Enum.sort(only) and except == [] do
157+
Inspect.Map
158+
else
159+
Inspect.Any
160+
end
161+
162+
filtered_fields =
163+
fields
164+
|> Enum.reject(&(&1 in except))
165+
|> Enum.filter(&(&1 in only))
166+
167+
filtered_guard =
168+
quote do
169+
var!(field) in unquote(filtered_fields)
170+
end
171+
172+
field_guard =
173+
if optional == [] do
174+
filtered_guard
175+
else
176+
optional_map =
177+
for field <- optional, into: %{} do
178+
default = Enum.find(info, %{}, &(&1.field == field)) |> Map.get(:default, nil)
179+
{field, default}
180+
end
181+
182+
quote do
183+
unquote(filtered_guard) and
184+
not case unquote(Macro.escape(optional_map)) do
185+
%{^var!(field) => var!(default)} ->
186+
var!(default) == Map.get(var!(struct), var!(field))
187+
188+
%{} ->
189+
false
190+
end
191+
end
192+
end
193+
194+
quote do
195+
defimpl Inspect, for: unquote(module) do
196+
def inspect(var!(struct), var!(opts)) do
197+
var!(infos) =
198+
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
199+
unquote(field_guard),
200+
do: var!(info)
201+
202+
var!(name) = Macro.inspect_atom(:literal, unquote(module))
203+
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
204+
end
205+
end
206+
end
207+
end
208+
209+
defp validate_option(option, option_list, fields, module) do
210+
case option_list -- fields do
211+
[] ->
212+
:ok
213+
214+
unknown_fields ->
215+
raise ArgumentError,
216+
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
217+
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
218+
end
219+
end
220+
142221
@doc """
143222
Converts `term` into an algebra document.
144223
@@ -548,79 +627,6 @@ defimpl Inspect, for: Reference do
548627
end
549628

550629
defimpl Inspect, for: Any do
551-
defmacro __deriving__(module, struct, options) do
552-
fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__])
553-
554-
only = Keyword.get(options, :only, fields)
555-
except = Keyword.get(options, :except, [])
556-
optional = Keyword.get(options, :optional, [])
557-
558-
:ok = validate_option(:only, only, fields, module)
559-
:ok = validate_option(:except, except, fields, module)
560-
:ok = validate_option(:optional, optional, fields, module)
561-
562-
inspect_module =
563-
if fields == Enum.sort(only) and except == [] do
564-
Inspect.Map
565-
else
566-
Inspect.Any
567-
end
568-
569-
filtered_fields =
570-
fields
571-
|> Enum.reject(&(&1 in except))
572-
|> Enum.filter(&(&1 in only))
573-
574-
filtered_guard =
575-
quote do
576-
var!(field) in unquote(filtered_fields)
577-
end
578-
579-
field_guard =
580-
if optional == [] do
581-
filtered_guard
582-
else
583-
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
584-
585-
quote do
586-
unquote(filtered_guard) and
587-
not case unquote(Macro.escape(optional_map)) do
588-
%{^var!(field) => var!(default)} ->
589-
var!(default) == Map.get(var!(struct), var!(field))
590-
591-
%{} ->
592-
false
593-
end
594-
end
595-
end
596-
597-
quote do
598-
defimpl Inspect, for: unquote(module) do
599-
def inspect(var!(struct), var!(opts)) do
600-
var!(infos) =
601-
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
602-
unquote(field_guard),
603-
do: var!(info)
604-
605-
var!(name) = Macro.inspect_atom(:literal, unquote(module))
606-
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
607-
end
608-
end
609-
end
610-
end
611-
612-
defp validate_option(option, option_list, fields, module) do
613-
case option_list -- fields do
614-
[] ->
615-
:ok
616-
617-
unknown_fields ->
618-
raise ArgumentError,
619-
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
620-
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
621-
end
622-
end
623-
624630
def inspect(%module{} = struct, opts) do
625631
try do
626632
{module.__struct__(), module.__info__(:struct)}

lib/elixir/lib/kernel.ex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5406,12 +5406,12 @@ defmodule Kernel do
54065406
defstruct name: nil, age: nil
54075407
end
54085408
5409-
For each protocol in `@derive`, Elixir will assert the protocol has
5410-
been implemented for `Any`. If the `Any` implementation defines a
5411-
`__deriving__/3` callback, the callback will be invoked and it should define
5412-
the implementation module. Otherwise an implementation that simply points to
5413-
the `Any` implementation is automatically derived. For more information on
5414-
the `__deriving__/3` callback, see `Protocol.derive/3`.
5409+
For each protocol in `@derive`, Elixir will verify if the protocol
5410+
has implemented the `c:Protocol.__deriving__/2` callback. If so,
5411+
the callback will be invoked and it should define the implementation
5412+
module. Otherwise an implementation that simply points to the `Any`
5413+
implementation is automatically derived. For more information, see
5414+
`Protocol.derive/3`.
54155415
54165416
## Enforcing keys
54175417

lib/elixir/lib/kernel/utils.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ defmodule Kernel.Utils do
213213
case enforce_keys -- :maps.keys(struct) do
214214
[] ->
215215
mapper = fn {key, val} ->
216-
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
216+
%{field: key, default: val}
217217
end
218218

219219
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})

lib/elixir/lib/module.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,8 @@ defmodule Module do
704704
@callback __info__(:macros) :: keyword()
705705
@callback __info__(:md5) :: binary()
706706
@callback __info__(:module) :: module()
707-
@callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil
707+
@callback __info__(:struct) ::
708+
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
708709

709710
@doc """
710711
Returns information about module attributes used by Elixir.

lib/elixir/lib/module/types/apply.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ defmodule Module.Types.Apply do
5454
functions: fas,
5555
macros: fas,
5656
struct:
57-
list(closed_map(default: term(), field: atom(), required: boolean()))
57+
list(closed_map(default: if_set(term()), field: atom()))
5858
|> union(atom([nil]))
5959
] ++ shared_info,
6060
__protocol__: [

0 commit comments

Comments
 (0)