lib/csv/exceptions.ex

defmodule CSV.RowLengthError do
  @moduledoc """
  Raised at runtime when the CSV has rows of variable length
  and `validate_row_length` is set to true.
  """

  defexception [:row, :message]

  def exception(options) do
    row = options |> Keyword.fetch!(:row)
    actual_length = options |> Keyword.fetch!(:actual_length)
    expected_length = options |> Keyword.fetch!(:expected_length)

    %__MODULE__{
      row: row,
      message:
        "Row #{row} has length #{actual_length} instead of expected length #{expected_length}\n\n" <>
          "You are seeing this error because :validate_row_length has been set to true\n"
    }
  end
end

defmodule CSV.StrayEscapeCharacterError do
  @moduledoc """
  Raised at runtime when the CSV row has stray quotes.
  """

  defexception [:line, :sequence_position, :message]

  def exception(options) do
    line = options |> Keyword.fetch!(:line)
    unredact = options |> Keyword.get(:unredact, false)
    sequence = options |> Keyword.fetch!(:sequence) |> get_sequence(unredact)

    message =
      "Stray escape character on line #{line}:" <>
        "\n\n#{sequence}" <>
        "\n\nThis error often happens when the wrong separator or escape character has been applied.\n"

    %__MODULE__{
      line: line,
      message: message
    }
  end

  defp get_sequence(_, false), do: "**redacted**"
  defp get_sequence(sequence, true), do: sequence
end

defmodule CSV.EscapeSequenceError do
  @moduledoc """
  Raised at runtime when the CSV stream either ends with unfinished escape sequences or
  escape sequences span more lines than specified by escape_max_lines (default 1000).
  """

  defexception [:line, :escape_sequence_start_line, :message]

  def exception(options) do
    line = options |> Keyword.fetch!(:line)
    stream_halted = options |> Keyword.get(:stream_halted, false)
    unredact = options |> Keyword.get(:unredact, false)

    escape_sequence_start =
      options |> Keyword.fetch!(:escape_sequence_start) |> get_sequence(unredact)

    mode = options |> Keyword.fetch!(:mode)

    continues_parsing =
      if mode == :normal do
        " Parsing will continue on line #{line + 1}."
      else
        " You can use normal mode to continue parsing rows even if single rows have errors."
      end

    message =
      if stream_halted do
        "Escape sequence started on line #{line}:" <>
          "\n\n#{escape_sequence_start}\n\ndid not terminate before the stream halted." <>
          continues_parsing <> "\n"
      else
        escape_max_lines = options |> Keyword.fetch!(:escape_max_lines)

        "Escape sequence started on line #{line}:" <>
          "\n\n#{escape_sequence_start}\n\ndid not terminate." <>
          continues_parsing <>
          "\n\n" <>
          "Escape sequences are allowed to span up to #{escape_max_lines} lines. " <>
          "This threshold avoids collecting the whole file into memory " <>
          "when an escape sequence does not terminate.\nYou can change " <>
          "it using the escape_max_lines option: https://hexdocs.pm/csv/CSV.html#decode/2\n"
      end

    %__MODULE__{
      message: message
    }
  end

  defp get_sequence(_, false), do: "**redacted**"
  defp get_sequence(sequence, true), do: sequence
end