Skip to main content

lib/rustq.ex

defmodule RustQ do
  @moduledoc """
  Rust template quasiquoting and code generation.

  RustQ renders real Rust templates from Elixir. Parse a template, bind
  placeholder identifiers/expressions, splice Rust fragments, then generate
  formatted Rust source.

  The most common entry points are:

    * `render!/3` for one-shot rendering from a string template.
    * `render_file!/2` for `.rs` template files.
    * `parse!/2`, `bind/2`, `splice/3`, and `codegen!/2` for pipeline-style codegen.
    * `parse_fragment!/2` and `valid_fragment?/2` for validating generated snippets.

  Use `RustQ.Rust` for Rust fragment builders, `RustQ.Rustler` for Rustler code
  generators, and `RustQ.Config` plus `mix rustq.gen` for project-level generated
  files.
  """

  alias RustQ.Error
  alias RustQ.Template

  @type source :: iodata()

  @doc """
  Parses and validates a Rust template.

  `filename` is used only in error messages; it does not need to exist on disk.
  """
  @spec parse(source(), String.t()) :: {:ok, Template.t()} | {:error, [map()]}
  def parse(source, filename) when is_binary(filename) do
    source = IO.iodata_to_binary(source)

    case RustQ.Native.parse(source) do
      :ok -> {:ok, %Template{source: source, filename: filename}}
      {:error, errors} -> {:error, Template.normalize_errors(errors, filename)}
    end
  end

  @doc """
  Like `parse/2`, but returns the template directly or raises on errors.
  """
  @spec parse!(source(), String.t()) :: Template.t()
  def parse!(source, filename) when is_binary(filename) do
    case parse(source, filename) do
      {:ok, template} ->
        template

      {:error, errors} ->
        raise Error, message: "RustQ parse error: #{inspect(errors)}", errors: errors
    end
  end

  @doc """
  Returns true when source is a valid Rust template.
  """
  @spec valid?(source(), String.t()) :: boolean()
  def valid?(source, filename) when is_binary(filename) do
    match?({:ok, _template}, parse(source, filename))
  end

  @doc """
  Reads and parses a Rust template file.

  Template files may include other Rust template files with
  `__rq_include!("relative/path.rs");`. Includes are expanded before Rust
  parsing and are resolved relative to the including file by default. Pass
  `:include_dir` to override the root directory for the initial file.
  """
  @spec from_file(Path.t(), keyword()) :: {:ok, Template.t()} | {:error, [map()] | File.posix()}
  def from_file(path, opts \\ []) do
    with {:ok, source} <- File.read(path),
         {:ok, source} <- Template.expand_includes(source, path, opts) do
      parse(source, path)
    end
  end

  @doc """
  Like `from_file/1`, but returns the template directly or raises on errors.
  """
  @spec from_file!(Path.t(), keyword()) :: Template.t()
  def from_file!(path, opts \\ []) do
    case from_file(path, opts) do
      {:ok, template} ->
        template

      {:error, errors} when is_list(errors) ->
        raise Error, message: "RustQ file parse error: #{inspect(errors)}", errors: errors

      {:error, reason} ->
        raise File.Error, reason: reason, action: "read file", path: path
    end
  end

  @doc """
  Renders a Rust template file.

  Accepts the same options as `render/3`.
  """
  @spec render_file(Path.t(), keyword()) :: {:ok, String.t()} | {:error, [map()] | File.posix()}
  def render_file(path, opts \\ []) do
    with {:ok, source} <- File.read(path),
         {:ok, source} <- Template.expand_includes(source, path, opts) do
      render(source, path, opts)
    end
  end

  @doc """
  Like `render_file/2`, but raises on errors.
  """
  @spec render_file!(Path.t(), keyword()) :: String.t()
  def render_file!(path, opts \\ []) do
    case render_file(path, opts) do
      {:ok, code} ->
        code

      {:error, errors} when is_list(errors) ->
        raise Error, message: "RustQ render file error: #{inspect(errors)}", errors: errors

      {:error, reason} ->
        raise File.Error, reason: reason, action: "read file", path: path
    end
  end

  @doc """
  Parses and validates a Rust fragment for a specific context.

  Supported contexts are `:item`, `:impl_item`, `:field`, `:stmt`, `:arg`,
  `:arm`, `:expr`, and `:type`.
  """
  @spec parse_fragment(atom(), term()) :: {:ok, RustQ.Rust.Fragment.t()} | {:error, [map()]}
  def parse_fragment(kind, fragment) when is_atom(kind) do
    code = RustQ.Rust.to_fragment(fragment)

    case validate_fragment(kind, code) do
      :ok -> {:ok, %RustQ.Rust.Fragment{kind: kind, code: code}}
      {:error, errors} -> {:error, errors}
    end
  end

  @doc """
  Like `parse_fragment/2`, but returns the fragment directly or raises on errors.
  """
  @spec parse_fragment!(atom(), term()) :: RustQ.Rust.Fragment.t()
  def parse_fragment!(kind, fragment) do
    case parse_fragment(kind, fragment) do
      {:ok, fragment} ->
        fragment

      {:error, errors} ->
        raise Error, message: "RustQ fragment parse error: #{inspect(errors)}", errors: errors
    end
  end

  @doc """
  Returns true when a Rust fragment is valid for the given context.
  """
  @spec valid_fragment?(atom(), term()) :: boolean()
  def valid_fragment?(kind, fragment),
    do: match?({:ok, _fragment}, parse_fragment(kind, fragment))

  @doc """
  Binds Rust placeholders in a parsed template.

  RustQ placeholders use the `__rq_` prefix. Use `__rq_Name` where Rust expects
  an identifier/type/lifetime, and `__rq_name!()` where Rust expects an
  expression or type macro.

  Values may be strings, atoms, `{:literal, value}`, `{:expr, code}`, or
  `{:type, type}` where type uses `RustQ.Rust.type/1` syntax.
  """
  @spec bind(Template.t(), keyword()) :: Template.t()
  def bind(%Template{} = template, bindings) when is_list(bindings) do
    %{template | bindings: Keyword.merge(template.bindings, bindings)}
  end

  @doc """
  Splices fragments into a parsed template.

  The splice name matches placeholders such as `__rq_items!();`,
  `__rq_fields: (),`, or `__rq_arms => unreachable!(),`.
  """
  @spec splice(Template.t(), atom(), term() | [term()]) :: Template.t()
  def splice(%Template{} = template, name, replacement) when is_atom(name) do
    %{template | splices: Keyword.put(template.splices, name, List.wrap(replacement))}
  end

  @doc """
  Splices a keyword/map/nested list of splice replacements.

  Duplicate names are concatenated, which allows independent generators to
  contribute fragments to the same splice point.
  """
  @spec splice(Template.t(), RustQ.Splice.source()) :: Template.t()
  def splice(%Template{} = template, splices) do
    %{template | splices: RustQ.Splice.merge([template.splices, splices])}
  end

  @doc """
  Generates formatted Rust source from a parsed template.
  """
  @spec codegen(Template.t(), keyword()) :: {:ok, String.t()} | {:error, [map()]}
  def codegen(%Template{} = template, opts \\ []) do
    case RustQ.Native.render(
           template.source,
           native_bindings(template.bindings),
           native_splices(template.splices)
         ) do
      {:ok, code} ->
        code
        |> Template.with_preamble(opts)
        |> Template.maybe_rustfmt(opts, template.filename)

      {:error, errors} when is_list(errors) ->
        {:error, Template.normalize_errors(errors, template.filename)}

      {:error, message} ->
        {:error, [%{message: message, filename: template.filename}]}
    end
  end

  @doc """
  Like `codegen/2`, but raises on errors.
  """
  @spec codegen!(Template.t(), keyword()) :: String.t()
  def codegen!(%Template{} = template, opts \\ []) do
    case codegen(template, opts) do
      {:ok, code} ->
        code

      {:error, errors} ->
        raise Error, message: "RustQ codegen error: #{inspect(errors)}", errors: errors
    end
  end

  @doc """
  Convenience wrapper around `parse/2`, `bind/2`, `splice/3`, and `codegen/2`.

  Options:

    * `:bind` - bindings passed to `bind/2`.
    * `:splice` - splice replacements passed to `splice/3`.
    * `:preamble` - optional text prepended after formatting.
  """
  @spec render(source(), String.t(), keyword()) :: {:ok, String.t()} | {:error, [map()]}
  def render(source, filename, opts \\ []) do
    with {:ok, source} <- Template.maybe_expand_includes(source, filename, opts),
         {:ok, template} <- parse(source, filename) do
      template = bind(template, Keyword.get(opts, :bind, []))

      template
      |> splice(Keyword.get(opts, :splice, []))
      |> codegen(opts)
    end
  end

  @doc """
  Like `render/3`, but raises on errors.
  """
  @spec render!(source(), String.t(), keyword()) :: String.t()
  def render!(source, filename, opts \\ []) do
    case render(source, filename, opts) do
      {:ok, code} ->
        code

      {:error, errors} ->
        raise Error, message: "RustQ render error: #{inspect(errors)}", errors: errors
    end
  end

  defp validate_fragment(:item, code),
    do: validate_splice_fragment(:items, code, "__rq_items!();")

  defp validate_fragment(:impl_item, code) do
    validate_splice_fragment(:items, code, "impl Target { __rq_items!(); }")
  end

  defp validate_fragment(:field, code) do
    validate_splice_fragment(:fields, code, "struct Target { __rq_fields: (), }")
  end

  defp validate_fragment(:stmt, code),
    do: validate_splice_fragment(:body, code, "fn target() { __rq_body!(); }")

  defp validate_fragment(:arg, code),
    do: validate_splice_fragment(:args, code, "fn target(__rq_args: ()) {}")

  defp validate_fragment(:arm, code) do
    validate_splice_fragment(
      :arms,
      code,
      "fn target(value: Option<i32>) { match value { __rq_arms => unreachable!(), } }"
    )
  end

  defp validate_fragment(:expr, code),
    do: validate_binding_fragment(:value, RustQ.Rust.expr(code), "fn target() { __rq_value!(); }")

  defp validate_fragment(:type, code),
    do: validate_binding_fragment(:value, {:type, {:raw, code}}, "type Target = __rq_value!();")

  defp validate_fragment(kind, _code) do
    {:error,
     [
       %{
         type: :unknown_fragment,
         context: kind,
         message: "unknown Rust fragment context",
         filename: "<fragment>"
       }
     ]}
  end

  defp validate_splice_fragment(name, code, template) do
    case render(template, "<fragment>", splice: [{name, [code]}]) do
      {:ok, _code} -> :ok
      {:error, errors} -> {:error, errors}
    end
  end

  defp validate_binding_fragment(name, value, template) do
    case render(template, "<fragment>", bind: [{name, value}]) do
      {:ok, _code} -> :ok
      {:error, errors} -> {:error, errors}
    end
  end

  defp native_bindings(bindings) do
    Enum.map(bindings, fn {name, value} -> {Atom.to_string(name), binding_value(value)} end)
  end

  defp binding_value(%RustQ.Rust.Fragment{} = fragment), do: RustQ.Rust.to_fragment(fragment)
  defp binding_value({:literal, value}), do: RustQ.Rust.literal(value)
  defp binding_value({:expr, value}), do: IO.iodata_to_binary(value)
  defp binding_value({:type, value}), do: RustQ.Rust.type(value)
  defp binding_value(value) when is_atom(value), do: Atom.to_string(value)
  defp binding_value(value) when is_binary(value), do: value
  defp binding_value(value) when is_list(value), do: IO.iodata_to_binary(value)

  defp native_splices(splices) do
    Enum.map(splices, fn {name, items} ->
      {Atom.to_string(name), Enum.map(items, &RustQ.Rust.to_fragment/1)}
    end)
  end
end