lib/validators/protocols/length.ex

defprotocol DataLength do
  @moduledoc """
  DataLength protocol defines a function to retrieve the length of data for various data types.
  Implemenations of this protocol need to implement `len(data)` function.

  ## Default Implementations

    The protocol provides default implementations for the following data types:
    - `String`: Returns the number of graphemes in the string.
    - `List`: Returns the number of elements in the list.
    - `Map`: Returns the number of key-value pairs at the first level of the map.
    - `Tuple`: Returns the number of elements in the tuple.
    - `Integer`: Returns the number of digits in the integer plus 1 for a minus sign if the number is negative.
    - `Float`: Returns the number of digits in the float plus 1 for a minus sign if the number is negative and 1 for a ".".


  ### Default implementations examples:

  `BitString` implementation returns the number of graphemes:
      iex> DataLength.len("Hello! Dzień dobry!")
      19

  `List` implementation returns the number of elements in the list.
      iex> DataLength.len(["a", :b, ["c"], %{d: "d"}])
      4

  `Map` implementation returns the number of key-value pairs at the first level of the map.:
      iex> DataLength.len(%{a: 1, b: [2], c: "hello"})
      3

  `Tuple` implementation returns the number of elements in the tuple:
      iex> DataLength.len({"a", :b, ["c", "d"], {"e"}, "f"})
      5

  `Integer` implementation returns the number of digits in the integer plus 1 for a minus sign if the number is negative:
      iex> DataLength.len(1234567890)
      10

      iex> DataLength.len(-1234567890)
      11

  `Float` implemantation returns the number of digits in the float plus 1 for a minus sign if the number is negative and 1 for a ".". Terminal zeros in fractional part are ignored:
      iex> DataLength.len(12345.6789)
      10

      iex> DataLength.len(12345.67890)
      10

      iex> DataLength.len(12345.67899)
      11

      iex> DataLength.len(-12345.67890)
      11

      iex> DataLength.len(-12345.67899)
      12


  ## Custom implementation:

  You can implement custom length calculations for other data types by providing specific protocol implementations.

  ### Example

        defmodule MyStruct do
          defstruct data: [1, 2, 3]
        end

        defimpl Length, for: MyStruct do
          def len(%MyStruct{data: data}), do: length(data)
        end


  """

  @doc """
  Retrieves the length of the input data.

  ## Parameters

  * `data` - The data for which to calculate the length.

  ## Returns

  An integer representing the length of the data.

  """
  @spec len(any) :: non_neg_integer
  def len(data)
end

defimpl DataLength, for: BitString do
  @spec len(binary) :: non_neg_integer
  def len(data), do: String.length(data)
end

defimpl DataLength, for: List do
  @spec len(list) :: non_neg_integer
  def len(data), do: length(data)
end

defimpl DataLength, for: Map do
  @spec len(map) :: non_neg_integer
  def len(data), do: map_size(data)
end

defimpl DataLength, for: Tuple do
  @spec len(tuple) :: non_neg_integer
  def len(data), do: tuple_size(data)
end

defimpl DataLength, for: Integer do
  @spec len(integer) :: non_neg_integer
  def len(data), do: data |> Integer.to_string() |> DataLength.len()
end

defimpl DataLength, for: Float do
  @spec len(float) :: non_neg_integer
  def len(data), do: data |> Float.to_string() |> DataLength.len()
end