Skip to main content

lib/mix/tasks/angelus.validate.horizons.ex

defmodule Mix.Tasks.Angelus.Validate.Horizons do
  @moduledoc "Validates or regenerates local JPL Horizons fixtures."

  use Mix.Task

  @fixture Path.join(["test", "fixtures", "horizons", "de442_positions.json"])

  @doc "Validates or regenerates local JPL Horizons fixtures according to CLI args."
  @impl true
  @spec run([String.t()]) :: :ok | no_return()
  def run(args) do
    case OptionParser.parse(args, strict: [check: :boolean, write: :boolean]) do
      {[check: true], [], []} ->
        check!()

      {[write: true], [], []} ->
        write!()

      {opts, [], []} when opts in [[], [check: false], [write: false]] ->
        help()

      {_opts, _rest, _invalid} ->
        Mix.raise("usage: mix angelus.validate.horizons --check | --write")
    end
  end

  defp check! do
    unless File.exists?(@fixture) do
      Mix.raise(
        "missing fixture #{@fixture}; run mix angelus.validate.horizons --write to generate it"
      )
    end

    with {:ok, body} <- File.read(@fixture),
         {:ok, fixture} <- Jason.decode(body),
         :ok <- validate_fixture(fixture) do
      Mix.shell().info("Horizons fixture is structurally valid: #{@fixture}")
    else
      {:error, reason} -> Mix.raise("invalid Horizons fixture: #{inspect(reason)}")
    end
  end

  @spec write!() :: no_return()
  defp write! do
    Mix.raise(
      "--write requires live JPL Horizons query support, which is not implemented in this first v0.1 scaffold"
    )
  end

  defp help do
    Mix.shell().info("Usage: mix angelus.validate.horizons --check | --write")
  end

  defp validate_fixture(%{"cases" => cases} = fixture) when is_list(cases) do
    required = [
      "source",
      "kernel",
      "lsk",
      "pck",
      "gm",
      "observer",
      "abcorr",
      "frame_base",
      "output"
    ]

    cond do
      Enum.any?(required, &(not Map.has_key?(fixture, &1))) -> {:error, :missing_fixture_metadata}
      Enum.empty?(cases) -> {:error, :empty_cases}
      true -> validate_cases(cases)
    end
  end

  defp validate_fixture(_fixture), do: {:error, :invalid_fixture_shape}

  defp validate_cases(cases) do
    required = [
      "datetime_utc",
      "body",
      "spice_target",
      "spice_id",
      "target_kind",
      "longitude",
      "latitude",
      "distance_au"
    ]

    case Enum.find(cases, fn case_ -> Enum.any?(required, &(not Map.has_key?(case_, &1))) end) do
      nil -> :ok
      _case -> {:error, :invalid_case_shape}
    end
  end

  @shortdoc "Validates JPL Horizons fixtures"
end