lib/rdf.ex

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

  RDF.ex consists of:

  - structs for the nodes of an RDF graph
    - `RDF.IRI`
    - `RDF.BlankNode`
    - `RDF.Literal`
  - various modules making working with the nodes of an RDF graph easier
    - `RDF.Term`
    - `RDF.Sigils`
    - `RDF.Guards`
  - 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`
  - structs 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.

  It also can be `use`'ed to add basic imports and aliases for working with RDF.ex
  on any module. See `__using__/1` for details.

  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.compile_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.compile_env(:rdf, :default_prefixes, %{}) do
    {mod, fun} ->
      if Application.compile_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.compile_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`, i.e. a `RDF.IRI` or `RDF.BlankNode`.

  ## 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`, i.e. a `RDF.IRI`, `RDF.BlankNode` or `RDF.Literal`.

  ## 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), to: RDF.NS.RDF
    @doc false
    defdelegate unquote(term)(s, o), 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
  defdelegate __terms__(), to: RDF.NS.RDF
  defdelegate __iris__(), to: RDF.NS.RDF
  defdelegate __strict__(), to: RDF.NS.RDF
  defdelegate __resolve_term__(term), to: RDF.NS.RDF

  @doc """
  Adds basic imports and aliases for working with RDF.ex.

  This allows to write

      use RDF

  instead of the following

      import RDF.Sigils
      import RDF.Guards
      import RDF.Namespace.IRI
    
      require RDF.Graph

      alias RDF.{XSD, NTriples, NQuads, Turtle}

  """
  defmacro __using__(_opts) do
    quote do
      import RDF.Sigils
      import RDF.Guards
      import RDF.Namespace.IRI

      require RDF.Graph

      alias RDF.{XSD, NTriples, NQuads, Turtle}
    end
  end
end