Skip to main content

lib/mix/tasks/rustq.templates.check.ex

defmodule Mix.Tasks.Rustq.Templates.Check do
  @moduledoc """
  Verifies that RustQ template fixtures still compile against the generated Rust support code.
  """

  use Mix.Task

  alias RustQ.Rust.AST.Builder, as: A

  @shortdoc "Checks RustQ's Rustler helper templates"

  @fixture Path.join(["fixtures", "rustler_template_check"])

  @impl Mix.Task
  def run(_args) do
    fixture = fixture_path()
    manifest = Path.join(fixture, "Cargo.toml")

    fixture
    |> Path.join("src/generated.rs")
    |> File.write!(generated_source())

    cargo(["fmt", "--manifest-path", manifest, "--", "--check"])
    cargo(["check", "--manifest-path", manifest])

    cargo([
      "clippy",
      "--manifest-path",
      manifest,
      "--",
      "-D",
      "warnings"
    ])
  end

  defp generated_source do
    RustQ.render!("__rq_items!();", "generated.rs",
      preamble:
        "// This file is generated by mix rustq.templates.check. Do not edit by hand.\n\n",
      splice: [items: generated_items()]
    )
  end

  defp generated_items do
    List.flatten([
      RustQ.Rustler.resource(:Document, fields: [nodes: {:vec, :Node}]),
      RustQ.Rustler.resource_type(:Document),
      RustQ.Rustler.resource_decoder(:Document),
      RustQ.Rustler.term_helpers(type_key: "atoms::r#type()"),
      RustQ.Rustler.atom_decoder(:decode_node_kind,
        returns: :NodeKind,
        cases: [text: "NodeKind::Text", space: "NodeKind::Space"]
      ),
      RustQ.Rustler.atom_dispatch(:dispatch_node,
        args: [kind: :Atom],
        on: "kind",
        cases: [text: "decode_text()", space: "decode_space()"]
      ),
      RustQ.Rustler.opts_helpers(),
      RustQ.Rustler.term_decoder(:ProgramInput,
        fields: [body: [type: {:vec, "Term<'a>"}, key: "atoms::body()", required: true]]
      ),
      RustQ.Rustler.term_decoder(:IfStatementInput,
        fields: [
          test: [type: "Term<'a>", key: "atoms::test()", required: true],
          consequent: [type: "Term<'a>", key: "atoms::consequent()", required: true],
          alternate: [type: {:option, "Term<'a>"}, key: "atoms::alternate()"]
        ]
      ),
      RustQ.Rustler.nif_struct(:ExText, "Folio.Content.Text",
        attrs: ["allow(dead_code)"],
        fields: [text: :String, size: {:option, :String}]
      ),
      RustQ.Rustler.nif_struct(:ExSpace, "Folio.Content.Space",
        attrs: ["allow(dead_code)"],
        fields: []
      ),
      RustQ.Rustler.tagged_enum(:ExContent,
        attrs: [A.allow_attr(:dead_code)],
        tag: A.call(:atom_struct),
        variants: [
          Text: [type: :ExText, module: "Elixir.Folio.Content.Text"],
          Space: [type: :ExSpace, module: "Elixir.Folio.Content.Space"]
        ]
      ),
      RustQ.Rustler.cached_atoms([:ok, {:node_changes, "nodeChanges"}]),
      RustQ.Rustler.term_builders(),
      RustQ.Rustler.nif_term_builders()
    ])
  end

  defp fixture_path do
    :rustq
    |> :code.priv_dir()
    |> List.to_string()
    |> Path.join(@fixture)
  end

  defp cargo(args) do
    {_, status} = System.cmd("cargo", args, into: IO.stream(), stderr_to_stdout: true)

    if status != 0 do
      Mix.raise("cargo #{Enum.join(args, " ")} failed")
    end
  end
end