Permalink
Browse files

Allow Inspect protocol to be derivable with the only/except options (#…

…8104)

Closes #8098
  • Loading branch information...
fertapric authored and josevalim committed Aug 20, 2018
1 parent 8b7640f commit 10ceadb448ba6a913f3070c3cc51c9c7be1f280b
Showing with 148 additions and 5 deletions.
  1. +69 −5 lib/elixir/lib/inspect.ex
  2. +4 −0 lib/elixir/lib/kernel.ex
  3. +75 −0 lib/elixir/test/elixir/inspect_test.exs
@@ -20,7 +20,7 @@ defprotocol Inspect do
## Examples
Many times, inspecting a structure can be implemented in function
of existing entities. For example, here is `MapSet`'s `inspect`
of existing entities. For example, here is `MapSet`'s `inspect/2`
implementation:
defimpl Inspect, for: MapSet do
@@ -39,21 +39,39 @@ defprotocol Inspect do
other string `">"`.
Since regular strings are valid entities in an algebra document,
an implementation of inspect may simply return a string,
although that will devoid it of any pretty-printing.
an implementation of the `Inspect` protocol may simply return a
string, although that will devoid it of any pretty-printing.
## Error handling
In case there is an error while your structure is being inspected,
Elixir will raise an `ArgumentError` error and will automatically fall back
to a raw representation for printing the structure.
You can however access the underlying error by invoking the Inspect
implementation directly. For example, to test Inspect.MapSet above,
You can however access the underlying error by invoking the `Inspect`
implementation directly. For example, to test `Inspect.MapSet` above,
you can invoke it as:
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
## Deriving
The `Inspect` protocol can be derived to hide certain fields from
structs, so they don't show up in logs, inspects and similar. This
is especially useful for fields containing private information.
The options `:only` and `:except` can be used with `@derive` to
specify which fields should and should not appear in the
algebra document:
defmodule User do
@derive {Inspect, only: [:id, :name]}
defstruct [:id, :name, :address]
end
inspect(%User{id: 1, name: "Homer", address: "742 Evergreen Terrace"})
#=> #User<id: 1, name: "Homer", ...>
"""
# Handle structs in Any
@@ -372,6 +390,39 @@ defimpl Inspect, for: Reference do
end
defimpl Inspect, for: Any do
defmacro __deriving__(module, struct, options) do
fields =
struct
|> Map.drop([:__exception__, :__struct__])
|> Map.keys()
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
inspect_module =
if fields == only and except == [] do
quote(do: Inspect.Map)
else
quote(do: Inspect.Any)
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(struct, opts) do
map = Map.take(struct, unquote(filtered_fields))
colorless_opts = %{opts | syntax_colors: []}
name = Inspect.Atom.inspect(unquote(module), colorless_opts)
unquote(inspect_module).inspect(map, name, opts)
end
end
end
end
def inspect(%module{} = struct, opts) do
try do
module.__struct__
@@ -388,4 +439,17 @@ defimpl Inspect, for: Any do
end
end
end
def inspect(map, name, opts) do
# Use the :limit option and an extra element to force
# `container_doc/6` to append "...".
opts = %{opts | limit: min(opts.limit, map_size(map))}
map = :maps.to_list(map) ++ ["..."]
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
container_doc(open, map, close, opts, &Inspect.List.keyword/2, separator: sep, break: :strict)
end
end
@@ -2005,6 +2005,10 @@ defmodule Kernel do
inspect(fn a, b -> a + b end)
#=> #Function<...>
The `Inspect` protocol can be derived to hide certain fields
from structs, so they don't show up in logs, inspects and similar.
See the "Deriving" section of the documentation of the `Inspect`
protocol for more information.
"""
@spec inspect(Inspect.t(), keyword) :: String.t()
def inspect(term, opts \\ []) when is_list(opts) do
@@ -483,6 +483,81 @@ defmodule Inspect.MapTest do
assert inspect(%{a: 9999}, opts) ==
"\e[32m%{\e[36m" <> "\e[31ma:\e[36m " <> "\e[34m9999\e[36m" <> "\e[32m}\e[36m"
end
defmodule StructWithoutOptions do
@derive Inspect
defstruct [:a, :b, :c, :d]
end
test "struct without options" do
struct = %StructWithoutOptions{a: 1, b: 2, c: 3, d: 4}
assert inspect(struct) == "%Inspect.MapTest.StructWithoutOptions{a: 1, b: 2, c: 3, d: 4}"
assert inspect(struct, pretty: true, width: 1) ==
"%Inspect.MapTest.StructWithoutOptions{\n a: 1,\n b: 2,\n c: 3,\n d: 4\n}"
end
defmodule StructWithOnlyOption do
@derive {Inspect, only: [:b, :c]}
defstruct [:a, :b, :c, :d]
end
test "struct with :only option" do
struct = %StructWithOnlyOption{a: 1, b: 2, c: 3, d: 4}
assert inspect(struct) == "#Inspect.MapTest.StructWithOnlyOption<b: 2, c: 3, ...>"
assert inspect(struct, pretty: true, width: 1) ==
"#Inspect.MapTest.StructWithOnlyOption<\n b: 2,\n c: 3,\n ...\n>"
end
defmodule StructWithEmptyOnlyOption do
@derive {Inspect, only: []}
defstruct [:a, :b, :c, :d]
end
test "struct with empty :only option" do
struct = %StructWithEmptyOnlyOption{a: 1, b: 2, c: 3, d: 4}
assert inspect(struct) == "#Inspect.MapTest.StructWithEmptyOnlyOption<...>"
end
defmodule StructWithAllFieldsInOnlyOption do
@derive {Inspect, only: [:a, :b]}
defstruct [:a, :b]
end
test "struct with all fields in the :only option" do
struct = %StructWithAllFieldsInOnlyOption{a: 1, b: 2}
assert inspect(struct) == "%Inspect.MapTest.StructWithAllFieldsInOnlyOption{a: 1, b: 2}"
assert inspect(struct, pretty: true, width: 1) ==
"%Inspect.MapTest.StructWithAllFieldsInOnlyOption{\n a: 1,\n b: 2\n}"
end
defmodule StructWithExceptOption do
@derive {Inspect, except: [:b, :c]}
defstruct [:a, :b, :c, :d]
end
test "struct with :except option" do
struct = %StructWithExceptOption{a: 1, b: 2, c: 3, d: 4}
assert inspect(struct) == "#Inspect.MapTest.StructWithExceptOption<a: 1, d: 4, ...>"
assert inspect(struct, pretty: true, width: 1) ==
"#Inspect.MapTest.StructWithExceptOption<\n a: 1,\n d: 4,\n ...\n>"
end
defmodule StructWithBothOnlyAndExceptOptions do
@derive {Inspect, only: [:a, :b], except: [:b, :c]}
defstruct [:a, :b, :c, :d]
end
test "struct with both :only and :except options" do
struct = %StructWithBothOnlyAndExceptOptions{a: 1, b: 2, c: 3, d: 4}
assert inspect(struct) == "#Inspect.MapTest.StructWithBothOnlyAndExceptOptions<a: 1, ...>"
assert inspect(struct, pretty: true, width: 1) ==
"#Inspect.MapTest.StructWithBothOnlyAndExceptOptions<\n a: 1,\n ...\n>"
end
end
defmodule Inspect.OthersTest do

0 comments on commit 10ceadb

Please sign in to comment.