Skip to main content

lib/pb/cel/runtime/convert.ex

defmodule PB.CEL.Runtime.Convert do
  @moduledoc false

  alias PB.CEL.Runtime.Time
  alias PB.CEL.Value

  @min_int -9_223_372_036_854_775_808
  @max_int 9_223_372_036_854_775_807
  @max_uint 18_446_744_073_709_551_615

  @type result :: {:ok, Value.t()} | {:error, String.t()}

  @spec to_bool([Value.t()]) :: result
  def to_bool([value]), do: convert(:bool, value)
  def to_bool(_values), do: no_such_overload()

  @spec to_bytes([Value.t()]) :: result
  def to_bytes([value]), do: convert(:bytes, value)
  def to_bytes(_values), do: no_such_overload()

  @spec to_double([Value.t()]) :: result
  def to_double([value]), do: convert(:double, value)
  def to_double(_values), do: no_such_overload()

  @spec to_int([Value.t()]) :: result
  def to_int([value]), do: convert(:int, value)
  def to_int(_values), do: no_such_overload()

  @spec to_string([Value.t()]) :: result
  def to_string([value]), do: convert(:string, value)
  def to_string(_values), do: no_such_overload()

  @spec to_timestamp([Value.t()]) :: result
  def to_timestamp([value]), do: convert(:timestamp, value)
  def to_timestamp(_values), do: no_such_overload()

  @spec to_duration([Value.t()]) :: result
  def to_duration([value]), do: convert(:duration, value)
  def to_duration(_values), do: no_such_overload()

  @spec to_uint([Value.t()]) :: result
  def to_uint([value]), do: convert(:uint, value)
  def to_uint(_values), do: no_such_overload()

  defp convert(:bool, {:bool, value}), do: {:ok, Value.bool(value)}

  defp convert(:bool, {:string, value})
       when value in ["1", "t", "T", "true", "TRUE", "True"],
       do: {:ok, Value.bool(true)}

  defp convert(:bool, {:string, value})
       when value in ["0", "f", "F", "false", "FALSE", "False"],
       do: {:ok, Value.bool(false)}

  defp convert(:bytes, {:bytes, value}), do: {:ok, Value.bytes(value)}
  defp convert(:bytes, {:string, value}), do: {:ok, Value.bytes(value)}
  defp convert(:double, {:double, value}), do: {:ok, Value.double(value)}
  defp convert(:double, {:int, value}), do: {:ok, Value.double(value * 1.0)}
  defp convert(:double, {:uint, value}), do: {:ok, Value.double(value * 1.0)}
  defp convert(:double, {:string, "NaN"}), do: {:ok, Value.double(:nan)}
  defp convert(:double, {:string, "Infinity"}), do: {:ok, Value.double(:infinity)}
  defp convert(:double, {:string, "-Infinity"}), do: {:ok, Value.double(:negative_infinity)}

  defp convert(:double, {:string, value}) do
    case Float.parse(value) do
      {double, ""} -> {:ok, Value.double(double)}
      _other -> {:error, "cannot convert string to double"}
    end
  end

  defp convert(:int, {:int, value}), do: {:ok, Value.int(value)}
  defp convert(:int, {:enum, _type, value}), do: {:ok, Value.int(value)}
  defp convert(:int, {:uint, value}) when value <= @max_int, do: {:ok, Value.int(value)}
  defp convert(:int, {:uint, _value}), do: {:error, "uint value is out of int range"}

  defp convert(:int, {:double, value}) when value > @min_int and value < @max_int do
    {:ok, Value.int(trunc(value))}
  end

  defp convert(:int, {:double, _value}), do: {:error, "double value is out of int range"}

  defp convert(:int, {:string, value}) do
    with {int, ""} <- Integer.parse(value),
         true <- int >= @min_int and int <= @max_int do
      {:ok, Value.int(int)}
    else
      _other -> {:error, "cannot convert string to int"}
    end
  end

  defp convert(:string, {:string, value}), do: {:ok, Value.string(value)}
  defp convert(:string, {:bool, value}), do: {:ok, Value.string(Kernel.to_string(value))}
  defp convert(:string, {:int, value}), do: {:ok, Value.string(Integer.to_string(value))}
  defp convert(:string, {:uint, value}), do: {:ok, Value.string(Integer.to_string(value))}
  defp convert(:string, {:double, :nan}), do: {:ok, Value.string("NaN")}
  defp convert(:string, {:double, :infinity}), do: {:ok, Value.string("Infinity")}
  defp convert(:string, {:double, :negative_infinity}), do: {:ok, Value.string("-Infinity")}

  defp convert(:string, {:double, value}) do
    {:ok, Value.string(:erlang.float_to_binary(value, [:short]))}
  end

  defp convert(:string, {:bytes, value}) do
    if String.valid?(value) do
      {:ok, Value.string(value)}
    else
      {:error, "bytes value is not valid UTF-8"}
    end
  end

  defp convert(:uint, {:uint, value}), do: {:ok, Value.uint(value)}
  defp convert(:uint, {:int, value}) when value >= 0, do: {:ok, Value.uint(value)}
  defp convert(:uint, {:int, _value}), do: {:error, "int value is out of uint range"}

  defp convert(:uint, {:double, value}) when value >= 0 and value < @max_uint do
    {:ok, Value.uint(trunc(value))}
  end

  defp convert(:uint, {:double, _value}), do: {:error, "double value is out of uint range"}

  defp convert(:uint, {:string, value}) do
    with {uint, ""} <- Integer.parse(value),
         true <- uint >= 0 and uint <= @max_uint do
      {:ok, Value.uint(uint)}
    else
      _other -> {:error, "cannot convert string to uint"}
    end
  end

  defp convert(target, value) when target in [:duration, :int, :string, :timestamp] do
    case Time.convert(target, value) do
      {:ok, value} -> {:ok, value}
      {:error, reason} -> {:error, reason}
      :error -> {:error, "cannot convert #{value_kind(value)} to #{target}"}
    end
  end

  defp convert(target, value), do: {:error, "cannot convert #{value_kind(value)} to #{target}"}

  defp value_kind(:null), do: "null"
  defp value_kind({kind, _value}), do: Atom.to_string(kind)
  defp value_kind({:enum, name, _value}), do: name
  defp value_kind({:object, type_url, _value}), do: type_url
  defp value_kind({:message, name, _fields}), do: name
  defp value_kind(other), do: inspect(other)

  defp no_such_overload, do: {:error, "no such overload"}
end