ash/lib/ash/ci_string.ex
Zach Daniel 01abe80a04 improvement: revamp ci_string
previously, ci_string would downcase all input/output automatically,
throwing away the "representation" of the original input. Now, that
is an option provided as a constraint, for example:

`attribute :email, :ci_string, constraints: [casing: :lower]`
or
`attribute :serial, :ci_string, constraints: [casing: :upper]`

All comparison logic remains the same, so the only thing that is affected
is the `to_string(value)` logic, which now returns based on the configured casing.
By default its just the value, but with `lower`/`upper` it will downcase/upcase the
value accordingly.
2021-06-25 13:19:14 -04:00

106 lines
2.5 KiB
Elixir

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]
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: :lt
defp do_compare(left, right) when left == right, do: :eq
defp do_compare(_, _), do: :gt
@doc "Returns the downcased value, only downcasing if it hasn't already been down"
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
use Comp
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