Files
n8n-openai-adapter/lib/elixir/lib/inspect.ex
T

490 lines
13 KiB
Elixir

import Kernel, except: [inspect: 1]
import Inspect.Algebra
alias Code.Identifier
defprotocol Inspect do
@moduledoc """
The `Inspect` protocol converts an Elixir data structure into an
algebra document.
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`.
## Examples
Many times, inspecting a structure can be implemented in function
of existing entities. For example, here is `MapSet`'s `inspect/2`
implementation:
defimpl Inspect, for: MapSet do
import Inspect.Algebra
def inspect(dict, opts) do
concat(["#MapSet<", to_doc(MapSet.to_list(dict), 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<"`,
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.
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.
## Error handling
In case there is an error while your structure is being inspected,
Elixir will raise an `ArgumentError` error and will automatically fall back
to a raw representation for printing the structure.
You can however access the underlying error by invoking the `Inspect`
implementation directly. For example, to test `Inspect.MapSet` above,
you can invoke it as:
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
## Deriving
The `Inspect` protocol can be derived to hide certain fields from
structs, so they don't show up in logs, inspects and similar. This
is especially useful for fields containing private information.
The options `:only` and `:except` can be used with `@derive` to
specify which fields should and should not appear in the
algebra document:
defmodule User do
@derive {Inspect, only: [:id, :name]}
defstruct [:id, :name, :address]
end
inspect(%User{id: 1, name: "Homer", address: "742 Evergreen Terrace"})
#=> #User<id: 1, name: "Homer", ...>
"""
# Handle structs in Any
@fallback_to_any true
@doc """
Converts `term` into an algebra document.
This function shouldn't be invoked directly, unless when implementing
a custom `inspect_fun` to be given to `Inspect.Opts`. Everywhere else,
`Inspect.Algebra.to_doc/2` should be preferred as it handles structs
and exceptions.
"""
@spec inspect(t, Inspect.Opts.t()) :: Inspect.Algebra.t()
def inspect(term, opts)
end
defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color(Identifier.inspect_as_atom(atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
defp color_key(nil), do: nil
defp color_key(_), do: :atom
end
defimpl Inspect, for: BitString do
def inspect(term, opts) when is_binary(term) do
%Inspect.Opts{binaries: bins, base: base, printable_limit: printable_limit} = opts
if base == :decimal and
(bins == :as_strings or (bins == :infer and String.printable?(term, printable_limit))) do
inspected =
case Identifier.escape(term, ?", printable_limit) do
{escaped, ""} -> [?", escaped, ?"]
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color(IO.iodata_to_binary(inspected), :string, opts)
else
inspect_bitstring(term, opts)
end
end
def inspect(term, opts) do
inspect_bitstring(term, opts)
end
defp inspect_bitstring("", opts) do
color("<<>>", :binary, opts)
end
defp inspect_bitstring(bitstring, opts) do
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
end
defp each_bit(_, 0, _) do
"..."
end
defp each_bit(<<>>, _counter, _opts) do
:doc_nil
end
defp each_bit(<<h::8>>, _counter, opts) do
Inspect.Integer.inspect(h, opts)
end
defp each_bit(<<h, t::bitstring>>, counter, opts) do
flex_glue(
concat(Inspect.Integer.inspect(h, opts), ","),
each_bit(t, decrement(counter), opts)
)
end
defp each_bit(bitstring, _counter, opts) do
size = bit_size(bitstring)
<<h::size(size)>> = bitstring
Inspect.Integer.inspect(h, opts) <> "::size(" <> Integer.to_string(size) <> ")"
end
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
end
defimpl Inspect, for: List do
def inspect([], opts) do
color("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
def inspect(term, opts) do
%Inspect.Opts{
charlists: lists,
char_lists: lists_deprecated,
printable_limit: printable_limit
} = opts
lists =
if lists == :infer and lists_deprecated != :infer do
case lists_deprecated do
:as_char_lists ->
IO.warn(
"the :char_lists inspect option and its :as_char_lists " <>
"value are deprecated, use the :charlists option and its " <>
":as_charlists value instead"
)
:as_charlists
_ ->
IO.warn("the :char_lists inspect option is deprecated, use :charlists instead")
lists_deprecated
end
else
lists
end
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?', printable_limit) do
{escaped, ""} -> [?', escaped, ?']
{escaped, _} -> [?', escaped, ?', " ++ ..."]
end
IO.iodata_to_binary(inspected)
keyword?(term) ->
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
true ->
container_doc(open, term, close, opts, &to_doc/2, separator: sep)
end
end
@doc false
def keyword({key, value}, opts) do
key = color(Identifier.inspect_as_key(key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@doc false
def keyword?([{key, _value} | rest]) when is_atom(key) do
case Atom.to_charlist(key) do
'Elixir.' ++ _ -> false
_ -> keyword?(rest)
end
end
def keyword?([]), do: true
def keyword?(_other), do: false
end
defimpl Inspect, for: Tuple do
def inspect(tuple, opts) do
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
end
end
defimpl Inspect, for: Map do
def inspect(map, opts) do
inspect(map, "", opts)
end
def inspect(map, name, opts) do
map = Map.to_list(map)
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)
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
end
defp to_map({key, value}, opts, sep) do
concat(concat(to_doc(key, opts), sep), to_doc(value, opts))
end
end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color(inspected, :number, opts)
end
defp base_to_value(base) do
case base do
:binary -> 2
:decimal -> 10
:octal -> 8
:hex -> 16
end
end
defp prepend_prefix(value, :decimal), do: value
defp prepend_prefix(<<?-, value::binary>>, base) do
"-" <> prepend_prefix(value, base)
end
defp prepend_prefix(value, base) do
prefix =
case base do
:binary -> "0b"
:octal -> "0o"
:hex -> "0x"
end
prefix <> value
end
end
defimpl Inspect, for: Float do
def inspect(term, opts) do
inspected = IO.iodata_to_binary(:io_lib_format.fwrite_g(term))
color(inspected, :number, opts)
end
end
defimpl Inspect, for: Regex do
def inspect(regex, opts) do
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary(['~r/', escaped, ?/, regex.opts])
color(source, :regex, opts)
end
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
defp normalize(<<char, rest::binary>>, acc), do: normalize(rest, <<acc::binary, char>>)
defp normalize(<<>>, acc), do: acc
defp escape_map(?\a), do: '\\a'
defp escape_map(?\f), do: '\\f'
defp escape_map(?\n), do: '\\n'
defp escape_map(?\r), do: '\\r'
defp escape_map(?\t), do: '\\t'
defp escape_map(?\v), do: '\\v'
defp escape_map(_), do: false
end
defimpl Inspect, for: Function do
def inspect(function, _opts) do
fun_info = Function.info(function)
mod = fun_info[:module]
name = fun_info[:name]
cond do
fun_info[:type] == :external and fun_info[:env] == [] ->
inspected_as_atom = Identifier.inspect_as_atom(mod)
inspected_as_function = Identifier.inspect_as_function(name)
"&#{inspected_as_atom}.#{inspected_as_function}/#{fun_info[:arity]}"
match?('elixir_compiler_' ++ _, Atom.to_charlist(mod)) ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
default_inspect(mod, fun_info)
end
true ->
default_inspect(mod, fun_info)
end
end
defp default_inspect(mod, fun_info) do
inspected_as_atom = Identifier.inspect_as_atom(mod)
extracted_name = extract_name(fun_info[:name])
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in #{inspected_as_atom}#{extracted_name}>"
end
defp extract_name([]) do
""
end
defp extract_name(name) do
case Identifier.extract_anonymous_fun_parent(name) do
{name, arity} ->
"." <> Identifier.inspect_as_function(name) <> "/" <> arity
:error ->
"." <> Identifier.inspect_as_function(name)
end
end
defp uniq(fun_info) do
Integer.to_string(fun_info[:new_index]) <> "." <> Integer.to_string(fun_info[:uniq])
end
end
defimpl Inspect, for: PID do
def inspect(pid, _opts) do
"#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
end
end
defimpl Inspect, for: Port do
def inspect(port, _opts) do
IO.iodata_to_binary(:erlang.port_to_list(port))
end
end
defimpl Inspect, for: Reference do
def inspect(ref, _opts) do
'#Ref' ++ rest = :erlang.ref_to_list(ref)
"#Reference" <> IO.iodata_to_binary(rest)
end
end
defimpl Inspect, for: Any do
defmacro __deriving__(module, struct, options) do
fields = Map.keys(struct) -- [:__exception__, :__struct__]
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
inspect_module =
if fields == only and except == [] do
Inspect.Map
else
Inspect.Any
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!(name) = Identifier.inspect_as_atom(unquote(module))
unquote(inspect_module).inspect(var!(map), var!(name), var!(opts))
end
end
end
end
def inspect(%module{} = struct, opts) do
try do
module.__struct__()
rescue
_ -> Inspect.Map.inspect(struct, opts)
else
dunder ->
if Map.keys(dunder) == Map.keys(struct) do
pruned = Map.drop(struct, [:__struct__, :__exception__])
Inspect.Map.inspect(pruned, Identifier.inspect_as_atom(module), opts)
else
Inspect.Map.inspect(struct, opts)
end
end
end
def inspect(map, name, opts) do
# Use the :limit option and an extra element to force
# `container_doc/6` to append "...".
opts = %{opts | limit: min(opts.limit, map_size(map))}
map = Map.to_list(map) ++ ["..."]
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
container_doc(open, map, close, opts, &Inspect.List.keyword/2, separator: sep, break: :strict)
end
end
require Protocol
Protocol.derive(
Inspect,
Macro.Env,
only: [
:module,
:file,
:line,
:function,
:context,
:aliases,
:requires,
:functions,
:macros,
:macro_aliases,
:context_modules,
:lexical_tracker
]
)