Skip to main content

lib/terminus_db/triples.ex

defmodule TerminusDB.Triples do
  @moduledoc """
  Triples (turtle) API for TerminusDB.

  Wraps the `/api/triples/{path}/{graph_type}` endpoints for reading and
  writing graph contents as turtle-encoded triples.

  All functions require a `TerminusDB.Config` scoped to a database.

  ## Quick start

      config =
        TerminusDB.Config.new(endpoint: "http://localhost:6363")
        |> TerminusDB.Config.with_database("mydb")

      {:ok, turtle} = TerminusDB.Triples.get(config)
      {:ok, _} = TerminusDB.Triples.update(config, turtle, author: "admin", message: "replace")
      {:ok, _} = TerminusDB.Triples.insert(config, more_turtle, author: "admin", message: "add")

  """

  alias TerminusDB.{Client, Config, Error}
  alias TerminusDB.Client.Params

  @type triples_opt ::
          {:graph_type, :instance | :schema}
          | {:organization, String.t()}
          | {:repo, String.t()}
          | {:author, String.t()}
          | {:message, String.t()}

  defp triples_path(config, opts) do
    org = opts[:organization] || config.organization
    db = config.database || raise Error, reason: :http, message: "no database scoped in config"
    repo = opts[:repo] || config.repo
    branch = config.branch
    graph_type = opts[:graph_type] || :instance
    "triples/#{org}/#{db}/#{repo}/branch/#{branch}/#{graph_type}"
  end

  defp build_commit_info(opts) do
    author = opts[:author]
    message = opts[:message]

    if author || message do
      %{"author" => author || "", "message" => message || ""}
    end
  end

  @doc """
  Retrieves the contents of the specified graph as turtle-encoded triples.

  ## Options

  - `:graph_type` — `:instance` (default) or `:schema`.
  - `:organization` — overrides `config.organization`.
  - `:repo` — overrides `config.repo`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: "@prefix : <http://example.org/> .")} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> {:ok, turtle} = TerminusDB.Triples.get(config)
      iex> turtle
      "@prefix : <http://example.org/> ."

  """
  @spec get(Config.t(), [triples_opt()]) :: {:ok, String.t()} | {:error, Error.t()}
  def get(config, opts \\ []) do
    path = triples_path(config, opts)
    Client.request(config, :get, path, area: :triples)
  end

  @doc """
  Retrieves triples, or raises.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: "<http://example.org/foo> <http://example.org/bar> <http://example.org/baz> .")} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> TerminusDB.Triples.get!(config)
      "<http://example.org/foo> <http://example.org/bar> <http://example.org/baz> ."

  """
  @spec get!(Config.t(), [triples_opt()]) :: String.t()
  def get!(config, opts \\ []) do
    case get(config, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Replaces the entire graph with the given turtle triples.

  ## Options

  - `:author` — commit author.
  - `:message` — commit message.
  - `:graph_type` — `:instance` (default) or `:schema`.
  - `:organization` — overrides `config.organization`.
  - `:repo` — overrides `config.repo`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> {:ok, resp} = TerminusDB.Triples.update(config, "@prefix : <http://example.org/> .", author: "admin", message: "replace")
      iex> resp["api:status"]
      "api:success"

  """
  @spec update(Config.t(), String.t(), [triples_opt()]) ::
          {:ok, map()} | {:error, Error.t()}
  def update(config, content, opts \\ []) do
    path = triples_path(config, opts)

    body =
      Params.maybe_put(%{"turtle" => content}, "commit_info", build_commit_info(opts))

    Client.request(config, :post, path, json: body, area: :triples)
  end

  @doc """
  Replaces graph with turtle triples, or raises.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> TerminusDB.Triples.update!(config, "@prefix : <http://example.org/> .")
      %{"api:status" => "api:success"}

  """
  @spec update!(Config.t(), String.t(), [triples_opt()]) :: map()
  def update!(config, content, opts \\ []) do
    case update(config, content, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end

  @doc """
  Inserts turtle triples into the graph (additive).

  ## Options

  Same as `update/3`.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> {:ok, resp} = TerminusDB.Triples.insert(config, ":foo :bar :baz .", author: "admin", message: "add")
      iex> resp["api:status"]
      "api:success"

  """
  @spec insert(Config.t(), String.t(), [triples_opt()]) ::
          {:ok, map()} | {:error, Error.t()}
  def insert(config, content, opts \\ []) do
    path = triples_path(config, opts)

    body =
      Params.maybe_put(%{"turtle" => content}, "commit_info", build_commit_info(opts))

    Client.request(config, :put, path, json: body, area: :triples)
  end

  @doc """
  Inserts turtle triples, or raises.

  ## Examples

      iex> config = TerminusDB.Config.new(
      ...>   endpoint: "http://localhost:6363",
      ...>   adapter: fn req -> {req, Req.Response.new(status: 200, body: %{"api:status" => "api:success"})} end
      ...> ) |> TerminusDB.Config.with_database("mydb")
      iex> TerminusDB.Triples.insert!(config, ":foo :bar :baz .")
      %{"api:status" => "api:success"}

  """
  @spec insert!(Config.t(), String.t(), [triples_opt()]) :: map()
  def insert!(config, content, opts \\ []) do
    case insert(config, content, opts) do
      {:ok, body} -> body
      {:error, error} -> raise error
    end
  end
end