Skip to content

Commit

Permalink
Allow Inspect protocol to be derivable with the only/except options (#…
Browse files Browse the repository at this point in the history
…8104)

Closes #8098
  • Loading branch information
fertapric authored and josevalim committed Aug 20, 2018
1 parent 8b7640f commit 10ceadb
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 5 deletions.
74 changes: 69 additions & 5 deletions lib/elixir/lib/inspect.ex
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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__
Expand All @@ -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
4 changes: 4 additions & 0 deletions lib/elixir/lib/kernel.ex
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions lib/elixir/test/elixir/inspect_test.exs
Expand Up @@ -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
Expand Down

0 comments on commit 10ceadb

Please sign in to comment.