lib/ash/ci_string.ex

defmodule Ash.CiString do
  @moduledoc """
  Represents a case insensitive string

  While some data layers are aware of case insensitive string types, in order for values
  of this type to be used in other parts of Ash Framework, it has to be embedded in a module
  this allows us to implement the `Comparable` protocol for it.

  For the type implementation, see `Ash.Type.CiString`
  """

  defstruct [:string, casted?: false, case: nil]

  @type t :: %__MODULE__{
          string: String.t(),
          casted?: boolean(),
          case: nil | :lower | :upper
        }

  @doc "Creates a case insensitive string"
  def sigil_i(value, mods) do
    cond do
      ?l in mods ->
        new(value, :lower)

      ?u in mods ->
        new(value, :upper)

      true ->
        new(value)
    end
  end

  defimpl Jason.Encoder do
    def encode(ci_string, opts) do
      ci_string
      |> Ash.CiString.value()
      |> Jason.Encode.string(opts)
    end
  end

  defimpl String.Chars do
    def to_string(ci_string) do
      Ash.CiString.value(ci_string)
    end
  end

  defimpl Inspect do
    import Inspect.Algebra

    def inspect(%Ash.CiString{string: string}, opts) do
      concat(["#Ash.CiString<", to_doc(string, opts), ">"])
    end
  end

  def new(value, casing \\ nil) do
    case casing do
      :upper ->
        %Ash.CiString{casted?: true, string: value && String.upcase(value)}

      :lower ->
        %Ash.CiString{casted?: true, string: value && String.downcase(value)}

      nil ->
        %Ash.CiString{casted?: false, string: value}
    end
  end

  def value(%Ash.CiString{string: value, casted?: false, case: :lower}) do
    value && String.downcase(value)
  end

  def value(%Ash.CiString{string: value, casted?: false, case: :upper}) do
    value && String.upcase(value)
  end

  def value(%Ash.CiString{string: value}) do
    value
  end

  def compare(left, right) do
    do_compare(to_comparable_string(left), to_comparable_string(right))
  end

  defp do_compare(left, right) when left == right, do: :eq
  defp do_compare(left, right) when left < right, do: :lt
  defp do_compare(_, _), do: :gt

  @doc "Returns the downcased value, only downcasing if it hasn't already been done"
  def to_comparable_string(value) when is_binary(value) do
    String.downcase(value)
  end

  def to_comparable_string(%__MODULE__{case: :lower, casted?: true, string: value}) do
    value
  end

  def to_comparable_string(%__MODULE__{string: value}) do
    value && String.downcase(value)
  end

  def to_comparable_string(nil), do: nil
end

import Ash.Type.Comparable

defcomparable left :: Ash.CiString, right :: BitString do
  Ash.CiString.compare(left, right)
end

defcomparable left :: Ash.CiString, right :: Ash.CiString do
  Ash.CiString.compare(left, right)
end