From f1d6e7c046d460125411dbd67a817a099009ecbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jun 2022 23:42:11 +0200 Subject: [PATCH 01/15] Add :order and :optional for deriving Inspect --- lib/elixir/lib/inspect.ex | 202 ++++++++++++++++++------ lib/elixir/test/elixir/inspect_test.exs | 36 ++++- 2 files changed, 185 insertions(+), 53 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 6407ac6adbf..8b7bd6eea61 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -8,16 +8,86 @@ defprotocol Inspect do The `Inspect` protocol converts an Elixir data structure into an algebra document. + This is typically done when you want to customize how your own + structs are inspected in logs and the terminal. + This documentation refers to implementing the `Inspect` protocol for your own data structures. To learn more about using inspect, see `Kernel.inspect/2` and `IO.inspect/2`. - The `inspect/2` function receives the entity to be inspected - followed by the inspecting options, represented by the struct - `Inspect.Opts`. Building of the algebra document is done with - `Inspect.Algebra`. + ## Inspect representation + + There are typically three choices of inspect representation. In order + to understand them, let's imagine we have the following `User` struct: + + defmodule User do + defstruct [:id, :name, :address] + end + + Our choices are: + + 1. Print the struct using Elixir's struct syntax, for example: + `%User{address: "Earth", id: 13, name: "Jane"}`. This is the + best choice if all struct fields are public. + + 2. Print using the `#User<...>` notation, for example: `#User`. + This notation does not emit valid Elixir code and is typically + used when the struct has private fields (for example, you may want + to hide the field `:address` to redact person identifiable information). + + 3. Print the struct using the expression syntax, for example: + `User.new(13, "Jane", "Earth")`. This assumes there is a `User.new/3` + function. This option is mostly used as an alternative to option 2 + for representing custom data structures, such as `MapSet`, `Date.Range`, + and others. + + You can implement the Inspect protocol for your own structs while + adhering to the conventions above. However, options 1 and 2 can be + quickly achieved by deriving the `Inspect` protocol. For option 3, + you need your custom implementation. + + ## Deriving + + The `Inspect` protocol can be derived to customize the order of fields + (the default is alphabetical) and hide certain fields from structs, + so they don't show up in logs, inspects and similar. The latter is + especially useful for fields containing private information. + + There are four possible options: + + * `:only` - only include the given fields when inspecting. + + * `:except` - remove the given fields when inspecting. + + * `:order` - (since v1.14.0) include fields in the given order. + Non-listed fields come after the listed ones. + + * `:optional` - (since v1.14.0) do not include a field if it + matches its default value. + + Whenever `:only` or `:except` are used to restrict fields, + the struct will be printed using the `#User<...>` notation, + as the struct can no longer be copy and pasted as valid Elixir + code. Let's see an example: + + defmodule User do + @derive {Inspect, only: [:id, :name]} + defstruct [:id, :name, :address] + end + + inspect(%User{id: 1, name: "Jane", address: "Earth"}) + #=> #User + + If you use only the options `:order` and `:optional`, the struct + will still be printed as `%User{...}`. - ## Examples + ## Custom implementation + + You can also define your custom protocol implementation by + defining the `inspect/2` function. The function receives the + entity to be inspected followed by the inspecting options, + represented by the struct `Inspect.Opts`. Building of the + algebra document is done with `Inspect.Algebra`. Many times, inspecting a structure can be implemented in function of existing entities. For example, here is `MapSet`'s `inspect/2` @@ -27,23 +97,24 @@ defprotocol Inspect do import Inspect.Algebra def inspect(map_set, opts) do - concat(["#MapSet<", to_doc(MapSet.to_list(map_set), opts), ">"]) + concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"]) end end The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from `Inspect.Algebra` and it concatenates algebra documents together. - In the example above it is concatenating the string `"#MapSet<"`, + In the example above it is concatenating the string `"MapSet.new("`, the document returned by `Inspect.Algebra.to_doc/2`, and the final - string `">"`. We prefix the module name `#` to denote the inspect - presentation is not actually valid Elixir syntax. + string `")"`. Therefore, the MapSet with the numbers 1, 2, and 3 + will be printed as: + + iex> MapSet.new([1, 2, 3], fn x -> x * 2 end) + MapSet.new([2, 4, 6]) - Finally, note strings themselves are valid algebra documents that - keep their formatting when pretty printed. This means your `Inspect` - implementation may simply return a string, although that will devoid - it of any pretty-printing. + In other words, `MapSet`'s inspect representation returns an expression + that, when evaluated, builds the `MapSet` itself. - ## Error handling + ### 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 @@ -55,24 +126,6 @@ defprotocol Inspect do 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 - """ # Handle structs in Any @@ -250,15 +303,18 @@ end defimpl Inspect, for: Map do def inspect(map, opts) do - inspect(map, "", opts) + inspect(Map.to_list(map), "", opts) end - def inspect(map, name, opts) do - map = Map.to_list(map) + def inspect(list, name, opts) do open = color("%" <> name <> "{", :map, opts) sep = color(",", :map, opts) close = color("}", :map, opts) - container_doc(open, map, close, opts, traverse_fun(map, opts), separator: sep, break: :strict) + + container_doc(open, list, close, opts, traverse_fun(list, opts), + separator: sep, + break: :strict + ) end defp traverse_fun(list, opts) do @@ -451,44 +507,89 @@ end defimpl Inspect, for: Any do defmacro __deriving__(module, struct, options) do - fields = Map.keys(struct) -- [:__exception__, :__struct__] + fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__]) only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) + optional = Keyword.get(options, :optional, []) + order = Keyword.get(options, :order, []) - :ok = validate_option(only, fields, module) - :ok = validate_option(except, fields, module) + :ok = validate_option(:only, only, fields, module) + :ok = validate_option(:except, except, fields, module) + :ok = validate_option(:optional, optional, fields, module) + :ok = validate_option(:order, order, fields, module) + + inspect_module = + if fields == only and except == [] do + Inspect.Map + else + Inspect.Any + end filtered_fields = fields |> Enum.reject(&(&1 in except)) |> Enum.filter(&(&1 in only)) - inspect_module = - if fields == only and except == [] do - Inspect.Map + ordered_fields = order ++ (filtered_fields -- order) + + mapper = + if optional == [] do + # If there are no optional fields, we will always include all fields, + # so a map operation suffices. + quote do + Enum.map(unquote(ordered_fields), fn var!(key) -> + case var!(struct) do + %{^var!(key) => var!(value)} -> {var!(key), var!(value)} + %{} -> {var!(key), nil} + end + end) + end else - Inspect.Any + optional_fields = + Enum.map(ordered_fields, fn field -> + if field in optional, do: {field, Map.fetch!(struct, field)}, else: field + end) + + # If there are optional fields, then we need to keep the order + # but potentially skip fields that match to their default value. + quote do + Enum.flat_map(unquote(Macro.escape(optional_fields)), fn + {var!(key), var!(default)} -> + case var!(struct) do + %{^var!(key) => ^var!(default)} -> [] + %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] + %{} -> [{var!(key), nil}] + end + + var!(key) -> + case var!(struct) do + %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] + %{} -> [{var!(key), nil}] + end + end) + end end quote do defimpl Inspect, for: unquote(module) do def inspect(var!(struct), var!(opts)) do - var!(map) = Map.take(var!(struct), unquote(filtered_fields)) + var!(list) = unquote(mapper) var!(name) = Macro.inspect_atom(:literal, unquote(module)) - unquote(inspect_module).inspect(var!(map), var!(name), var!(opts)) + unquote(inspect_module).inspect(var!(list), var!(name), var!(opts)) end end end end - defp validate_option(option_list, fields, module) do + defp validate_option(option, option_list, fields, module) do case option_list -- fields do [] -> :ok unknown_fields -> raise ArgumentError, - "unknown fields #{Kernel.inspect(unknown_fields)} given when deriving the Inspect protocol for #{Kernel.inspect(module)}. :only and :except values must match struct fields" + "unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <> + "when deriving the Inspect protocol for #{Kernel.inspect(module)}" end end @@ -501,15 +602,14 @@ defimpl Inspect, for: Any do dunder -> if Map.keys(dunder) == Map.keys(struct) do pruned = Map.drop(struct, [:__struct__, :__exception__]) - Inspect.Map.inspect(pruned, Macro.inspect_atom(:literal, module), opts) + Inspect.Map.inspect(Map.to_list(pruned), Macro.inspect_atom(:literal, module), opts) else Inspect.Map.inspect(struct, opts) end end end - def inspect(map, name, opts) do - map = Map.to_list(map) ++ [:...] + def inspect(list, name, opts) do open = color("#" <> name <> "<", :map, opts) sep = color(",", :map, opts) close = color(">", :map, opts) @@ -519,7 +619,7 @@ defimpl Inspect, for: Any do :..., _opts -> "..." end - container_doc(open, map, close, opts, fun, separator: sep, break: :strict) + container_doc(open, list ++ [:...], close, opts, fun, separator: sep, break: :strict) end end diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 860da0e76b7..925dcc783be 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -696,7 +696,7 @@ defmodule Inspect.MapTest do test "struct missing fields in the :only option" do assert_raise ArgumentError, - "unknown fields [:c] given when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInOnlyOption. :only and :except values must match struct fields", + "unknown fields [:c] in :only when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInOnlyOption", fn -> defmodule StructMissingFieldsInOnlyOption do @derive {Inspect, only: [:c]} @@ -707,7 +707,7 @@ defmodule Inspect.MapTest do test "struct missing fields in the :except option" do assert_raise ArgumentError, - "unknown fields [:c, :d] given when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInExceptOption. :only and :except values must match struct fields", + "unknown fields [:c, :d] in :except when deriving the Inspect protocol for Inspect.MapTest.StructMissingFieldsInExceptOption", fn -> defmodule StructMissingFieldsInExceptOption do @derive {Inspect, except: [:c, :d]} @@ -741,6 +741,38 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "#Inspect.MapTest.StructWithBothOnlyAndExceptOptions<\n a: 1,\n ...\n>" end + + defmodule StructWithOptionalAndOrder do + @derive {Inspect, order: [:c, :d], optional: [:b, :c]} + defstruct [:a, :b, :c, :d] + end + + test "struct with both :order and :optional options" do + struct = %StructWithOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} + + assert inspect(struct) == + "%Inspect.MapTest.StructWithOptionalAndOrder{c: 3, d: 4, a: 1, b: 2}" + + struct = %StructWithOptionalAndOrder{} + assert inspect(struct) == "%Inspect.MapTest.StructWithOptionalAndOrder{d: nil, a: nil}" + end + + defmodule StructWithExceptOptionalAndOrder do + @derive {Inspect, order: [:c, :d], optional: [:b, :c], except: [:e]} + defstruct [:a, :b, :c, :d, :e] + end + + test "struct with :except, :order, and :optional options" do + struct = %StructWithExceptOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} + + assert inspect(struct) == + "#Inspect.MapTest.StructWithExceptOptionalAndOrder" + + struct = %StructWithExceptOptionalAndOrder{} + + assert inspect(struct) == + "#Inspect.MapTest.StructWithExceptOptionalAndOrder" + end end defmodule Inspect.OthersTest do From 872dd89af60a31f1de6e653ff1bab138549ea46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jun 2022 23:56:27 +0200 Subject: [PATCH 02/15] Use new inspect for URI --- lib/elixir/lib/uri.ex | 126 +++++----------------- lib/elixir/test/elixir/exception_test.exs | 4 +- 2 files changed, 26 insertions(+), 104 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index 5036e5ac049..9c70dfcd4dc 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -10,14 +10,9 @@ defmodule URI do be aware that the `authority` field is deprecated and should not be populated. """ - defstruct scheme: nil, - path: nil, - query: nil, - fragment: nil, - authority: nil, - userinfo: nil, - host: nil, - port: nil + fields = [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] + @derive {Inspect, order: fields, optional: fields} + defstruct fields @type t :: %__MODULE__{ authority: authority, @@ -495,68 +490,39 @@ defmodule URI do iex> URI.new("https://elixir-lang.org/") {:ok, %URI{ - fragment: nil, + scheme: "https", host: "elixir-lang.org", - path: "/", port: 443, - query: nil, - scheme: "https", - userinfo: nil + path: "/" }} iex> URI.new("//elixir-lang.org/") {:ok, %URI{ - fragment: nil, host: "elixir-lang.org", - path: "/", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "/" }} iex> URI.new("/foo/bar") {:ok, %URI{ - fragment: nil, - host: nil, - path: "/foo/bar", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "/foo/bar" }} iex> URI.new("foo/bar") {:ok, %URI{ - fragment: nil, - host: nil, - path: "foo/bar", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "foo/bar" }} iex> URI.new("//[fe80::]/") {:ok, %URI{ - fragment: nil, host: "fe80::", - path: "/", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "/" }} iex> URI.new("https:?query") {:ok, %URI{ - fragment: nil, - host: nil, - path: nil, - port: 443, - query: "query", scheme: "https", - userinfo: nil + port: 443, + query: "query" }} iex> URI.new("/invalid_greater_than_in_path/>") @@ -567,13 +533,10 @@ defmodule URI do iex> {:ok, uri} = URI.new("https://elixir-lang.org/") iex> URI.new(uri) {:ok, %URI{ - fragment: nil, + scheme: "https", host: "elixir-lang.org", - path: "/", port: 443, - query: nil, - scheme: "https", - userinfo: nil + path: "/" }} """ @doc since: "1.13.0" @@ -594,13 +557,10 @@ defmodule URI do iex> URI.new!("https://elixir-lang.org/") %URI{ - fragment: nil, + scheme: "https", host: "elixir-lang.org", - path: "/", port: 443, - query: nil, - scheme: "https", - userinfo: nil + path: "/" } iex> URI.new!("/invalid_greater_than_in_path/>") @@ -611,13 +571,10 @@ defmodule URI do iex> uri = URI.new!("https://elixir-lang.org/") iex> URI.new!(uri) %URI{ - fragment: nil, + scheme: "https", host: "elixir-lang.org", - path: "/", port: 443, - query: nil, - scheme: "https", - userinfo: nil + path: "/" } """ @doc since: "1.13.0" @@ -683,50 +640,28 @@ defmodule URI do iex> URI.parse("https://elixir-lang.org/") %URI{ + scheme: "https", authority: "elixir-lang.org", - fragment: nil, host: "elixir-lang.org", path: "/", - port: 443, - query: nil, - scheme: "https", - userinfo: nil + port: 443 } iex> URI.parse("//elixir-lang.org/") %URI{ authority: "elixir-lang.org", - fragment: nil, host: "elixir-lang.org", - path: "/", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "/" } iex> URI.parse("/foo/bar") %URI{ - authority: nil, - fragment: nil, - host: nil, - path: "/foo/bar", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "/foo/bar" } iex> URI.parse("foo/bar") %URI{ - authority: nil, - fragment: nil, - host: nil, - path: "foo/bar", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "foo/bar" } In contrast to `URI.new/1`, this function will parse poorly-formed @@ -734,14 +669,7 @@ defmodule URI do iex> URI.parse("/invalid_greater_than_in_path/>") %URI{ - authority: nil, - fragment: nil, - host: nil, - path: "/invalid_greater_than_in_path/>", - port: nil, - query: nil, - scheme: nil, - userinfo: nil + path: "/invalid_greater_than_in_path/>" } Another example is a URI with brackets in query strings. It is accepted @@ -750,14 +678,8 @@ defmodule URI do iex> URI.parse("/?foo[bar]=baz") %URI{ - authority: nil, - fragment: nil, - host: nil, path: "/", - port: nil, - query: "foo[bar]=baz", - scheme: nil, - userinfo: nil + query: "foo[bar]=baz" } """ diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index e9b97c1f3d4..7b6ff9d32c4 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -453,7 +453,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/2: # 1 - %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} + %URI{scheme: \"https\", authority: \"elixir-lang.org\", host: \"elixir-lang.org\", port: 443} # 2 URI @@ -469,7 +469,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/1: # 1 - %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} + %URI{scheme: \"https\", authority: \"elixir-lang.org\", host: \"elixir-lang.org\", port: 443} Attempted function clauses (showing 1 out of 1): From b8ef14b1132c59d6616d5f399bae4d9415343162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 00:01:10 +0200 Subject: [PATCH 03/15] Use struct inspect for Version --- CHANGELOG.md | 3 ++- lib/elixir/lib/version.ex | 14 ++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4166b00085..4cbbb4240ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,7 +108,8 @@ TODO. * [Enum] Allow slicing with steps in `Enum.slice/2` * [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero * [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol - * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, `Version`, and `Version.Requirement` + * [Inspect] Allow `:order` and `:optional` when deriving the Inspect protocol + * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement` * [Kernel] Allow any guard expression as the size of a bitstring in a pattern match * [Kernel] Allow composite types with pins as the map key in a pattern match * [Kernel] Print escaped version of control chars when they show up as unexpected tokens diff --git a/lib/elixir/lib/version.ex b/lib/elixir/lib/version.ex index fb869959fee..6f1c60dc091 100644 --- a/lib/elixir/lib/version.ex +++ b/lib/elixir/lib/version.ex @@ -100,6 +100,7 @@ defmodule Version do import Kernel, except: [match?: 2] @enforce_keys [:major, :minor, :patch] + @derive {Inspect, order: [:major, :minor, :patch, :pre, :build], optional: [:pre, :build]} defstruct [:major, :minor, :patch, :build, pre: []] @type version :: String.t() | t @@ -353,9 +354,8 @@ defmodule Version do ## Examples - iex> {:ok, version} = Version.parse("2.0.1-alpha1") - iex> version - Version.parse!("2.0.1-alpha1") + iex> Version.parse("2.0.1-alpha1") + {:ok, %Version{major: 2, minor: 0, patch: 1, pre: ["alpha1"]}} iex> Version.parse("2.0-alpha1") :error @@ -382,7 +382,7 @@ defmodule Version do ## Examples iex> Version.parse!("2.0.1-alpha1") - Version.parse!("2.0.1-alpha1") + %Version{major: 2, minor: 0, patch: 1, pre: ["alpha1"]} iex> Version.parse!("2.0-alpha1") ** (Version.InvalidVersionError) invalid version: "2.0-alpha1" @@ -664,12 +664,6 @@ defimpl String.Chars, for: Version do defdelegate to_string(version), to: Version end -defimpl Inspect, for: Version do - def inspect(self, _opts) do - "Version.parse!(\"" <> Version.to_string(self) <> "\")" - end -end - defimpl String.Chars, for: Version.Requirement do def to_string(%Version.Requirement{source: source}) do source From 56c76d3115b2bc15931892aa01eee5053b6db594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 06:16:18 +0200 Subject: [PATCH 04/15] Do not show struct fields with default value by default --- lib/elixir/lib/inspect.ex | 132 +++++++++---------- lib/elixir/lib/inspect/algebra.ex | 18 ++- lib/elixir/lib/uri.ex | 2 +- lib/elixir/lib/version.ex | 4 +- lib/elixir/test/elixir/inspect_test.exs | 42 ++---- lib/elixir/test/elixir/string/chars_test.exs | 2 +- lib/ex_unit/test/ex_unit/formatter_test.exs | 2 +- lib/iex/test/iex/info_test.exs | 12 +- 8 files changed, 88 insertions(+), 126 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 8b7bd6eea61..168853eb8dc 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -28,7 +28,8 @@ defprotocol Inspect do 1. Print the struct using Elixir's struct syntax, for example: `%User{address: "Earth", id: 13, name: "Jane"}`. This is the - best choice if all struct fields are public. + default representation and best choice if all struct fields + are public. 2. Print using the `#User<...>` notation, for example: `#User`. This notation does not emit valid Elixir code and is typically @@ -42,9 +43,9 @@ defprotocol Inspect do and others. You can implement the Inspect protocol for your own structs while - adhering to the conventions above. However, options 1 and 2 can be - quickly achieved by deriving the `Inspect` protocol. For option 3, - you need your custom implementation. + adhering to the conventions above. Option 1 is the default representation + and you can quickly achieve option 2 by deriving the `Inspect` protocol. + For option 3, you need your custom implementation. ## Deriving @@ -62,9 +63,6 @@ defprotocol Inspect do * `:order` - (since v1.14.0) include fields in the given order. Non-listed fields come after the listed ones. - * `:optional` - (since v1.14.0) do not include a field if it - matches its default value. - Whenever `:only` or `:except` are used to restrict fields, the struct will be printed using the `#User<...>` notation, as the struct can no longer be copy and pasted as valid Elixir @@ -78,8 +76,8 @@ defprotocol Inspect do inspect(%User{id: 1, name: "Jane", address: "Earth"}) #=> #User - If you use only the options `:order` and `:optional`, the struct - will still be printed as `%User{...}`. + If you use only the `:order` option, the struct will still be + printed as `%User{...}`. ## Custom implementation @@ -303,31 +301,39 @@ end defimpl Inspect, for: Map do def inspect(map, opts) do - inspect(Map.to_list(map), "", opts) - end + list = Map.to_list(map) - def inspect(list, name, opts) do - open = color("%" <> name <> "{", :map, opts) - sep = color(",", :map, opts) - close = color("}", :map, opts) + fun = + if Inspect.List.keyword?(list) do + &Inspect.List.keyword/2 + else + sep = color(" => ", :map, opts) + &to_map(&1, &2, sep) + end - container_doc(open, list, close, opts, traverse_fun(list, opts), - separator: sep, - break: :strict - ) + map_container_doc("%{", list, opts, fun) end - defp traverse_fun(list, opts) do - if Inspect.List.keyword?(list) do - &Inspect.List.keyword/2 - else - sep = color(" => ", :map, opts) - &to_map(&1, &2, sep) + defp to_map({key, value}, opts, sep) do + concat(concat(to_doc(key, opts), sep), to_doc(value, opts)) + end + + def inspect(list, name, struct, opts) do + fun = fn {key, value}, opts -> + case struct do + %{^key => ^value} -> empty() + %{} -> Inspect.List.keyword({key, value}, opts) + end end + + map_container_doc("%" <> name <> "{", list, opts, fun) end - defp to_map({key, value}, opts, sep) do - concat(concat(to_doc(key, opts), sep), to_doc(value, opts)) + defp map_container_doc(open, list, opts, fun) do + open = color(open, :map, opts) + sep = color(",", :map, opts) + close = color("}", :map, opts) + container_doc(open, list, close, opts, fun, separator: sep, break: :strict) end end @@ -510,12 +516,10 @@ defimpl Inspect, for: Any do fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__]) only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) - optional = Keyword.get(options, :optional, []) order = Keyword.get(options, :order, []) :ok = validate_option(:only, only, fields, module) :ok = validate_option(:except, except, fields, module) - :ok = validate_option(:optional, optional, fields, module) :ok = validate_option(:order, order, fields, module) inspect_module = @@ -532,50 +536,25 @@ defimpl Inspect, for: Any do ordered_fields = order ++ (filtered_fields -- order) - mapper = - if optional == [] do - # If there are no optional fields, we will always include all fields, - # so a map operation suffices. - quote do - Enum.map(unquote(ordered_fields), fn var!(key) -> - case var!(struct) do - %{^var!(key) => var!(value)} -> {var!(key), var!(value)} - %{} -> {var!(key), nil} - end - end) - end - else - optional_fields = - Enum.map(ordered_fields, fn field -> - if field in optional, do: {field, Map.fetch!(struct, field)}, else: field - end) - - # If there are optional fields, then we need to keep the order - # but potentially skip fields that match to their default value. - quote do - Enum.flat_map(unquote(Macro.escape(optional_fields)), fn - {var!(key), var!(default)} -> - case var!(struct) do - %{^var!(key) => ^var!(default)} -> [] - %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] - %{} -> [{var!(key), nil}] - end - - var!(key) -> - case var!(struct) do - %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] - %{} -> [{var!(key), nil}] - end - end) - end - end - quote do defimpl Inspect, for: unquote(module) do def inspect(var!(struct), var!(opts)) do - var!(list) = unquote(mapper) + var!(list) = + Enum.map(unquote(ordered_fields), fn var!(key) -> + case var!(struct) do + %{^var!(key) => var!(value)} -> {var!(key), var!(value)} + %{} -> {var!(key), nil} + end + end) + var!(name) = Macro.inspect_atom(:literal, unquote(module)) - unquote(inspect_module).inspect(var!(list), var!(name), var!(opts)) + + unquote(inspect_module).inspect( + var!(list), + var!(name), + unquote(module).__struct__(), + var!(opts) + ) end end end @@ -602,21 +581,28 @@ defimpl Inspect, for: Any do dunder -> if Map.keys(dunder) == Map.keys(struct) do pruned = Map.drop(struct, [:__struct__, :__exception__]) - Inspect.Map.inspect(Map.to_list(pruned), Macro.inspect_atom(:literal, module), opts) + name = Macro.inspect_atom(:literal, module) + Inspect.Map.inspect(Map.to_list(pruned), name, dunder, opts) else Inspect.Map.inspect(struct, opts) end end end - def inspect(list, name, opts) do + def inspect(list, name, struct, opts) do open = color("#" <> name <> "<", :map, opts) sep = color(",", :map, opts) close = color(">", :map, opts) fun = fn - {key, value}, opts -> Inspect.List.keyword({key, value}, opts) - :..., _opts -> "..." + {key, value}, opts -> + case struct do + %{^key => ^value} -> empty() + %{} -> Inspect.List.keyword({key, value}, opts) + end + + :..., _opts -> + "..." end container_doc(open, list ++ [:...], close, opts, fun, separator: sep, break: :strict) diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index 7bc6c939e4a..934a5eedcf0 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -403,9 +403,12 @@ defmodule Inspect.Algebra do The limit in the given `inspect_opts` is respected and when reached this function stops processing and outputs `"..."` instead. + Empty algebra documents are not included in the output. + ## Options * `:separator` - the separator used between each doc + * `:break` - If `:strict`, always break between each element. If `:flex`, breaks only when necessary. If `:maybe`, chooses `:flex` only if all elements are text-based, otherwise is `:strict` @@ -469,9 +472,12 @@ defmodule Inspect.Algebra do defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) and is_limit(limit) do - limit = decrement(limit) - doc = fun.(term, %{opts | limit: limit}) - container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc)) + new_limit = decrement(limit) + + case fun.(term, %{opts | limit: new_limit}) do + :doc_nil -> container_each(terms, limit, opts, fun, acc, simple?) + doc -> container_each(terms, new_limit, opts, fun, [doc | acc], simple? and simple?(doc)) + end end defp container_each([left | right], limit, opts, fun, acc, simple?) when is_limit(limit) do @@ -480,8 +486,10 @@ defmodule Inspect.Algebra do right = fun.(right, %{opts | limit: limit}) simple? = simple? and simple?(left) and simple?(right) - doc = join(left, right, simple?, @tail_separator) - {:lists.reverse([doc | acc]), simple?} + case join(left, right, simple?, @tail_separator) do + :doc_nil -> {:lists.reverse(acc), simple?} + doc -> {:lists.reverse([doc | acc]), simple?} + end end defp decrement(:infinity), do: :infinity diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index 9c70dfcd4dc..ab1a48e4406 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -11,7 +11,7 @@ defmodule URI do """ fields = [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] - @derive {Inspect, order: fields, optional: fields} + @derive {Inspect, order: fields} defstruct fields @type t :: %__MODULE__{ diff --git a/lib/elixir/lib/version.ex b/lib/elixir/lib/version.ex index 6f1c60dc091..6ff74c14528 100644 --- a/lib/elixir/lib/version.ex +++ b/lib/elixir/lib/version.ex @@ -100,8 +100,8 @@ defmodule Version do import Kernel, except: [match?: 2] @enforce_keys [:major, :minor, :patch] - @derive {Inspect, order: [:major, :minor, :patch, :pre, :build], optional: [:pre, :build]} - defstruct [:major, :minor, :patch, :build, pre: []] + @derive {Inspect, order: [:major, :minor, :patch, :pre, :build]} + defstruct [:major, :minor, :patch, pre: [], build: nil] @type version :: String.t() | t @type requirement :: String.t() | Version.Requirement.t() diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 925dcc783be..a30c5de869a 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -623,8 +623,8 @@ defmodule Inspect.MapTest do end test "exception" do - assert inspect(%RuntimeError{message: "runtime error"}) == - "%RuntimeError{message: \"runtime error\"}" + assert inspect(%RuntimeError{}) == "%RuntimeError{}" + assert inspect(%RuntimeError{message: "another"}) == "%RuntimeError{message: \"another\"}" end test "colors" do @@ -667,6 +667,9 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "#Inspect.MapTest.StructWithOnlyOption<\n b: 2,\n c: 3,\n ...\n>" + struct = %StructWithOnlyOption{a: 1, b: 2} + assert inspect(struct) == "#Inspect.MapTest.StructWithOnlyOption" + struct = %{struct | c: [1, 2, 3, 4]} assert inspect(struct) == "#Inspect.MapTest.StructWithOnlyOption" end @@ -692,6 +695,9 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "%Inspect.MapTest.StructWithAllFieldsInOnlyOption{\n a: 1,\n b: 2\n}" + + struct = %StructWithAllFieldsInOnlyOption{a: 1} + assert inspect(struct) == "%Inspect.MapTest.StructWithAllFieldsInOnlyOption{a: 1}" end test "struct missing fields in the :only option" do @@ -741,38 +747,6 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "#Inspect.MapTest.StructWithBothOnlyAndExceptOptions<\n a: 1,\n ...\n>" end - - defmodule StructWithOptionalAndOrder do - @derive {Inspect, order: [:c, :d], optional: [:b, :c]} - defstruct [:a, :b, :c, :d] - end - - test "struct with both :order and :optional options" do - struct = %StructWithOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} - - assert inspect(struct) == - "%Inspect.MapTest.StructWithOptionalAndOrder{c: 3, d: 4, a: 1, b: 2}" - - struct = %StructWithOptionalAndOrder{} - assert inspect(struct) == "%Inspect.MapTest.StructWithOptionalAndOrder{d: nil, a: nil}" - end - - defmodule StructWithExceptOptionalAndOrder do - @derive {Inspect, order: [:c, :d], optional: [:b, :c], except: [:e]} - defstruct [:a, :b, :c, :d, :e] - end - - test "struct with :except, :order, and :optional options" do - struct = %StructWithExceptOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} - - assert inspect(struct) == - "#Inspect.MapTest.StructWithExceptOptionalAndOrder" - - struct = %StructWithExceptOptionalAndOrder{} - - assert inspect(struct) == - "#Inspect.MapTest.StructWithExceptOptionalAndOrder" - end end defmodule Inspect.OthersTest do diff --git a/lib/elixir/test/elixir/string/chars_test.exs b/lib/elixir/test/elixir/string/chars_test.exs index 02a397a485c..0fa3f2e1252 100644 --- a/lib/elixir/test/elixir/string/chars_test.exs +++ b/lib/elixir/test/elixir/string/chars_test.exs @@ -155,7 +155,7 @@ defmodule String.Chars.ErrorsTest do test "user-defined struct" do message = - "protocol String\.Chars not implemented for %String.Chars.ErrorsTest.Foo{foo: \"bar\"} of type String.Chars.ErrorsTest.Foo (a struct)" + "protocol String\.Chars not implemented for %String.Chars.ErrorsTest.Foo{} of type String.Chars.ErrorsTest.Foo (a struct)" assert_raise Protocol.UndefinedError, message, fn -> to_string(%Foo{}) diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index 9ef4cf53e72..1514e21e80c 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -483,7 +483,7 @@ defmodule ExUnit.FormatterTest do message = "got RuntimeError with message \"oops\" while retrieving Exception.message/1 " <> - "for %ExUnit.FormatterTest.BadMessage{key: 0}. Stacktrace:" + "for %ExUnit.FormatterTest.BadMessage{}. Stacktrace:" assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """ 1) world (Hello) diff --git a/lib/iex/test/iex/info_test.exs b/lib/iex/test/iex/info_test.exs index b68c7fe6a7e..aa589fefe23 100644 --- a/lib/iex/test/iex/info_test.exs +++ b/lib/iex/test/iex/info_test.exs @@ -168,10 +168,7 @@ defmodule IEx.InfoTest do {:ok, date} = Date.new(2017, 1, 1) info = Info.info(date) assert get_key(info, "Data type") == "Date" - - assert get_key(info, "Raw representation") == - "%Date{calendar: Calendar.ISO, day: 1, month: 1, year: 2017}" - + assert get_key(info, "Raw representation") == "%Date{day: 1, month: 1, year: 2017}" assert get_key(info, "Reference modules") == "Date, Calendar, Map" assert get_key(info, "Description") =~ "a date" assert get_key(info, "Description") =~ "`~D`" @@ -181,10 +178,7 @@ defmodule IEx.InfoTest do {:ok, time} = Time.new(23, 59, 59) info = Info.info(time) assert get_key(info, "Data type") == "Time" - - assert get_key(info, "Raw representation") == - "%Time{calendar: Calendar.ISO, hour: 23, microsecond: {0, 0}, minute: 59, second: 59}" - + assert get_key(info, "Raw representation") == "%Time{hour: 23, minute: 59, second: 59}" assert get_key(info, "Reference modules") == "Time, Calendar, Map" assert get_key(info, "Description") =~ "a time" assert get_key(info, "Description") =~ "`~T`" @@ -196,7 +190,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "NaiveDateTime" assert get_key(info, "Raw representation") == - "%NaiveDateTime{calendar: Calendar.ISO, day: 1, hour: 23, microsecond: {0, 0}, minute: 59, month: 1, second: 59, year: 2017}" + "%NaiveDateTime{day: 1, hour: 23, minute: 59, month: 1, second: 59, year: 2017}" assert get_key(info, "Reference modules") == "NaiveDateTime, Calendar, Map" From 7a71515107ba52e37b420c3fe2869c950105fe8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 06:19:27 +0200 Subject: [PATCH 05/15] doc fix --- lib/elixir/lib/inspect.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 168853eb8dc..015acdd50be 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -54,7 +54,7 @@ defprotocol Inspect do so they don't show up in logs, inspects and similar. The latter is especially useful for fields containing private information. - There are four possible options: + The supported options are: * `:only` - only include the given fields when inspecting. From 5c90a01f43675a23b4db610d4ee8158bc14f1816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 06:25:01 +0200 Subject: [PATCH 06/15] algebra rollback --- lib/elixir/lib/inspect/algebra.ex | 14 +++++--------- lib/elixir/test/elixir/inspect/algebra_test.exs | 8 ++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index 934a5eedcf0..2f432fee390 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -473,11 +473,9 @@ defmodule Inspect.Algebra do defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) and is_limit(limit) do new_limit = decrement(limit) - - case fun.(term, %{opts | limit: new_limit}) do - :doc_nil -> container_each(terms, limit, opts, fun, acc, simple?) - doc -> container_each(terms, new_limit, opts, fun, [doc | acc], simple? and simple?(doc)) - end + doc = fun.(term, %{opts | limit: new_limit}) + limit = if doc == :doc_nil, do: limit, else: new_limit + container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc)) end defp container_each([left | right], limit, opts, fun, acc, simple?) when is_limit(limit) do @@ -486,10 +484,8 @@ defmodule Inspect.Algebra do right = fun.(right, %{opts | limit: limit}) simple? = simple? and simple?(left) and simple?(right) - case join(left, right, simple?, @tail_separator) do - :doc_nil -> {:lists.reverse(acc), simple?} - doc -> {:lists.reverse([doc | acc]), simple?} - end + doc = join(left, right, simple?, @tail_separator) + {:lists.reverse([doc | acc]), simple?} end defp decrement(:infinity), do: :infinity diff --git a/lib/elixir/test/elixir/inspect/algebra_test.exs b/lib/elixir/test/elixir/inspect/algebra_test.exs index 3c6477a4d9e..e5cb8b70111 100644 --- a/lib/elixir/test/elixir/inspect/algebra_test.exs +++ b/lib/elixir/test/elixir/inspect/algebra_test.exs @@ -308,4 +308,12 @@ defmodule Inspect.AlgebraTest do assert sm.(["a" | empty()]) |> render(80) == "[a]" assert sm.([empty() | "b"]) |> render(80) == "[b]" end + + test "formatting container_doc with empty and limit" do + opts = %Inspect.Opts{limit: 2} + value = ["a", empty(), "b"] + + assert container_doc("[", value, "]", opts, fn d, _ -> d end, separator: ",") |> render(80) == + "[a, b]" + end end From 0d244f287dff03766541baaabf649ba71aa925a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 06:27:03 +0200 Subject: [PATCH 07/15] Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbbb4240ce..8b679fc8be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,7 +108,7 @@ TODO. * [Enum] Allow slicing with steps in `Enum.slice/2` * [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero * [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol - * [Inspect] Allow `:order` and `:optional` when deriving the Inspect protocol + * [Inspect] Skip fields set to their default value when inspecting structs * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement` * [Kernel] Allow any guard expression as the size of a bitstring in a pattern match * [Kernel] Allow composite types with pins as the map key in a pattern match From 5fa6f649dea6f44384ef9a9757586d7f4ab6c077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 06:29:01 +0200 Subject: [PATCH 08/15] Field order --- lib/elixir/lib/uri.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index ab1a48e4406..76cfd6a6571 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -6,7 +6,7 @@ defmodule URI do URIs or encoding query strings). The functions in this module are implemented according to [RFC 3986](https://tools.ietf.org/html/rfc3986). - URIs are structs behind the scenes. If you are creating `URI` structs manually, + URIs are represented with Elixir structs. If you are creating `URI` structs manually, be aware that the `authority` field is deprecated and should not be populated. """ @@ -15,14 +15,14 @@ defmodule URI do defstruct fields @type t :: %__MODULE__{ + scheme: nil | binary, authority: authority, - fragment: nil | binary, + userinfo: nil | binary host: nil | binary, - path: nil | binary, port: nil | :inet.port_number(), + path: nil | binary, query: nil | binary, - scheme: nil | binary, - userinfo: nil | binary + fragment: nil | binary } @typedoc deprecated: "The authority field is deprecated" From 859f492fb70656283d5fcf91f856d6eab1d6b74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 10:45:13 +0200 Subject: [PATCH 09/15] Revert "Changelog" This reverts commit 0d244f287dff03766541baaabf649ba71aa925a8. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b679fc8be5..4cbbb4240ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,7 +108,7 @@ TODO. * [Enum] Allow slicing with steps in `Enum.slice/2` * [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero * [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol - * [Inspect] Skip fields set to their default value when inspecting structs + * [Inspect] Allow `:order` and `:optional` when deriving the Inspect protocol * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement` * [Kernel] Allow any guard expression as the size of a bitstring in a pattern match * [Kernel] Allow composite types with pins as the map key in a pattern match From a53c0a1bbbdf5b1f823be1d53f562d666028fd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 10:47:35 +0200 Subject: [PATCH 10/15] Revert "Do not show struct fields with default value by default" This reverts commit 56c76d3115b2bc15931892aa01eee5053b6db594. --- lib/elixir/lib/inspect.ex | 124 +++++++++++-------- lib/elixir/lib/inspect/algebra.ex | 3 - lib/elixir/lib/uri.ex | 4 +- lib/elixir/lib/version.ex | 4 +- lib/elixir/test/elixir/inspect_test.exs | 42 +++++-- lib/elixir/test/elixir/string/chars_test.exs | 2 +- lib/ex_unit/test/ex_unit/formatter_test.exs | 2 +- lib/iex/test/iex/info_test.exs | 12 +- 8 files changed, 119 insertions(+), 74 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 015acdd50be..e0db0afb31c 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -63,6 +63,10 @@ defprotocol Inspect do * `:order` - (since v1.14.0) include fields in the given order. Non-listed fields come after the listed ones. + * `:optional` - (since v1.14.0) do not include a field if it + matches its default value. This can be used to simplify the + struct representation at the cost of hiding information. + Whenever `:only` or `:except` are used to restrict fields, the struct will be printed using the `#User<...>` notation, as the struct can no longer be copy and pasted as valid Elixir @@ -76,8 +80,8 @@ defprotocol Inspect do inspect(%User{id: 1, name: "Jane", address: "Earth"}) #=> #User - If you use only the `:order` option, the struct will still be - printed as `%User{...}`. + If you use only the options `:order` and `:optional`, the struct + will still be printed as `%User{...}`. ## Custom implementation @@ -301,39 +305,31 @@ end defimpl Inspect, for: Map do def inspect(map, opts) do - list = Map.to_list(map) - - fun = - if Inspect.List.keyword?(list) do - &Inspect.List.keyword/2 - else - sep = color(" => ", :map, opts) - &to_map(&1, &2, sep) - end - - map_container_doc("%{", list, opts, fun) + inspect(Map.to_list(map), "", opts) end - defp to_map({key, value}, opts, sep) do - concat(concat(to_doc(key, opts), sep), to_doc(value, opts)) + def inspect(list, name, opts) do + open = color("%" <> name <> "{", :map, opts) + sep = color(",", :map, opts) + close = color("}", :map, opts) + + container_doc(open, list, close, opts, traverse_fun(list, opts), + separator: sep, + break: :strict + ) end - def inspect(list, name, struct, opts) do - fun = fn {key, value}, opts -> - case struct do - %{^key => ^value} -> empty() - %{} -> Inspect.List.keyword({key, value}, opts) - end + defp traverse_fun(list, opts) do + if Inspect.List.keyword?(list) do + &Inspect.List.keyword/2 + else + sep = color(" => ", :map, opts) + &to_map(&1, &2, sep) end - - map_container_doc("%" <> name <> "{", list, opts, fun) end - defp map_container_doc(open, list, opts, fun) do - open = color(open, :map, opts) - sep = color(",", :map, opts) - close = color("}", :map, opts) - container_doc(open, list, close, opts, fun, separator: sep, break: :strict) + defp to_map({key, value}, opts, sep) do + concat(concat(to_doc(key, opts), sep), to_doc(value, opts)) end end @@ -516,10 +512,12 @@ defimpl Inspect, for: Any do fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__]) only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) + optional = Keyword.get(options, :optional, []) order = Keyword.get(options, :order, []) :ok = validate_option(:only, only, fields, module) :ok = validate_option(:except, except, fields, module) + :ok = validate_option(:optional, optional, fields, module) :ok = validate_option(:order, order, fields, module) inspect_module = @@ -536,25 +534,50 @@ defimpl Inspect, for: Any do ordered_fields = order ++ (filtered_fields -- order) - quote do - defimpl Inspect, for: unquote(module) do - def inspect(var!(struct), var!(opts)) do - var!(list) = - Enum.map(unquote(ordered_fields), fn var!(key) -> + mapper = + if optional == [] do + # If there are no optional fields, we will always include all fields, + # so a map operation suffices. + quote do + Enum.map(unquote(ordered_fields), fn var!(key) -> + case var!(struct) do + %{^var!(key) => var!(value)} -> {var!(key), var!(value)} + %{} -> {var!(key), nil} + end + end) + end + else + optional_fields = + Enum.map(ordered_fields, fn field -> + if field in optional, do: {field, Map.fetch!(struct, field)}, else: field + end) + + # If there are optional fields, then we need to keep the order + # but potentially skip fields that match to their default value. + quote do + Enum.flat_map(unquote(Macro.escape(optional_fields)), fn + {var!(key), var!(default)} -> case var!(struct) do - %{^var!(key) => var!(value)} -> {var!(key), var!(value)} - %{} -> {var!(key), nil} + %{^var!(key) => ^var!(default)} -> [] + %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] + %{} -> [{var!(key), nil}] end - end) - var!(name) = Macro.inspect_atom(:literal, unquote(module)) + var!(key) -> + case var!(struct) do + %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] + %{} -> [{var!(key), nil}] + end + end) + end + end - unquote(inspect_module).inspect( - var!(list), - var!(name), - unquote(module).__struct__(), - var!(opts) - ) + quote do + defimpl Inspect, for: unquote(module) do + def inspect(var!(struct), var!(opts)) do + var!(list) = unquote(mapper) + var!(name) = Macro.inspect_atom(:literal, unquote(module)) + unquote(inspect_module).inspect(var!(list), var!(name), var!(opts)) end end end @@ -581,28 +604,21 @@ defimpl Inspect, for: Any do dunder -> if Map.keys(dunder) == Map.keys(struct) do pruned = Map.drop(struct, [:__struct__, :__exception__]) - name = Macro.inspect_atom(:literal, module) - Inspect.Map.inspect(Map.to_list(pruned), name, dunder, opts) + Inspect.Map.inspect(Map.to_list(pruned), Macro.inspect_atom(:literal, module), opts) else Inspect.Map.inspect(struct, opts) end end end - def inspect(list, name, struct, opts) do + def inspect(list, name, opts) do open = color("#" <> name <> "<", :map, opts) sep = color(",", :map, opts) close = color(">", :map, opts) fun = fn - {key, value}, opts -> - case struct do - %{^key => ^value} -> empty() - %{} -> Inspect.List.keyword({key, value}, opts) - end - - :..., _opts -> - "..." + {key, value}, opts -> Inspect.List.keyword({key, value}, opts) + :..., _opts -> "..." end container_doc(open, list ++ [:...], close, opts, fun, separator: sep, break: :strict) diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index 2f432fee390..f298d251372 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -403,12 +403,9 @@ defmodule Inspect.Algebra do The limit in the given `inspect_opts` is respected and when reached this function stops processing and outputs `"..."` instead. - Empty algebra documents are not included in the output. - ## Options * `:separator` - the separator used between each doc - * `:break` - If `:strict`, always break between each element. If `:flex`, breaks only when necessary. If `:maybe`, chooses `:flex` only if all elements are text-based, otherwise is `:strict` diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index 76cfd6a6571..7a364fe67e7 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -11,13 +11,13 @@ defmodule URI do """ fields = [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] - @derive {Inspect, order: fields} + @derive {Inspect, order: fields, optional: fields} defstruct fields @type t :: %__MODULE__{ scheme: nil | binary, authority: authority, - userinfo: nil | binary + userinfo: nil | binary, host: nil | binary, port: nil | :inet.port_number(), path: nil | binary, diff --git a/lib/elixir/lib/version.ex b/lib/elixir/lib/version.ex index 6ff74c14528..6f1c60dc091 100644 --- a/lib/elixir/lib/version.ex +++ b/lib/elixir/lib/version.ex @@ -100,8 +100,8 @@ defmodule Version do import Kernel, except: [match?: 2] @enforce_keys [:major, :minor, :patch] - @derive {Inspect, order: [:major, :minor, :patch, :pre, :build]} - defstruct [:major, :minor, :patch, pre: [], build: nil] + @derive {Inspect, order: [:major, :minor, :patch, :pre, :build], optional: [:pre, :build]} + defstruct [:major, :minor, :patch, :build, pre: []] @type version :: String.t() | t @type requirement :: String.t() | Version.Requirement.t() diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index a30c5de869a..925dcc783be 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -623,8 +623,8 @@ defmodule Inspect.MapTest do end test "exception" do - assert inspect(%RuntimeError{}) == "%RuntimeError{}" - assert inspect(%RuntimeError{message: "another"}) == "%RuntimeError{message: \"another\"}" + assert inspect(%RuntimeError{message: "runtime error"}) == + "%RuntimeError{message: \"runtime error\"}" end test "colors" do @@ -667,9 +667,6 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "#Inspect.MapTest.StructWithOnlyOption<\n b: 2,\n c: 3,\n ...\n>" - struct = %StructWithOnlyOption{a: 1, b: 2} - assert inspect(struct) == "#Inspect.MapTest.StructWithOnlyOption" - struct = %{struct | c: [1, 2, 3, 4]} assert inspect(struct) == "#Inspect.MapTest.StructWithOnlyOption" end @@ -695,9 +692,6 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "%Inspect.MapTest.StructWithAllFieldsInOnlyOption{\n a: 1,\n b: 2\n}" - - struct = %StructWithAllFieldsInOnlyOption{a: 1} - assert inspect(struct) == "%Inspect.MapTest.StructWithAllFieldsInOnlyOption{a: 1}" end test "struct missing fields in the :only option" do @@ -747,6 +741,38 @@ defmodule Inspect.MapTest do assert inspect(struct, pretty: true, width: 1) == "#Inspect.MapTest.StructWithBothOnlyAndExceptOptions<\n a: 1,\n ...\n>" end + + defmodule StructWithOptionalAndOrder do + @derive {Inspect, order: [:c, :d], optional: [:b, :c]} + defstruct [:a, :b, :c, :d] + end + + test "struct with both :order and :optional options" do + struct = %StructWithOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} + + assert inspect(struct) == + "%Inspect.MapTest.StructWithOptionalAndOrder{c: 3, d: 4, a: 1, b: 2}" + + struct = %StructWithOptionalAndOrder{} + assert inspect(struct) == "%Inspect.MapTest.StructWithOptionalAndOrder{d: nil, a: nil}" + end + + defmodule StructWithExceptOptionalAndOrder do + @derive {Inspect, order: [:c, :d], optional: [:b, :c], except: [:e]} + defstruct [:a, :b, :c, :d, :e] + end + + test "struct with :except, :order, and :optional options" do + struct = %StructWithExceptOptionalAndOrder{a: 1, b: 2, c: 3, d: 4} + + assert inspect(struct) == + "#Inspect.MapTest.StructWithExceptOptionalAndOrder" + + struct = %StructWithExceptOptionalAndOrder{} + + assert inspect(struct) == + "#Inspect.MapTest.StructWithExceptOptionalAndOrder" + end end defmodule Inspect.OthersTest do diff --git a/lib/elixir/test/elixir/string/chars_test.exs b/lib/elixir/test/elixir/string/chars_test.exs index 0fa3f2e1252..02a397a485c 100644 --- a/lib/elixir/test/elixir/string/chars_test.exs +++ b/lib/elixir/test/elixir/string/chars_test.exs @@ -155,7 +155,7 @@ defmodule String.Chars.ErrorsTest do test "user-defined struct" do message = - "protocol String\.Chars not implemented for %String.Chars.ErrorsTest.Foo{} of type String.Chars.ErrorsTest.Foo (a struct)" + "protocol String\.Chars not implemented for %String.Chars.ErrorsTest.Foo{foo: \"bar\"} of type String.Chars.ErrorsTest.Foo (a struct)" assert_raise Protocol.UndefinedError, message, fn -> to_string(%Foo{}) diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index 1514e21e80c..9ef4cf53e72 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -483,7 +483,7 @@ defmodule ExUnit.FormatterTest do message = "got RuntimeError with message \"oops\" while retrieving Exception.message/1 " <> - "for %ExUnit.FormatterTest.BadMessage{}. Stacktrace:" + "for %ExUnit.FormatterTest.BadMessage{key: 0}. Stacktrace:" assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """ 1) world (Hello) diff --git a/lib/iex/test/iex/info_test.exs b/lib/iex/test/iex/info_test.exs index aa589fefe23..b68c7fe6a7e 100644 --- a/lib/iex/test/iex/info_test.exs +++ b/lib/iex/test/iex/info_test.exs @@ -168,7 +168,10 @@ defmodule IEx.InfoTest do {:ok, date} = Date.new(2017, 1, 1) info = Info.info(date) assert get_key(info, "Data type") == "Date" - assert get_key(info, "Raw representation") == "%Date{day: 1, month: 1, year: 2017}" + + assert get_key(info, "Raw representation") == + "%Date{calendar: Calendar.ISO, day: 1, month: 1, year: 2017}" + assert get_key(info, "Reference modules") == "Date, Calendar, Map" assert get_key(info, "Description") =~ "a date" assert get_key(info, "Description") =~ "`~D`" @@ -178,7 +181,10 @@ defmodule IEx.InfoTest do {:ok, time} = Time.new(23, 59, 59) info = Info.info(time) assert get_key(info, "Data type") == "Time" - assert get_key(info, "Raw representation") == "%Time{hour: 23, minute: 59, second: 59}" + + assert get_key(info, "Raw representation") == + "%Time{calendar: Calendar.ISO, hour: 23, microsecond: {0, 0}, minute: 59, second: 59}" + assert get_key(info, "Reference modules") == "Time, Calendar, Map" assert get_key(info, "Description") =~ "a time" assert get_key(info, "Description") =~ "`~T`" @@ -190,7 +196,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "NaiveDateTime" assert get_key(info, "Raw representation") == - "%NaiveDateTime{day: 1, hour: 23, minute: 59, month: 1, second: 59, year: 2017}" + "%NaiveDateTime{calendar: Calendar.ISO, day: 1, hour: 23, microsecond: {0, 0}, minute: 59, month: 1, second: 59, year: 2017}" assert get_key(info, "Reference modules") == "NaiveDateTime, Calendar, Map" From bde716cdf9fc84044f97d7dc75ea0d6120912aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 10:50:36 +0200 Subject: [PATCH 11/15] Revert "Use new inspect for URI" This reverts commit 872dd89af60a31f1de6e653ff1bab138549ea46e. --- lib/elixir/lib/uri.ex | 126 +++++++++++++++++----- lib/elixir/test/elixir/exception_test.exs | 4 +- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index 7a364fe67e7..f095d279191 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -10,9 +10,14 @@ defmodule URI do be aware that the `authority` field is deprecated and should not be populated. """ - fields = [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] - @derive {Inspect, order: fields, optional: fields} - defstruct fields + defstruct scheme: nil, + path: nil, + query: nil, + fragment: nil, + authority: nil, + userinfo: nil, + host: nil, + port: nil @type t :: %__MODULE__{ scheme: nil | binary, @@ -490,39 +495,68 @@ defmodule URI do iex> URI.new("https://elixir-lang.org/") {:ok, %URI{ - scheme: "https", + fragment: nil, host: "elixir-lang.org", + path: "/", port: 443, - path: "/" + query: nil, + scheme: "https", + userinfo: nil }} iex> URI.new("//elixir-lang.org/") {:ok, %URI{ + fragment: nil, host: "elixir-lang.org", - path: "/" + path: "/", + port: nil, + query: nil, + scheme: nil, + userinfo: nil }} iex> URI.new("/foo/bar") {:ok, %URI{ - path: "/foo/bar" + fragment: nil, + host: nil, + path: "/foo/bar", + port: nil, + query: nil, + scheme: nil, + userinfo: nil }} iex> URI.new("foo/bar") {:ok, %URI{ - path: "foo/bar" + fragment: nil, + host: nil, + path: "foo/bar", + port: nil, + query: nil, + scheme: nil, + userinfo: nil }} iex> URI.new("//[fe80::]/") {:ok, %URI{ + fragment: nil, host: "fe80::", - path: "/" + path: "/", + port: nil, + query: nil, + scheme: nil, + userinfo: nil }} iex> URI.new("https:?query") {:ok, %URI{ - scheme: "https", + fragment: nil, + host: nil, + path: nil, port: 443, - query: "query" + query: "query", + scheme: "https", + userinfo: nil }} iex> URI.new("/invalid_greater_than_in_path/>") @@ -533,10 +567,13 @@ defmodule URI do iex> {:ok, uri} = URI.new("https://elixir-lang.org/") iex> URI.new(uri) {:ok, %URI{ - scheme: "https", + fragment: nil, host: "elixir-lang.org", + path: "/", port: 443, - path: "/" + query: nil, + scheme: "https", + userinfo: nil }} """ @doc since: "1.13.0" @@ -557,10 +594,13 @@ defmodule URI do iex> URI.new!("https://elixir-lang.org/") %URI{ - scheme: "https", + fragment: nil, host: "elixir-lang.org", + path: "/", port: 443, - path: "/" + query: nil, + scheme: "https", + userinfo: nil } iex> URI.new!("/invalid_greater_than_in_path/>") @@ -571,10 +611,13 @@ defmodule URI do iex> uri = URI.new!("https://elixir-lang.org/") iex> URI.new!(uri) %URI{ - scheme: "https", + fragment: nil, host: "elixir-lang.org", + path: "/", port: 443, - path: "/" + query: nil, + scheme: "https", + userinfo: nil } """ @doc since: "1.13.0" @@ -640,28 +683,50 @@ defmodule URI do iex> URI.parse("https://elixir-lang.org/") %URI{ - scheme: "https", authority: "elixir-lang.org", + fragment: nil, host: "elixir-lang.org", path: "/", - port: 443 + port: 443, + query: nil, + scheme: "https", + userinfo: nil } iex> URI.parse("//elixir-lang.org/") %URI{ authority: "elixir-lang.org", + fragment: nil, host: "elixir-lang.org", - path: "/" + path: "/", + port: nil, + query: nil, + scheme: nil, + userinfo: nil } iex> URI.parse("/foo/bar") %URI{ - path: "/foo/bar" + authority: nil, + fragment: nil, + host: nil, + path: "/foo/bar", + port: nil, + query: nil, + scheme: nil, + userinfo: nil } iex> URI.parse("foo/bar") %URI{ - path: "foo/bar" + authority: nil, + fragment: nil, + host: nil, + path: "foo/bar", + port: nil, + query: nil, + scheme: nil, + userinfo: nil } In contrast to `URI.new/1`, this function will parse poorly-formed @@ -669,7 +734,14 @@ defmodule URI do iex> URI.parse("/invalid_greater_than_in_path/>") %URI{ - path: "/invalid_greater_than_in_path/>" + authority: nil, + fragment: nil, + host: nil, + path: "/invalid_greater_than_in_path/>", + port: nil, + query: nil, + scheme: nil, + userinfo: nil } Another example is a URI with brackets in query strings. It is accepted @@ -678,8 +750,14 @@ defmodule URI do iex> URI.parse("/?foo[bar]=baz") %URI{ + authority: nil, + fragment: nil, + host: nil, path: "/", - query: "foo[bar]=baz" + port: nil, + query: "foo[bar]=baz", + scheme: nil, + userinfo: nil } """ diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index 7b6ff9d32c4..e9b97c1f3d4 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -453,7 +453,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/2: # 1 - %URI{scheme: \"https\", authority: \"elixir-lang.org\", host: \"elixir-lang.org\", port: 443} + %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} # 2 URI @@ -469,7 +469,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/1: # 1 - %URI{scheme: \"https\", authority: \"elixir-lang.org\", host: \"elixir-lang.org\", port: 443} + %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} Attempted function clauses (showing 1 out of 1): From 6e26272b7909412408253e3465d415ebed9bc2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 10:55:22 +0200 Subject: [PATCH 12/15] Revert again --- lib/elixir/lib/uri.ex | 30 ++++++++++++----------- lib/elixir/test/elixir/exception_test.exs | 4 +-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index f095d279191..9ff3b09a428 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -5,19 +5,25 @@ defmodule URI do This module provides functions for working with URIs (for example, parsing URIs or encoding query strings). The functions in this module are implemented according to [RFC 3986](https://tools.ietf.org/html/rfc3986). + """ + + @doc """ + The URI struct. + + The fields are defined to match the following URI representation + (with field names between brackets): + + [scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment] + - URIs are represented with Elixir structs. If you are creating `URI` structs manually, - be aware that the `authority` field is deprecated and should not be populated. + Note the `authority` field is deprecated. `parse/1` will still + populate it for backwards compatibility but you should generally + avoid setting or getting it. """ - defstruct scheme: nil, - path: nil, - query: nil, - fragment: nil, - authority: nil, - userinfo: nil, - host: nil, - port: nil + fields = [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] + @derive {Inspect, order: fields, optional: [:authority]} + defstruct fields @type t :: %__MODULE__{ scheme: nil | binary, @@ -707,7 +713,6 @@ defmodule URI do iex> URI.parse("/foo/bar") %URI{ - authority: nil, fragment: nil, host: nil, path: "/foo/bar", @@ -719,7 +724,6 @@ defmodule URI do iex> URI.parse("foo/bar") %URI{ - authority: nil, fragment: nil, host: nil, path: "foo/bar", @@ -734,7 +738,6 @@ defmodule URI do iex> URI.parse("/invalid_greater_than_in_path/>") %URI{ - authority: nil, fragment: nil, host: nil, path: "/invalid_greater_than_in_path/>", @@ -750,7 +753,6 @@ defmodule URI do iex> URI.parse("/?foo[bar]=baz") %URI{ - authority: nil, fragment: nil, host: nil, path: "/", diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index e9b97c1f3d4..fd720df874a 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -453,7 +453,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/2: # 1 - %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} + %URI{scheme: \"https\", authority: \"elixir-lang.org\", userinfo: nil, host: \"elixir-lang.org\", port: 443, path: nil, query: nil, fragment: nil} # 2 URI @@ -469,7 +469,7 @@ defmodule ExceptionTest do The following arguments were given to ExceptionTest.Req.get!/1: # 1 - %URI{authority: \"elixir-lang.org\", fragment: nil, host: \"elixir-lang.org\", path: nil, port: 443, query: nil, scheme: \"https\", userinfo: nil} + %URI{scheme: \"https\", authority: \"elixir-lang.org\", userinfo: nil, host: \"elixir-lang.org\", port: 443, path: nil, query: nil, fragment: nil} Attempted function clauses (showing 1 out of 1): From 10c9a6b5faf3dfd72544a4be96e3023f2544ca95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 20:00:17 +0200 Subject: [PATCH 13/15] Store struct metadata on info --- lib/elixir/lib/kernel.ex | 14 +++++++++++--- lib/elixir/lib/kernel/utils.ex | 17 ++++++++++++----- lib/elixir/lib/module.ex | 26 ++++++++++++-------------- lib/elixir/src/elixir_erl.erl | 20 +++++++++++++++----- lib/elixir/src/elixir_module.erl | 9 ++------- lib/elixir/test/elixir/kernel_test.exs | 5 +++++ 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 3bad1ad1608..bc6f04114bc 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -5119,13 +5119,21 @@ defmodule Kernel do list before invoking `defstruct/1`: defmodule User do - @derive [MyProtocol] - defstruct name: nil, age: 10 + 11 + @derive MyProtocol + defstruct name: nil, age: nil end MyProtocol.call(john) # it works! - For each protocol in the `@derive` list, Elixir will assert the protocol has + A common example is to `@derive` the `Inspect` protocol to hide certain fields + when the struct is printed: + + defmodule User do + @derive {Inspect, only: :name} + defstruct name: nil, age: nil + end + + For each protocol in `@derive`, Elixir will assert the protocol has been implemented for `Any`. If the `Any` implementation defines a `__deriving__/3` callback, the callback will be invoked and it should define the implementation module. Otherwise an implementation that simply points to diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 712583c26f2..b5ef70eea5f 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -157,7 +157,7 @@ defmodule Kernel.Utils do [] -> quote do Enum.reduce(var!(kv), @__struct__, fn {key, val}, map -> - Map.replace!(map, key, val) + %{map | key => val} end) end @@ -166,7 +166,7 @@ defmodule Kernel.Utils do {map, keys} = Enum.reduce(var!(kv), {@__struct__, unquote(enforce_keys)}, fn {key, val}, {map, keys} -> - {Map.replace!(map, key, val), List.delete(keys, key)} + {%{map | key => val}, List.delete(keys, key)} end) case keys do @@ -193,10 +193,17 @@ defmodule Kernel.Utils do case enforce_keys -- :maps.keys(struct) do [] -> - derive = Module.get_attribute(module, :derive) + # The __struct__ field is used for expansion and for loading remote structs Module.put_attribute(module, :__struct__, struct) - Module.put_attribute(module, :__derived__, derive) - {struct, Module.get_attribute(module, :derive), body} + + # Finally store all field metadata to go into __info__(:struct) + mapper = fn {key, val} -> + %{field: key, default: val, required: :lists.member(key, enforce_keys)} + end + + {set, _} = :elixir_module.data_tables(module) + :ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)}) + {struct, Module.delete_attribute(module, :derive), body} error_keys -> raise ArgumentError, diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index c7da8376342..d5dba470fd8 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -550,6 +550,8 @@ defmodule Module do * `:module` - the module atom name + * `:struct` - if the module defines a struct and if so each field in order + """ @callback __info__(:attributes) :: keyword() @callback __info__(:compile) :: [term()] @@ -557,6 +559,7 @@ defmodule Module do @callback __info__(:macros) :: keyword() @callback __info__(:md5) :: binary() @callback __info__(:module) :: module() + @callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil @doc """ Returns information about module attributes used by Elixir. @@ -1814,22 +1817,17 @@ defmodule Module do [] -> :ok - to_derive -> - case :ets.lookup(set, :__derived__) do - [{_, derived, _}] -> - if to_derive != :lists.reverse(derived) do - message = - "warning: module attribute @derive was set after defstruct, all @derive calls must come before defstruct" - - IO.warn(message, env) - end - - [] -> - message = + _ -> + message = + case :ets.lookup(set, :__struct__) do + [] -> "warning: module attribute @derive was set but never used (it must come before defstruct)" - IO.warn(message, env) - end + _ -> + "warning: module attribute @derive was set after defstruct, all @derive calls must come before defstruct" + end + + IO.warn(message, env) end end diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 05523fa36ef..0f8757b0cb2 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -75,6 +75,9 @@ elixir_to_erl([], Ann) -> {nil, Ann}; elixir_to_erl(<<>>, Ann) -> {bin, Ann, []}; +elixir_to_erl(#{} = Map, Ann) -> + Assocs = [{map_field_assoc, Ann, elixir_to_erl(K, Ann), elixir_to_erl(V, Ann)} || {K, V} <- maps:to_list(Map)], + {map, Ann, Assocs}; elixir_to_erl(Tree, Ann) when is_list(Tree) -> elixir_to_erl_cons(Tree, Ann); elixir_to_erl(Tree, Ann) when is_atom(Tree) -> @@ -249,7 +252,7 @@ functions_form(Line, Module, Def, Defmacro, Exports, Body, Deprecated, Struct) - [{attribute, Line, export, lists:sort([{'__info__', 1} | Exports])}, Spec, Info | Body]. add_info_function(Line, Module, Def, Defmacro, Deprecated, Struct) -> - AllowedAttrs = [attributes, compile, functions, macros, md5, exports_md5, module, deprecated], + AllowedAttrs = [attributes, compile, functions, macros, md5, exports_md5, module, deprecated, struct], AllowedArgs = lists:map(fun(Atom) -> {atom, Line, Atom} end, AllowedAttrs), SortedDef = lists:sort(Def), SortedDefmacro = lists:sort(Defmacro), @@ -269,6 +272,7 @@ add_info_function(Line, Module, Def, Defmacro, Deprecated, Struct) -> get_module_info(Module), functions_info(SortedDef), macros_info(SortedDefmacro), + struct_info(Struct), exports_md5_info(Struct, SortedDef, SortedDefmacro), get_module_info(Module, attributes), get_module_info(Module, compile), @@ -285,20 +289,26 @@ exports_md5_info(Struct, Def, Defmacro) -> %% Deprecations do not need to be part of exports_md5 because it is always %% checked by the runtime pass, so it is not really part of compilation. Md5 = erlang:md5(erlang:term_to_binary({Def, Defmacro, Struct})), - {clause, 0, [{atom, 0, exports_md5}], [], [elixir_erl:elixir_to_erl(Md5)]}. + {clause, 0, [{atom, 0, exports_md5}], [], [elixir_to_erl(Md5)]}. functions_info(Def) -> - {clause, 0, [{atom, 0, functions}], [], [elixir_erl:elixir_to_erl(Def)]}. + {clause, 0, [{atom, 0, functions}], [], [elixir_to_erl(Def)]}. macros_info(Defmacro) -> - {clause, 0, [{atom, 0, macros}], [], [elixir_erl:elixir_to_erl(Defmacro)]}. + {clause, 0, [{atom, 0, macros}], [], [elixir_to_erl(Defmacro)]}. + +struct_info(nil) -> + {clause, 0, [{atom, 0, struct}], [], [{atom, 0, nil}]}; +struct_info(Fields) -> + FieldsWithoutDefault = [maps:remove(default, FieldInfo) || FieldInfo <- Fields], + {clause, 0, [{atom, 0, struct}], [], [elixir_to_erl(FieldsWithoutDefault)]}. get_module_info(Module, Key) -> Call = ?remote(0, erlang, get_module_info, [{atom, 0, Module}, {var, 0, 'Key'}]), {clause, 0, [{match, 0, {var, 0, 'Key'}, {atom, 0, Key}}], [], [Call]}. deprecated_info(Deprecated) -> - {clause, 0, [{atom, 0, deprecated}], [], [elixir_erl:elixir_to_erl(Deprecated)]}. + {clause, 0, [{atom, 0, deprecated}], [], [elixir_to_erl(Deprecated)]}. % Typespecs diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 92d5d9783cd..81155dcab7d 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -430,14 +430,9 @@ warn_unused_attributes(File, DataSet, DataBag, PersistedAttrs) -> || [Key, Line] <- ets:select(DataSet, Query)]. get_struct(Set) -> - case ets:lookup(Set, '__struct__') of + case ets:lookup(Set, {elixir, struct}) of [] -> nil; - [{_, Struct, _}] -> - case ets:lookup(Set, enforce_keys) of - [] -> {Struct, []}; - [{_, EnforceKeys, _}] when is_list(EnforceKeys) -> {Struct, EnforceKeys}; - [{_, EnforceKeys, _}] -> {Struct, [EnforceKeys]} - end + [{_, Struct}] -> Struct end. get_deprecated(Bag) -> diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index 0d7d592d943..f170c94751e 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -811,6 +811,11 @@ defmodule KernelTest do refute {:__info__, 1} in Kernel.__info__(:functions) end + test ":struct" do + assert Kernel.__info__(:struct) == nil + assert hd(URI.__info__(:struct)) == %{field: :scheme, required: false} + end + test "others" do assert Kernel.__info__(:module) == Kernel assert is_list(Kernel.__info__(:compile)) From e46edf2d912cb99e2fd4c7d2b2ca192be94f2a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 20:30:36 +0200 Subject: [PATCH 14/15] Respect order when inspecting --- CHANGELOG.md | 3 +- lib/elixir/lib/inspect.ex | 116 ++++++++++-------------- lib/elixir/lib/uri.ex | 6 +- lib/elixir/lib/version.ex | 22 +++-- lib/elixir/test/elixir/inspect_test.exs | 8 +- lib/iex/test/iex/info_test.exs | 6 +- 6 files changed, 72 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbbb4240ce..235884acbdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,7 +108,8 @@ TODO. * [Enum] Allow slicing with steps in `Enum.slice/2` * [Float] Do not show floats in scientific notation if below `1.0e16` and the fractional value is precisely zero * [Inspect] Improve error reporting when there is a faulty implementation of the `Inspect` protocol - * [Inspect] Allow `:order` and `:optional` when deriving the Inspect protocol + * [Inspect] Allow `:optional` when deriving the Inspect protocol + * [Inspect] Inspect struct fields in the order they are defined * [Inspect] Use expression-based inspection for `Date.Range`, `MapSet`, and `Version.Requirement` * [Kernel] Allow any guard expression as the size of a bitstring in a pattern match * [Kernel] Allow composite types with pins as the map key in a pattern match diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index e0db0afb31c..c601761f452 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -60,9 +60,6 @@ defprotocol Inspect do * `:except` - remove the given fields when inspecting. - * `:order` - (since v1.14.0) include fields in the given order. - Non-listed fields come after the listed ones. - * `:optional` - (since v1.14.0) do not include a field if it matches its default value. This can be used to simplify the struct representation at the cost of hiding information. @@ -80,8 +77,8 @@ defprotocol Inspect do inspect(%User{id: 1, name: "Jane", address: "Earth"}) #=> #User - If you use only the options `:order` and `:optional`, the struct - will still be printed as `%User{...}`. + If you use only the `:optional` option, the struct will still be + printed as `%User{...}`. ## Custom implementation @@ -305,32 +302,34 @@ end defimpl Inspect, for: Map do def inspect(map, opts) do - inspect(Map.to_list(map), "", opts) - end + list = Map.to_list(map) - def inspect(list, name, opts) do - open = color("%" <> name <> "{", :map, opts) - sep = color(",", :map, opts) - close = color("}", :map, opts) + fun = + if Inspect.List.keyword?(list) do + &Inspect.List.keyword/2 + else + sep = color(" => ", :map, opts) + &to_assoc(&1, &2, sep) + end - container_doc(open, list, close, opts, traverse_fun(list, opts), - separator: sep, - break: :strict - ) + map_container_doc(list, "", opts, fun) end - defp traverse_fun(list, opts) do - if Inspect.List.keyword?(list) do - &Inspect.List.keyword/2 - else - sep = color(" => ", :map, opts) - &to_map(&1, &2, sep) - end + def inspect(map, name, infos, opts) do + fun = fn %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) end + map_container_doc(infos, name, opts, fun) end - defp to_map({key, value}, opts, sep) do + defp to_assoc({key, value}, opts, sep) do concat(concat(to_doc(key, opts), sep), to_doc(value, opts)) end + + defp map_container_doc(list, name, opts, fun) do + open = color("%" <> name <> "{", :map, opts) + sep = color(",", :map, opts) + close = color("}", :map, opts) + container_doc(open, list, close, opts, fun, separator: sep, break: :strict) + end end defimpl Inspect, for: Integer do @@ -513,12 +512,10 @@ defimpl Inspect, for: Any do only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) optional = Keyword.get(options, :optional, []) - order = Keyword.get(options, :order, []) :ok = validate_option(:only, only, fields, module) :ok = validate_option(:except, except, fields, module) :ok = validate_option(:optional, optional, fields, module) - :ok = validate_option(:order, order, fields, module) inspect_module = if fields == only and except == [] do @@ -532,52 +529,33 @@ defimpl Inspect, for: Any do |> Enum.reject(&(&1 in except)) |> Enum.filter(&(&1 in only)) - ordered_fields = order ++ (filtered_fields -- order) - - mapper = + optional? = if optional == [] do - # If there are no optional fields, we will always include all fields, - # so a map operation suffices. - quote do - Enum.map(unquote(ordered_fields), fn var!(key) -> - case var!(struct) do - %{^var!(key) => var!(value)} -> {var!(key), var!(value)} - %{} -> {var!(key), nil} - end - end) - end + false else - optional_fields = - Enum.map(ordered_fields, fn field -> - if field in optional, do: {field, Map.fetch!(struct, field)}, else: field - end) + optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)} - # If there are optional fields, then we need to keep the order - # but potentially skip fields that match to their default value. quote do - Enum.flat_map(unquote(Macro.escape(optional_fields)), fn - {var!(key), var!(default)} -> - case var!(struct) do - %{^var!(key) => ^var!(default)} -> [] - %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] - %{} -> [{var!(key), nil}] - end - - var!(key) -> - case var!(struct) do - %{^var!(key) => var!(value)} -> [{var!(key), var!(value)}] - %{} -> [{var!(key), nil}] - end - end) + case unquote(Macro.escape(optional_map)) do + %{^var!(field) => var!(default)} -> + var!(default) == Map.get(var!(struct), var!(field)) + + %{} -> + false + end end end quote do defimpl Inspect, for: unquote(module) do def inspect(var!(struct), var!(opts)) do - var!(list) = unquote(mapper) + var!(infos) = + for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct), + var!(field) in unquote(filtered_fields) and not unquote(optional?), + do: var!(info) + var!(name) = Macro.inspect_atom(:literal, unquote(module)) - unquote(inspect_module).inspect(var!(list), var!(name), var!(opts)) + unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts)) end end end @@ -597,31 +575,35 @@ defimpl Inspect, for: Any do def inspect(%module{} = struct, opts) do try do - module.__struct__() + {module.__struct__(), module.__info__(:struct)} rescue _ -> Inspect.Map.inspect(struct, opts) else - dunder -> + {dunder, fields} -> if Map.keys(dunder) == Map.keys(struct) do - pruned = Map.drop(struct, [:__struct__, :__exception__]) - Inspect.Map.inspect(Map.to_list(pruned), Macro.inspect_atom(:literal, module), opts) + infos = + for %{field: field} = info <- fields, + field not in [:__struct__, :__exception__], + do: info + + Inspect.Map.inspect(struct, Macro.inspect_atom(:literal, module), infos, opts) else Inspect.Map.inspect(struct, opts) end end end - def inspect(list, name, opts) do + def inspect(map, name, infos, opts) do open = color("#" <> name <> "<", :map, opts) sep = color(",", :map, opts) close = color(">", :map, opts) fun = fn - {key, value}, opts -> Inspect.List.keyword({key, value}, opts) + %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) :..., _opts -> "..." end - container_doc(open, list ++ [:...], close, opts, fun, separator: sep, break: :strict) + container_doc(open, infos ++ [:...], close, opts, fun, separator: sep, break: :strict) end end diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index 9ff3b09a428..bfb2ebd0793 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -20,10 +20,8 @@ defmodule URI do populate it for backwards compatibility but you should generally avoid setting or getting it. """ - - fields = [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] - @derive {Inspect, order: fields, optional: [:authority]} - defstruct fields + @derive {Inspect, optional: [:authority]} + defstruct [:scheme, :authority, :userinfo, :host, :port, :path, :query, :fragment] @type t :: %__MODULE__{ scheme: nil | binary, diff --git a/lib/elixir/lib/version.ex b/lib/elixir/lib/version.ex index 6f1c60dc091..61e9d760234 100644 --- a/lib/elixir/lib/version.ex +++ b/lib/elixir/lib/version.ex @@ -26,14 +26,6 @@ defmodule Version do "1.0.0-alpha.3+20130417140000.amd64" - ## Struct - - The version is represented by the `Version` struct and fields - are named according to SemVer 2.0: `:major`, `:minor`, `:patch`, - `:pre`, and `:build`. You can read those fields but you should - not create a new `Version` directly via the struct syntax. Instead - use the functions in this module. - ## Requirements Requirements allow you to specify which versions of a given @@ -99,9 +91,19 @@ defmodule Version do import Kernel, except: [match?: 2] + @doc """ + The Version struct. + + It contains the fields `:major`, `:minor`, `:patch`, `:pre`, and + `:build` according to SemVer 2.0, where `:pre` is a list. + + You can read those fields but you should not create a new `Version` + directly via the struct syntax. Instead use the functions in this + module. + """ @enforce_keys [:major, :minor, :patch] - @derive {Inspect, order: [:major, :minor, :patch, :pre, :build], optional: [:pre, :build]} - defstruct [:major, :minor, :patch, :build, pre: []] + @derive {Inspect, optional: [:pre, :build]} + defstruct [:major, :minor, :patch, pre: [], build: nil] @type version :: String.t() | t @type requirement :: String.t() | Version.Requirement.t() diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 925dcc783be..27f76ced173 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -743,8 +743,8 @@ defmodule Inspect.MapTest do end defmodule StructWithOptionalAndOrder do - @derive {Inspect, order: [:c, :d], optional: [:b, :c]} - defstruct [:a, :b, :c, :d] + @derive {Inspect, optional: [:b, :c]} + defstruct [:c, :d, :a, :b] end test "struct with both :order and :optional options" do @@ -758,8 +758,8 @@ defmodule Inspect.MapTest do end defmodule StructWithExceptOptionalAndOrder do - @derive {Inspect, order: [:c, :d], optional: [:b, :c], except: [:e]} - defstruct [:a, :b, :c, :d, :e] + @derive {Inspect, optional: [:b, :c], except: [:e]} + defstruct [:c, :d, :e, :a, :b] end test "struct with :except, :order, and :optional options" do diff --git a/lib/iex/test/iex/info_test.exs b/lib/iex/test/iex/info_test.exs index b68c7fe6a7e..33f0794ded3 100644 --- a/lib/iex/test/iex/info_test.exs +++ b/lib/iex/test/iex/info_test.exs @@ -170,7 +170,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "Date" assert get_key(info, "Raw representation") == - "%Date{calendar: Calendar.ISO, day: 1, month: 1, year: 2017}" + "%Date{year: 2017, month: 1, day: 1, calendar: Calendar.ISO}" assert get_key(info, "Reference modules") == "Date, Calendar, Map" assert get_key(info, "Description") =~ "a date" @@ -183,7 +183,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "Time" assert get_key(info, "Raw representation") == - "%Time{calendar: Calendar.ISO, hour: 23, microsecond: {0, 0}, minute: 59, second: 59}" + "%Time{hour: 23, minute: 59, second: 59, microsecond: {0, 0}, calendar: Calendar.ISO}" assert get_key(info, "Reference modules") == "Time, Calendar, Map" assert get_key(info, "Description") =~ "a time" @@ -196,7 +196,7 @@ defmodule IEx.InfoTest do assert get_key(info, "Data type") == "NaiveDateTime" assert get_key(info, "Raw representation") == - "%NaiveDateTime{calendar: Calendar.ISO, day: 1, hour: 23, microsecond: {0, 0}, minute: 59, month: 1, second: 59, year: 2017}" + "%NaiveDateTime{year: 2017, month: 1, day: 1, hour: 23, minute: 59, second: 59, microsecond: {0, 0}, calendar: Calendar.ISO}" assert get_key(info, "Reference modules") == "NaiveDateTime, Calendar, Map" From 74573aa5b460006af43a28f2231f02a489b33029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jun 2022 23:23:33 +0200 Subject: [PATCH 15/15] Update lib/elixir/lib/inspect.ex --- lib/elixir/lib/inspect.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index c601761f452..29923748c5a 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -508,7 +508,7 @@ end defimpl Inspect, for: Any do defmacro __deriving__(module, struct, options) do - fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__]) + fields = Map.keys(struct) -- [:__exception__, :__struct__] only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) optional = Keyword.get(options, :optional, [])