lib/rdf.ex

defmodule RDF do
  @moduledoc """
  The top-level module of RDF.ex.

  RDF.ex consists of:

  - modules for the nodes of an RDF graph
    - `RDF.Term`
    - `RDF.IRI`
    - `RDF.BlankNode`
    - `RDF.Literal`
  - the `RDF.Literal.Datatype` system
  - a facility for the mapping of URIs of a vocabulary to Elixir modules and
    functions: `RDF.Vocabulary.Namespace`
  - a facility for the automatic generation of resource identifiers: `RDF.Resource.Generator`
  - modules for the construction of statements
    - `RDF.Triple`
    - `RDF.Quad`
    - `RDF.Statement`
  - modules for collections of statements
    - `RDF.Description`
    - `RDF.Graph`
    - `RDF.Dataset`
    - `RDF.Data`
    - `RDF.List`
    - `RDF.Diff`
  - functions to construct and execute basic graph pattern queries: `RDF.Query`
  - functions for working with RDF serializations: `RDF.Serialization`
  - behaviours for the definition of RDF serialization formats
    - `RDF.Serialization.Format`
    - `RDF.Serialization.Decoder`
    - `RDF.Serialization.Encoder`
  - and the implementation of various RDF serialization formats
    - `RDF.NTriples`
    - `RDF.NQuads`
    - `RDF.Turtle`

  This top-level module provides shortcut functions for the construction of the
  basic elements and structures of RDF and some general helper functions.

  For a general introduction you may refer to the guides on the [homepage](https://rdf-elixir.dev).
  """

  alias RDF.{
    IRI,
    BlankNode,
    Literal,
    Namespace,
    Description,
    Graph,
    Dataset,
    Serialization,
    PrefixMap
  }

  import RDF.Guards
  import RDF.Utils.Bootstrapping

  @star? Application.get_env(:rdf, :star, true)
  @doc """
  Returns whether RDF-star support is enabled.
  """
  def star?(), do: @star?

  defdelegate default_base_iri(), to: IRI, as: :default_base

  @standard_prefixes PrefixMap.new(
                       xsd: xsd_iri_base(),
                       rdf: rdf_iri_base(),
                       rdfs: rdfs_iri_base()
                     )

  @doc """
  A fixed set prefixes that will always be part of the `default_prefixes/0`.

  ```elixir
  #{inspect(@standard_prefixes, pretty: true)}
  ```

  See `default_prefixes/0`, if you don't want these standard prefixes to be part
  of the default prefixes.
  """
  def standard_prefixes(), do: @standard_prefixes

  @doc """
  A user-defined `RDF.PrefixMap` of prefixes to IRI namespaces.

  This prefix map will be used implicitly wherever a prefix map is expected, but
  not provided. For example, when you don't pass a prefix map to the Turtle serializer,
  this prefix map will be used.

  By default the `standard_prefixes/0` are part of this prefix map, but you can
  define additional default prefixes via the `default_prefixes` compile-time
  configuration.

  For example:

      config :rdf,
        default_prefixes: %{
          ex: "http://example.com/"
        }

  You can also set `:default_prefixes` to a module-function tuple `{mod, fun}`
  with a function which should be called to determine the default prefixes.

  If you don't want the `standard_prefixes/0` to be part of the default prefixes,
  or you want to map the standard prefixes to different namespaces (strongly discouraged!),
  you can set the `use_standard_prefixes` compile-time configuration flag to `false`.

      config :rdf,
        use_standard_prefixes: false

  """
  case Application.get_env(:rdf, :default_prefixes, %{}) do
    {mod, fun} ->
      if Application.get_env(:rdf, :use_standard_prefixes, true) do
        def default_prefixes() do
          PrefixMap.merge!(@standard_prefixes, apply(unquote(mod), unquote(fun), []))
        end
      else
        def default_prefixes(), do: apply(unquote(mod), unquote(fun), [])
      end

    default_prefixes ->
      @default_prefixes PrefixMap.new(default_prefixes)
      if Application.get_env(:rdf, :use_standard_prefixes, true) do
        def default_prefixes() do
          PrefixMap.merge!(@standard_prefixes, @default_prefixes)
        end
      else
        def default_prefixes(), do: @default_prefixes
      end
  end

  @doc """
  Returns the `default_prefixes/0` with additional prefix mappings.

  The `prefix_mappings` can be given in any format accepted by `RDF.PrefixMap.new/1`.
  """
  def default_prefixes(prefix_mappings) do
    default_prefixes() |> PrefixMap.merge!(prefix_mappings)
  end

  defdelegate read_string(string, opts), to: Serialization
  defdelegate read_string!(string, opts), to: Serialization
  defdelegate read_stream(stream, opts \\ []), to: Serialization
  defdelegate read_stream!(stream, opts \\ []), to: Serialization
  defdelegate read_file(filename, opts \\ []), to: Serialization
  defdelegate read_file!(filename, opts \\ []), to: Serialization
  defdelegate write_string(data, opts), to: Serialization
  defdelegate write_string!(data, opts), to: Serialization
  defdelegate write_stream(data, opts), to: Serialization
  defdelegate write_file(data, filename, opts \\ []), to: Serialization
  defdelegate write_file!(data, filename, opts \\ []), to: Serialization

  @doc """
  Checks if the given value is a RDF resource.

  ## Examples

  Supposed `EX` is a `RDF.Vocabulary.Namespace` and `Foo` is not.

      iex> RDF.resource?(RDF.iri("http://example.com/resource"))
      true
      iex> RDF.resource?(EX.resource)
      true
      iex> RDF.resource?(EX.Resource)
      true
      iex> RDF.resource?(Foo.Resource)
      false
      iex> RDF.resource?(RDF.bnode)
      true
      iex> RDF.resource?(RDF.XSD.integer(42))
      false
      iex> RDF.resource?(42)
      false
  """
  def resource?(value)
  def resource?(%IRI{}), do: true
  def resource?(%BlankNode{}), do: true

  def resource?(qname) when maybe_ns_term(qname) do
    case Namespace.resolve_term(qname) do
      {:ok, iri} -> resource?(iri)
      _ -> false
    end
  end

  if @star? do
    def resource?({_, _, _} = triple), do: RDF.Triple.valid?(triple)
  end

  def resource?(_), do: false

  @doc """
  Checks if the given value is a RDF term.

  ## Examples

  Supposed `EX` is a `RDF.Vocabulary.Namespace` and `Foo` is not.

      iex> RDF.term?(RDF.iri("http://example.com/resource"))
      true
      iex> RDF.term?(EX.resource)
      true
      iex> RDF.term?(EX.Resource)
      true
      iex> RDF.term?(Foo.Resource)
      false
      iex> RDF.term?(RDF.bnode)
      true
      iex> RDF.term?(RDF.XSD.integer(42))
      true
      iex> RDF.term?(42)
      false
  """
  def term?(value)
  def term?(%Literal{}), do: true
  def term?(value), do: resource?(value)

  defdelegate uri?(value), to: IRI, as: :valid?
  defdelegate iri?(value), to: IRI, as: :valid?
  defdelegate uri(value), to: IRI, as: :new
  defdelegate iri(value), to: IRI, as: :new
  defdelegate uri!(value), to: IRI, as: :new!
  defdelegate iri!(value), to: IRI, as: :new!

  @doc """
  Checks if the given value is a blank node.

  ## Examples

      iex> RDF.bnode?(RDF.bnode)
      true
      iex> RDF.bnode?(RDF.iri("http://example.com/resource"))
      false
      iex> RDF.bnode?(42)
      false
  """
  def bnode?(%BlankNode{}), do: true
  def bnode?(_), do: false

  defdelegate bnode(), to: BlankNode, as: :new
  defdelegate bnode(id), to: BlankNode, as: :new

  @doc """
  Checks if the given value is a RDF literal.
  """
  def literal?(%Literal{}), do: true
  def literal?(_), do: false

  defdelegate literal(value), to: Literal, as: :new
  defdelegate literal(value, opts), to: Literal, as: :new

  if @star? do
    alias RDF.Star.{Triple, Quad, Statement}

    defdelegate triple(s, p, o, property_map \\ nil), to: Triple, as: :new
    defdelegate triple(tuple, property_map \\ nil), to: Triple, as: :new

    defdelegate quad(s, p, o, g, property_map \\ nil), to: Quad, as: :new
    defdelegate quad(tuple, property_map \\ nil), to: Quad, as: :new

    defdelegate statement(s, p, o), to: Statement, as: :new
    defdelegate statement(s, p, o, g), to: Statement, as: :new
    defdelegate statement(tuple, property_map \\ nil), to: Statement, as: :new

    defdelegate coerce_subject(subject, property_map \\ nil), to: Statement
    defdelegate coerce_predicate(predicate), to: Statement
    defdelegate coerce_predicate(predicate, property_map), to: Statement
    defdelegate coerce_object(object, property_map \\ nil), to: Statement
    defdelegate coerce_graph_name(graph_name), to: Statement
  else
    alias RDF.{Triple, Quad, Statement}

    defdelegate triple(s, p, o, property_map \\ nil), to: Triple, as: :new
    defdelegate triple(tuple, property_map \\ nil), to: Triple, as: :new

    defdelegate quad(s, p, o, g, property_map \\ nil), to: Quad, as: :new
    defdelegate quad(tuple, property_map \\ nil), to: Quad, as: :new

    defdelegate statement(s, p, o), to: Statement, as: :new
    defdelegate statement(s, p, o, g), to: Statement, as: :new
    defdelegate statement(tuple, property_map \\ nil), to: Statement, as: :new

    defdelegate coerce_subject(subject), to: Statement
    defdelegate coerce_predicate(predicate), to: Statement
    defdelegate coerce_predicate(predicate, property_map), to: Statement
    defdelegate coerce_object(object), to: Statement
    defdelegate coerce_graph_name(graph_name), to: Statement
  end

  defdelegate description(subject, opts \\ []), to: Description, as: :new

  defdelegate graph(), to: Graph, as: :new
  defdelegate graph(arg), to: Graph, as: :new
  defdelegate graph(arg1, arg2), to: Graph, as: :new

  defdelegate dataset(), to: Dataset, as: :new
  defdelegate dataset(arg), to: Dataset, as: :new
  defdelegate dataset(arg1, arg2), to: Dataset, as: :new

  defdelegate diff(arg1, arg2), to: RDF.Diff

  defdelegate list?(resource, graph), to: RDF.List, as: :node?
  defdelegate list?(description), to: RDF.List, as: :node?

  def list(native_list), do: RDF.List.from(native_list)
  def list(head, %Graph{} = graph), do: RDF.List.new(head, graph)
  def list(native_list, opts), do: RDF.List.from(native_list, opts)

  defdelegate prefix_map(prefixes), to: RDF.PrefixMap, as: :new
  defdelegate property_map(property_map), to: RDF.PropertyMap, as: :new

  ############################################################################
  # These alias functions for the RDF.NS.RDF namespace are mandatory.
  # Without them the property functions are inaccessible, since the namespace
  # can't be aliased, because it gets in conflict with the root namespace
  # of the project.

  defdelegate langString(value, opts), to: RDF.LangString, as: :new
  defdelegate lang_string(value, opts), to: RDF.LangString, as: :new

  for term <- ~w[type subject predicate object first rest value]a do
    defdelegate unquote(term)(), to: RDF.NS.RDF
    @doc false
    defdelegate unquote(term)(s, o), to: RDF.NS.RDF
    @doc false
    defdelegate unquote(term)(s, o1, o2), to: RDF.NS.RDF
    @doc false
    defdelegate unquote(term)(s, o1, o2, o3), to: RDF.NS.RDF
    @doc false
    defdelegate unquote(term)(s, o1, o2, o3, o4), to: RDF.NS.RDF
    @doc false
    defdelegate unquote(term)(s, o1, o2, o3, o4, o5), to: RDF.NS.RDF
  end

  defdelegate langString(), to: RDF.NS.RDF
  defdelegate lang_string(), to: RDF.NS.RDF, as: :langString
  defdelegate unquote(nil)(), to: RDF.NS.RDF

  defdelegate __base_iri__(), to: RDF.NS.RDF
end