lib/rdf/model/literal/datatypes/generic.ex

defmodule RDF.Literal.Generic do
  @moduledoc """
  A generic `RDF.Literal.Datatype` for literals of an unknown datatype.
  """

  defstruct [:value, :datatype]

  use RDF.Literal.Datatype,
    name: "generic",
    id: nil

  alias RDF.Literal.Datatype
  alias RDF.{Literal, IRI}
  import RDF.Guards

  @type t :: %__MODULE__{
          value: String.t(),
          datatype: String.t()
        }

  @impl Datatype
  @spec new(any, String.t() | IRI.t() | keyword) :: Literal.t()
  def new(value, datatype_or_opts \\ [])
  def new(value, %IRI{} = datatype), do: new(value, datatype: datatype)

  def new(value, datatype) when is_binary(datatype) or maybe_ns_term(datatype),
    do: new(value, datatype: datatype)

  def new(value, opts) do
    %Literal{
      literal: %__MODULE__{
        value: value,
        datatype: Keyword.get(opts, :datatype) |> normalize_datatype()
      }
    }
  end

  defp normalize_datatype(nil), do: nil
  defp normalize_datatype(""), do: nil
  defp normalize_datatype(%IRI{} = datatype), do: to_string(datatype)

  defp normalize_datatype(datatype) when maybe_ns_term(datatype),
    do: datatype |> RDF.iri() |> to_string()

  defp normalize_datatype(datatype), do: datatype

  @impl Datatype
  @spec new!(any, String.t() | IRI.t() | keyword) :: Literal.t()
  def new!(value, datatype_or_opts \\ []) do
    literal = new(value, datatype_or_opts)

    if valid?(literal) do
      literal
    else
      raise ArgumentError,
            "#{inspect(value)} with datatype #{inspect(literal.literal.datatype)} is not a valid #{inspect(__MODULE__)}"
    end
  end

  @impl Datatype
  def datatype_id(%Literal{literal: literal}), do: datatype_id(literal)
  def datatype_id(%__MODULE__{} = literal), do: RDF.iri(literal.datatype)

  @impl Datatype
  def value(%Literal{literal: literal}), do: value(literal)
  def value(%__MODULE__{} = literal), do: literal.value

  @impl Datatype
  def lexical(%Literal{literal: literal}), do: lexical(literal)
  def lexical(%__MODULE__{} = literal), do: literal.value

  @impl Datatype
  def canonical(%Literal{literal: %__MODULE__{}} = literal), do: literal
  def canonical(%__MODULE__{} = literal), do: literal(literal)

  @impl Datatype
  def canonical?(%Literal{literal: literal}), do: canonical?(literal)
  def canonical?(%__MODULE__{}), do: true

  @impl Datatype
  def valid?(%Literal{literal: %__MODULE__{} = literal}), do: valid?(literal)
  def valid?(%__MODULE__{datatype: datatype}) when is_binary(datatype), do: true
  def valid?(_), do: false

  @doc """
  Since generic literals don't support casting, always returns `nil`.
  """
  def cast(_), do: nil

  @impl Datatype
  def do_cast(_), do: nil

  @impl Datatype
  def do_equal_value_same_or_derived_datatypes?(
        %{datatype: datatype} = left,
        %{datatype: datatype} = right
      ),
      do: left == right

  def do_equal_value_same_or_derived_datatypes?(_, _), do: nil

  @impl Datatype
  def do_compare(
        %__MODULE__{datatype: datatype} = left_literal,
        %__MODULE__{datatype: datatype} = right_literal
      ) do
    case {left_literal.value, right_literal.value} do
      {left_value, right_value} when left_value < right_value ->
        :lt

      {left_value, right_value} when left_value > right_value ->
        :gt

      _ ->
        if equal_value?(left_literal, right_literal), do: :eq
    end
  end

  def do_compare(_, _), do: nil

  @impl Datatype
  def update(literal, fun, opts \\ [])
  def update(%Literal{literal: literal}, fun, opts), do: update(literal, fun, opts)

  def update(%__MODULE__{} = literal, fun, _opts) do
    literal
    |> value()
    |> fun.()
    |> new(datatype: literal.datatype)
  end
end