Custom inspect for some binaries in structs

Building a library that uses binary communication over the internet… here is what a common structure looks like when it is being inspected:

%Structure{
  data: <<218, 84, 144, 157, 59, 107, 55, 125, 38, 84, 224, 136, 117, 150,
    180, 159, 142, 54, 196, 48, 15, 219, 158, 185, 227, 228, 58, 71, 220, 237,
    167, 137>>
}

Now, people tend to discuss about it using hex… so it would be much easier to reason with if it looked more like this:

%Structure{
  data: <<0xda54909d3b6b377d2654e0887596b49f8e36c4300fdb9eb9e3e43a47dceda789::256>>
}

A nice benefit of that notation is that when pasted in a REPL, it parses just fine.

I tried to use defimpl for Structure, redefining the inspect function and it works… problem is, inspect ends up returning a green string without indentation. Trying to fix this, I was in the process of writing a ton of code, which doesn’t feel right…

So… is there way to instruct elixir to output to the 2nd example without messing up with the formatting?

Here is an example of how the data is being formatted

  defp print_compact_bitstring(bitstring) do
    hex_data = Base.encode16(bitstring, case: :lower)
    data_size = byte_size(bitstring) * @byte

    "<<0x#{hex_data}::#{data_size}>>"
  end
iex(3)> data = <<218, 84, 144, 157, 59, 107, 55, 125, 38, 84, 224, 136, 117, 150,
...(3)>     180, 159, 142, 54, 196, 48, 15, 219, 158, 185, 227, 228, 58, 71, 220, 237,
...(3)>     167, 137>>
<<218, 84, 144, 157, 59, 107, 55, 125, 38, 84, 224, 136, 117, 150, 180, 159,
  142, 54, 196, 48, 15, 219, 158, 185, 227, 228, 58, 71, 220, 237, 167, 137>>
iex(4)> Base.encode16(data)
"DA54909D3B6B377D2654E0887596B49F8E36C4300FDB9EB9E3E43A47DCEDA789"

Does that help? Base — Elixir v1.13.4

Pass lower: true to get lowercase:

iex(6)> Base.encode16(data, case: :lower)
"da54909d3b6b377d2654e0887596b49f8e36c4300fdb9eb9e3e43a47dceda789"

print_compact_bitstring above does the formatting…

The need is that I’d like it to be applied all over in the REPL without calling it.

I took a stab at it and got this result:

Code (Note I don’t have the @byte module attribute set, so I just deleted it. Though I’m assuming its 8):

defmodule Structure do
  defstruct [:data]
end

defimpl Inspect, for: Structure do
  import Inspect.Algebra

  def inspect(structure, opts) do
    concat ["%Structure{data: ", to_doc(print_compact_bitstring(structure.data), opts), "}"]
  end

  defp print_compact_bitstring(bitstring) do
    hex_data = Base.encode16(bitstring, case: :lower)
    data_size = byte_size(bitstring)

    "<<0x#{hex_data}::#{data_size}>>"
  end
end

IEx:

iex(1)> s = %Structure{data: <<218, 84, 144, 157, 59, 107, 55, 125, 38, 84, 224, 136, 117, 150,                                                                                                                                                                                                         
...(1)>     180, 159, 142, 54, 196, 48, 15, 219, 158, 185, 227, 228, 58, 71, 220, 237,                                                                                                                                                                                                                  
...(1)>     167, 137>>}                                                                                                                                                                                                                                                                                 
%Structure{data: "<<0xda54909d3b6b377d2654e0887596b49f8e36c4300fdb9eb9e3e43a47dceda789::32>>"} 
1 Like

Thank you for the example!

Thing is, the formatting is gone with this method.

I found a way in the meantime… I created a module called HexBinary that formats itself correctly…

defmodule HexBinary do
  defstruct [:data]
end

defimpl Inspect, for: HexBinary do
  @byte 8

  def inspect(%HexBinary{} = binary, _opts) do
    print_compact_bitstring(binary.data)
  end

  defp print_compact_bitstring(bitstring) do
    hex_data = Base.encode16(bitstring, case: :lower)
    data_size = byte_size(bitstring) * @byte

    "<<0x#{hex_data}::#{data_size}>>"
  end
end

Then, I switch the binaries I want to be formatted while inspecting the %Structure{} just like this:

defimpl Inspect, for: Structure do
  def inspect(%Structure{data: data} = structure, opts) do
    %{structure | data: %HexBinary{data: data}}
    |> Inspect.Map.inspect(Code.Identifier.inspect_as_atom(Structure), opts)
  end
end

Ends up keeping the formatting just fine…

Try this one:

defmodule Structure do
  defstruct [:data]
end

defimpl Inspect, for: Structure do
  import Inspect.Algebra

  @byte 8

  def inspect(map, inspect_opts) do
    list = map |> Map.from_struct() |> Map.to_list()
    open = color("%Structure{", :map, inspect_opts)
    sep = color(",", :map, inspect_opts)
    close = color("}", :map, inspect_opts)
    opts = [separator: sep, break: :strict]
    container_doc(open, list, close, inspect_opts, &traverse(&1, &2), opts)
  end

  def traverse({key, value}, opts) do
    value_doc =
      if key == :data do
        inspect_data(value, opts)
      else
        to_doc(value, opts)
      end

    key = :key |> Macro.inspect_atom(key) |> color(:atom, opts)
    concat(key, concat(" ", value_doc))
  end

  defp inspect_data(data, opts) do
    hex_data = Base.encode16(data, case: :lower)
    data_size = byte_size(data) * @byte
    left = color("<<", :binary, opts)
    bitstring = color("0x#{hex_data}", :number, opts)
    delimeter = color("::", :binary, opts)
    size = color("#{data_size}", :number, opts)
    right = color(">>", :binary, opts)

    left
    |> concat(bitstring)
    |> concat(delimeter)
    |> concat(size)
    |> concat(right)
    |> group()
  end
end

The above code is really flexible, for example it would work even if you add extra keys to your struct and it supports colors passed to Inspect.Opts as same as in a normal inspection of struct.

1 Like

If remember correctly you can just write it as:

defimpl Inspect, for: Structure do
  def inspect(%Structure{data: data} = structure, opts) do
    Inspect.Any.inspect(%{structure | data: %HexBinary{data: data}}, opts)
  end
end

However keep in mind that in your way you loose inspecting colours.

1 Like

It works! Just need to pass the opts to the inspect function…

defimpl Inspect, for: Structure do
  def inspect(%Structure{data: data} = structure, opts) do
    %{structure | data: %HexBinary{data: data}}
    |> Inspect.Any.inspect(opts)
  end
end

I will try the previous answer, it looks promising!

Thank you very much!

Looks like this function is cutting edge stuff not available in elixir 1.13.4

Correct, I wrote it from memory. I have edited code snippet. The previous example was checked. :slight_smile:

Yes, it’s new since 1.14. :slight_smile:

To make it work best I looked at Elixir source code. If remember correctly the previous version called a function in private module called Code.Identifier. This one is important to handle all types of key for example: :"A Custom Atom Key" and many more cases.

I would not recommend that, but if you are really sure that all keys would be a “proper” atom (no need to escape) then you can use a concat together with raw string : and use the color/3 with :atom type.

Also for more information about new function see it’s documentation: Macro.inspect_atom/2