lib/relyra/conformance_fixtures.ex

defmodule Relyra.ConformanceFixtures do
  @moduledoc false

  @required_keys ~w(id requirement_ids provenance)
  @executed_statuses MapSet.new(["pass", "reject"])

  @spec load_manifest!(Path.t()) :: [map()]
  def load_manifest!(manifest_path) when is_binary(manifest_path) do
    manifest_path = Path.expand(manifest_path)
    manifest_dir = Path.dirname(manifest_path)

    manifest_path
    |> File.read!()
    |> Jason.decode!()
    |> require_list!(manifest_path)
    |> require_keys!(@required_keys)
    |> Enum.map(fn row ->
      row
      |> Map.put(:manifest_path, manifest_path)
      |> Map.put(:manifest_dir, manifest_dir)
    end)
  end

  @spec require_keys!([map()], [String.t()]) :: [map()]
  def require_keys!(rows, required_keys) when is_list(rows) and is_list(required_keys) do
    Enum.map(rows, fn row ->
      require_row_keys!(row, required_keys)
    end)
  end

  @spec executed_rows([map()]) :: [map()]
  def executed_rows(rows) when is_list(rows) do
    Enum.filter(rows, fn row ->
      MapSet.member?(@executed_statuses, Map.get(row, "status"))
    end)
  end

  @spec coverage_rows([map()]) :: [map()]
  def coverage_rows(rows) when is_list(rows), do: rows

  @spec fixture_xml(map()) :: binary()
  def fixture_xml(row) when is_map(row) do
    cond do
      is_binary(Map.get(row, "xml")) ->
        Map.fetch!(row, "xml")

      is_binary(Map.get(row, "xml_path")) ->
        row
        |> Map.fetch!(:manifest_dir)
        |> Path.join(Map.fetch!(row, "xml_path"))
        |> File.read!()

      true ->
        raise ArgumentError,
              "fixture row #{inspect(Map.get(row, "id"))} is missing xml or xml_path content"
    end
  end

  defp require_list!(rows, _manifest_path) when is_list(rows), do: rows

  defp require_list!(other, manifest_path) do
    raise ArgumentError,
          "manifest #{manifest_path} must decode to a list of rows, got: #{inspect(other)}"
  end

  defp require_row_keys!(row, required_keys) when is_map(row) do
    missing_keys =
      required_keys
      |> Enum.reject(&present?(Map.get(row, &1)))
      |> maybe_add_fixture_content_key(row)

    if missing_keys == [] do
      row
    else
      raise ArgumentError,
            "manifest row #{inspect(Map.get(row, "id"))} missing keys: #{Enum.join(missing_keys, ", ")}"
    end
  end

  defp require_row_keys!(row, _required_keys) do
    raise ArgumentError, "manifest row must be a map, got: #{inspect(row)}"
  end

  defp maybe_add_fixture_content_key(missing_keys, row) do
    if present?(Map.get(row, "xml")) or present?(Map.get(row, "status")) do
      missing_keys
    else
      missing_keys ++ ["xml|status"]
    end
  end

  defp present?(value) when is_binary(value), do: String.trim(value) != ""
  defp present?(value) when is_list(value), do: value != []
  defp present?(value) when is_map(value), do: map_size(value) > 0
  defp present?(value), do: not is_nil(value)
end