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

400 lines
11 KiB
Elixir

defmodule Protocol do
@moduledoc false
# We need to use :lists because Enum is not available yet
require :lists, as: L
@doc """
Handle `defprotocol`. It will define a function for each
protocol plus two extra functions:
* `__protocol__/1` - returns the protocol name when :name is given,
and a keyword list with the protocol functions
when :functions is given;
* `__impl_for__/1` - receives one argument and returns a module
that implements the protocol for the given
data type. If no implementation matches, returns nil;
* `__impl_for__!/1` - same as above but raises an error if an implementation is not found
"""
def defprotocol(name, [do: block]) do
quote do
defmodule unquote(name) do
# We don't allow function definition inside protocols
import Kernel, except: [
defmacrop: 1, defmacrop: 2, defmacrop: 4,
defmacro: 1, defmacro: 2, defmacro: 4,
defp: 1, defp: 2, defp: 4,
def: 1, def: 2, def: 4
]
# Import the new dsl that holds the new def
import Protocol.DSL
# Set up a clear slate to store defined functions
@functions []
# Invoke the user given block
unquote(block)
# Define callbacks and meta information
{ conversions, fallback, returns_nil } = Protocol.conversions_for(__MODULE__, @only, @except)
Protocol.impl_for(conversions, fallback, returns_nil, __ENV__)
Protocol.meta(@functions, conversions, fallback, returns_nil, __ENV__)
end
end
end
@doc """
Implement the given protocol for the given module.
It also defines a `__impl__` function which
returns the protocol being implemented.
"""
def defimpl(protocol, opts) do
do_defimpl(protocol, :lists.keysort(1, opts))
end
defp do_defimpl(protocol, [do: block, for: for]) when is_list(for) do
lc f inlist for, do: do_defimpl(protocol, [do: block, for: f])
end
defp do_defimpl(protocol, [do: block, for: for]) do
quote do
protocol = unquote(protocol)
for = unquote(for)
name = Module.concat(protocol, for)
Protocol.assert_protocol(protocol)
defmodule name do
@behaviour unquote(protocol)
unquote(block)
def __impl__, do: unquote(protocol)
end
end
end
# Check if the given module is a protocol. Raises an error
# if not loaded or not a protocol.
@doc false
def assert_protocol(module) do
case Code.ensure_compiled(module) do
{ :module, ^module } -> nil
_ -> raise ArgumentError, message: "#{module} is not loaded"
end
try do
module.__protocol__(:name)
rescue
UndefinedFunctionError ->
raise ArgumentError, message: "#{module} is not a protocol"
end
end
# Implements the function that detects the protocol and returns
# the module to dispatch to. Returns module.Record for records
# which should be properly handled by the dispatching function.
@doc false
def impl_for(conversions, fallback, returns_nil, env) do
contents = lc kind inlist conversions do
each_impl_for(kind, conversions, fallback)
end
if returns_nil do
contents = contents ++ [quote do
defp __raw_impl__(_) do
nil
end
end]
end
Module.eval_quoted env, contents
end
# Defines meta information about the protocol and internal callbacks.
@doc false
def meta(functions, conversions, fallback, returns_nil, env) do
any = L.keyfind(Any, 1, conversions) != false
meta = quote do
unless Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
@type t :: unquote(generate_type(conversions, any))
end
def __protocol__(:name), do: __MODULE__
def __protocol__(:functions), do: unquote(:lists.sort(functions))
end
impl_for = if L.keyfind(Record, 1, conversions) do
quote do
def __impl_for__(arg) do
case __raw_impl__(arg) do
__MODULE__.Record ->
target = Module.concat(__MODULE__, :erlang.element(1, arg))
try do
target.__impl__
target
catch
:error, :undef, [[{ ^target, :__impl__, [], _ }|_]|_] ->
unquote(fallback)
end
other ->
other
end
end
end
else
quote do
def __impl_for__(arg), do: __raw_impl__(arg)
end
end
impl_bang = if returns_nil do
quote do
def __impl_for__!(arg) do
__impl_for__(arg) || raise(Protocol.UndefinedError, protocol: __MODULE__, structure: arg)
end
end
else
quote do
def __impl_for__!(arg), do: __impl_for__(arg)
end
end
Module.eval_quoted env, [meta, impl_for, impl_bang]
end
# Returns the default conversions according to the given
# only/except options.
@doc false
def conversions_for(module, only, except) do
kinds = all_types
conversions =
if only do
L.map(fn i -> L.keyfind(i, 1, kinds) end, only)
else
except = except || [Any]
L.foldl(fn i, list -> L.keydelete(i, 1, list) end, kinds, except)
end
fallback = cond do
L.keyfind(Tuple, 1, conversions) ->
Module.concat module, Tuple
L.keyfind(Any, 1, conversions) ->
Module.concat module, Any
true ->
nil
end
# If any is not in the list and we don't implement all
# protocols, we need to handle nil cases.
returns_nil = L.keyfind(Any, 1, conversions) == false and length(conversions) < 10
{ conversions, fallback, returns_nil }
end
## Helpers
defp generate_type(_conversions, true) do
quote(do: any)
end
defp generate_type(conversions, false) do
or_function = fn({ _, _, x }, acc) -> { :|, 0, [acc, x] } end
{ _, _, first } = hd(conversions)
:lists.foldl(or_function, first, tl(conversions))
end
defp all_types do
[ { Record, :is_record, quote do: tuple },
{ Tuple, :is_tuple, quote do: tuple },
{ Atom, :is_atom, quote do: atom },
{ List, :is_list, quote do: list },
{ BitString, :is_bitstring, quote do: <<>> },
{ Number, :is_number, quote do: number },
{ Function, :is_function, quote do: fun },
{ PID, :is_pid, quote do: pid },
{ Port, :is_port, quote do: port },
{ Reference, :is_reference, quote do: reference },
{ Any, :is_any, quote do: any } ]
end
# Returns a quoted expression that allows to check
# if the first item in the tuple is a built-in or not.
defp is_builtin?([{h,_,_}]) do
quote do
first == unquote(h)
end
end
defp is_builtin?([{h,_,_}|t]) do
quote do
first == unquote(h) or unquote(is_builtin?(t))
end
end
# Specially handle records in the case we have fallbacks.
defp each_impl_for({ _, :is_record, _ }, conversions, fallback) do
quote do
defp __raw_impl__(arg) when is_tuple(arg) and is_atom(:erlang.element(1, arg)) do
first = :erlang.element(1, arg)
case unquote(is_builtin?(conversions)) do
true -> unquote(fallback)
false ->
case atom_to_list(first) do
'Elixir-' ++ _ -> __MODULE__.Record
_ -> unquote(fallback)
end
end
end
end
end
# Special case any as we don't need to generate a guard.
defp each_impl_for({ _, :is_any, _ }, _, _) do
quote do
defp __raw_impl__(_) do
__MODULE__.Any
end
end
end
# Generate all others protocols.
defp each_impl_for({ kind, fun, _ }, _, _) do
quote do
defp __raw_impl__(arg) when unquote(fun).(arg) do
__MODULE__.unquote(kind)
end
end
end
end
defmodule Protocol.DSL do
@moduledoc false
# We need to use :lists because Enum is not available yet
require :lists, as: L
@doc false
def args_and_body(module, name, arity) do
# Generate arguments according the arity. The arguments
# are named xa, xb and so forth. We cannot use string
# interpolation to generate the arguments because of compile
# dependencies, so we use the <<>> instead.
args = lc i inlist :lists.seq(1, arity) do
{ binary_to_atom(<<?x, i + 64>>), 0, :quoted }
end
{ conversions, fallback, returns_nil } = conversions_for(module)
clauses = [default_clause(name, args)]
if returns_nil do
clauses = [nil_clause()|clauses]
end
if L.keyfind(Record, 1, conversions) do
clauses = [record_clause(name, args, fallback)|clauses]
end
body =
quote do
case __raw_impl__(xA), do: unquote({ :->, 0, clauses })
end
{ args, body }
end
@doc false
def callback_from_spec(module, name, arity) do
tuple = { name, arity }
specs = Module.get_attribute(module, :spec)
found = lc { k, v } inlist specs, k == tuple do
Kernel.Typespec.define_callback(module, tuple, v)
true
end
found != []
end
defp conversions_for(module) do
only = Module.get_attribute(module, :only)
except = Module.get_attribute(module, :except)
Protocol.conversions_for(module, only, except)
end
defp default_clause(name, args) do
quote do: { [other], apply(other, unquote(name), [unquote_splicing(args)]) }
end
defp nil_clause() do
quote do
{ [nil], raise(Protocol.UndefinedError, protocol: __MODULE__, structure: xA) }
end
end
defp record_clause(name, args, fallback) do
arity = length(args)
quote do
{ [__MODULE__.Record],
(target = Module.concat(__MODULE__, :erlang.element(1, xA))
try do
target.unquote(name)(unquote_splicing(args))
catch
:error, :undef, [[{ ^target, name, args, _ }|_]|_] when
name == unquote(name) and length(args) == unquote(arity) ->
unquote(catch_clause(args, fallback))
end) }
end
end
defp catch_clause(args, fallback) do
if fallback do
quote do
apply unquote(fallback), name, [unquote_splicing(args)]
end
else
quote do
raise Protocol.UndefinedError, protocol: __MODULE__, structure: xA
end
end
end
defmacro def(expression) do
case expression do
{ _, _, args } when args == [] or is_atom(args) ->
raise ArgumentError, message: "protocol functions expect at least one argument"
{ name, _, args } when is_atom(name) and is_list(args) ->
:ok
_ ->
raise ArgumentError, message: "invalid args for defprotocol"
end
arity = length(args)
type_args = lc _ inlist :lists.seq(2, arity), do: quote(do: term)
type_args = [quote(do: t) | type_args]
quote do
name = unquote(name)
arity = unquote(arity)
@functions [{name, arity}|@functions]
# Generate a fake definition with the user
# signature that will be used by docs
Kernel.def unquote(name).(unquote_splicing(args))
{ args, body } = Protocol.DSL.args_and_body(__MODULE__, name, arity)
Kernel.def unquote(name), args, [], do: body
# Convert the spec to callback if possible,
# otherwise generate a dummy callback
Protocol.DSL.callback_from_spec(__MODULE__, name, arity) ||
@callback unquote(name)(unquote_splicing(type_args)), do: term
end
end
end