defmodule PB.CEL.Checker.NameResolution do
@moduledoc false
alias PB.CEL.Env
alias PB.CEL.Checker.Types, as: CType
alias PB.CEL.Proto
alias PB.CEL.Type
alias PB.CEL.WellKnownTypes
@type resolved ::
%{
required(:kind) => :var,
required(:name) => String.t(),
required(:type) => CType.t()
}
| %{
required(:kind) => :local,
required(:name) => String.t(),
required(:type) => CType.t()
}
| %{
required(:kind) => :type,
required(:name) => String.t(),
required(:type) => CType.t(),
required(:denotation) => Type.t()
}
| %{
required(:kind) => :enum_value,
required(:name) => String.t(),
required(:type) => CType.t(),
required(:value) => Proto.value()
}
@spec resolve(map, Env.t(), [%{String.t() => CType.t()}]) :: {:ok, resolved} | :error
def resolve(expr, %Env{} = env, scopes \\ []) do
with {:ok, name, absolute?} <- qualified_name(expr) do
resolve_name(name, env, scopes, absolute?)
end
end
@spec resolve_message_name(String.t(), Env.t()) :: String.t()
def resolve_message_name("." <> name, %Env{} = env) do
case resolve_message(env, name) do
{:ok, message} -> Atom.to_string(message)
:error -> name
end
end
def resolve_message_name(name, %Env{} = env) when is_binary(name) do
candidates = candidates(name, env, false)
case Enum.find(candidates, &known_message_name?(env, &1)) do
nil -> fallback_message_name(name, env)
resolved -> resolved
end
end
@spec resolve_enum_call(PB.CEL.Call.t(), Env.t(), [%{String.t() => CType.t()}]) ::
{:ok, String.t()} | :error
def resolve_enum_call(%PB.CEL.Call{shape: :global, function: name}, %Env{} = env, scopes) do
resolve_enum_name(name, env, scopes, false)
end
def resolve_enum_call(
%PB.CEL.Call{shape: :receiver, target: target, function: function},
%Env{} = env,
scopes
) do
with {:ok, prefix, absolute?} <- qualified_name(target) do
resolve_enum_name(prefix <> "." <> function, env, scopes, absolute?)
end
end
def resolve_enum_call(%PB.CEL.Call{}, %Env{}, _scopes), do: :error
defp resolve_name(name, env, scopes, false) do
case local_resolution(name, scopes) do
{:ok, resolved} -> {:ok, resolved}
:field_selection -> :error
:error -> resolve_env_name(name, env, false)
end
end
defp resolve_name(name, env, _scopes, true), do: resolve_env_name(name, env, true)
defp resolve_env_name(name, env, absolute?) do
name
|> candidates(env, absolute?)
|> Enum.find_value(&resolve_candidate(env, &1))
|> case do
nil -> :error
resolved -> {:ok, resolved}
end
end
defp local_resolution(name, scopes) do
case String.split(name, ".", trim: true) do
[local_name] ->
fetch_local(scopes, local_name)
[local_name | _fields] ->
if local_name?(scopes, local_name), do: :field_selection, else: :error
[] ->
:error
end
end
defp fetch_local(scopes, name) do
Enum.find_value(scopes, :error, fn scope ->
case Map.fetch(scope, name) do
{:ok, type} -> {:ok, %{kind: :local, name: name, type: type}}
:error -> nil
end
end)
end
defp local_name?(scopes, name) do
Enum.any?(scopes, &Map.has_key?(&1, name))
end
defp resolve_candidate(env, name) do
case Env.fetch_var(env, name) do
{:ok, type} ->
%{kind: :var, name: name, type: type}
:error ->
resolve_schema_name(env, name) || resolve_type_denotation(name)
end
end
defp resolve_schema_name(env, name) do
case schema_enum_value(env, name) do
{:ok, enum_value} ->
enum_name = Atom.to_string(enum_value.enum)
%{
kind: :enum_value,
name: enum_name <> "." <> Atom.to_string(enum_value.value),
type: CType.message(enum_name),
value: %{kind: {:enum_value, %{type: enum_name, value: enum_value.number}}}
}
:error ->
case schema_type_denotation(env, name) do
{:ok, denotation} ->
%{
kind: :type,
name: name,
type: CType.type(CType.from_cel(denotation)),
denotation: denotation,
value: %{kind: {:type_value, schema_type_value_name(denotation)}}
}
:error ->
nil
end
end
end
defp schema_enum_value(%Env{} = env, name) do
case env.message_schema.enum_value(env.schema, name) do
{:ok, enum_value} -> {:ok, enum_value}
:unknown_enum -> :error
:unknown_value -> :error
end
end
defp resolve_type_denotation(name) do
case Type.denotation(name) do
{:ok, denotation} ->
%{
kind: :type,
name: name,
type: CType.type(CType.from_cel(denotation)),
denotation: denotation
}
:error ->
nil
end
end
defp schema_type_denotation(%Env{} = env, name) do
case resolve_message(env, name) do
{:ok, message} ->
message = Atom.to_string(message)
case WellKnownTypes.type_denotation(message) do
{:ok, denotation} -> {:ok, denotation}
:error -> {:ok, Type.message(message)}
end
:error ->
case env.message_schema.resolve_enum(env.schema, name) do
{:ok, enum} -> {:ok, Type.message(Atom.to_string(enum))}
:error -> :error
end
end
end
defp schema_type_value_name(%{type_kind: {:message_type, name}}), do: name
defp schema_type_value_name(%{type_kind: {:well_known, :TIMESTAMP}}),
do: "google.protobuf.Timestamp"
defp schema_type_value_name(%{type_kind: {:well_known, :DURATION}}),
do: "google.protobuf.Duration"
defp resolve_enum_name(name, env, scopes, absolute?) do
with :ok <- local_prefix_available(name, scopes, absolute?) do
name
|> candidates(env, absolute?)
|> Enum.find_value(fn candidate ->
case env.message_schema.resolve_enum(env.schema, candidate) do
{:ok, enum} -> Atom.to_string(enum)
:error -> nil
end
end)
|> case do
nil -> :error
enum -> {:ok, enum}
end
end
end
defp local_prefix_available(_name, _scopes, true), do: :ok
defp local_prefix_available(name, scopes, false) do
root = name |> String.split(".", parts: 2) |> hd()
if local_name?(scopes, root), do: :error, else: :ok
end
defp candidates("." <> name, _env, _absolute?), do: [name]
defp candidates(name, _env, true), do: [name]
defp candidates(name, %Env{container: container}, _absolute?) when container in ["", nil],
do: [name]
defp candidates(name, %Env{container: container}, _absolute?) do
parts = String.split(container, ".", trim: true)
container_candidates =
for size <- length(parts)..1//-1 do
parts
|> Enum.take(size)
|> Enum.join(".")
|> then(&(&1 <> "." <> name))
end
container_candidates ++ [name]
end
defp fallback_message_name(name, %Env{container: container}) do
cond do
String.contains?(name, ".") -> name
container in ["", nil] -> name
true -> container <> "." <> name
end
end
defp known_message_name?(env, name) do
WellKnownTypes.literal_message?(name) or env.message_schema.known_message?(env.schema, name)
end
defp resolve_message(%Env{} = env, name),
do: env.message_schema.resolve_message(env.schema, name)
defp qualified_name(%{expr_kind: {:ident_expr, %{name: "." <> name}}}), do: {:ok, name, true}
defp qualified_name(%{expr_kind: {:ident_expr, %{name: name}}}), do: {:ok, name, false}
defp qualified_name(%{expr_kind: {:select_expr, %{operand: operand, field: field}}}) do
with {:ok, prefix, absolute?} <- qualified_name(operand) do
{:ok, prefix <> "." <> field, absolute?}
end
end
defp qualified_name(_expr), do: :error
end