defmodule Once.Parse do @moduledoc false use Once.Constants import Once.Encode alias Once.Type @doc """ Convert a value from format_in to format_out. If they are the same format, the format is validated. """ @spec maybe_convert(Type.id(), Once.format(), Once.format()) :: :error | {:ok, Type.id()} def maybe_convert(value, format_in, format_out) def maybe_convert(value, :raw, :raw), do: {:ok, value} def maybe_convert(value, :url64, :url64) do if valid64?(value), do: {:ok, value}, else: :error end def maybe_convert(value, :hex, :hex) do if valid16?(value), do: {:ok, value}, else: :error end def maybe_convert(value, :hex32, :hex32) do if valid32?(value), do: {:ok, value}, else: :error end def maybe_convert(value, format_in, format_out) when format_in in @int_formats and format_out in @int_formats do convert_int(value, format_out) end def maybe_convert(value, format_in, format_out) do value |> to_raw(format_in) |> case do {:ok, raw} -> {:ok, from_raw(raw, format_out)} _ -> :error end end @doc """ Identify the value's format. All ints are grouped together for convert_int/2 to deal with. """ @spec identify_format(Type.id()) :: {:ok, Once.format()} | :error def identify_format(value) def identify_format(<<_::88>>), do: {:ok, :url64} def identify_format(<<_::64>>), do: {:ok, :raw} def identify_format(<<_::128>>), do: {:ok, :hex} def identify_format(<<_::104>>), do: {:ok, :hex32} def identify_format(int) when is_integer(int), do: {:ok, :unsigned} def identify_format(_), do: :error @doc """ Parse value as a numeric string if the second arg is true. """ @spec maybe_parse_num_str(Type.id(), boolean()) :: {:ok, Type.id()} | :error def maybe_parse_num_str(value, parse_int_opt) def maybe_parse_num_str(value, true) when is_binary(value), do: parse_num_str(value) def maybe_parse_num_str(value, _), do: {:ok, value} @doc """ Convert from a raw 8-byte binary to any format. """ @spec from_raw(<<_::64>>, Once.format()) :: Type.id() def from_raw(raw, to_format) def from_raw(raw, :raw), do: raw def from_raw(raw, :url64), do: encode64(raw) def from_raw(raw, :hex), do: encode16(raw) def from_raw(raw, :hex32), do: encode32(raw) def from_raw(<>, :signed), do: int def from_raw(<>, :unsigned), do: int @doc """ Convert from any format to a raw 8-byte binary. """ @spec to_raw(Type.id(), Once.format()) :: {:ok, <<_::64>>} | :error def to_raw(value, from_format) def to_raw(value, :raw), do: {:ok, value} def to_raw(value, :url64), do: decode64(value) def to_raw(value, :hex), do: decode16(value) def to_raw(value, :hex32), do: decode32(value) def to_raw(value, int_format) when int_format in @int_formats do case convert_int(value, :unsigned) do {:ok, int} -> {:ok, <>} _ -> :error end end ########### # Private # ########### # convert a signed to unsigned int and back defp convert_int(int, format) defp convert_int(int, _) when int < @signed_min, do: :error defp convert_int(int, _) when int > @unsigned_max, do: :error defp convert_int(int, :signed) when int > @signed_max, do: {:ok, int - @bigint_size} defp convert_int(int, :unsigned) when int < @unsigned_min, do: {:ok, int + @bigint_size} defp convert_int(int, _), do: {:ok, int} defp parse_num_str(value) do try do {:ok, String.to_integer(value)} rescue _ -> :error end end end