defmodule Confispex.Type.CSV do
@moduledoc """
A CSV type.
Casts a CSV string to a list with values which are cast according to `:of` option.
### Options
* `:of` - `Confispex.Type.String` is used by default. Can be used any other type according to
`t:Confispex.Type.type_reference/0`
## Examples
iex> Confispex.Type.cast("John,user1@example.com", Confispex.Type.CSV)
{:ok, ["John", "user1@example.com"]}
iex> Confispex.Type.cast("John,user1@example.com", {Confispex.Type.CSV, of: Confispex.Type.Email})
{:error,
{"John,user1@example.com", {Confispex.Type.CSV, [of: Confispex.Type.Email]},
[
nested: [
{"John", Confispex.Type.Email,
[parsing: ["expected a string in format ", {:highlight, "username@host"}]]}
]
]}}
iex> Confispex.Type.cast(~s|John,"user1@example.com|, Confispex.Type.CSV)
{:error,
{~s|John,"user1@example.com|, Confispex.Type.CSV,
[parsing: ~s|expected escape character " but reached the end of file|]}}
"""
@behaviour Confispex.Type
@impl true
def cast(value, opts) when is_binary(value) do
with {:ok, line} <- parse_csv(value) do
of = Keyword.get(opts, :of, Confispex.Type.String)
results =
Enum.map(line, fn value ->
Confispex.Type.cast(value, of)
end)
case Enum.filter(results, &match?({:error, _}, &1)) do
[] -> {:ok, Enum.map(results, &elem(&1, 1))}
results -> {:error, nested: Enum.map(results, &elem(&1, 1))}
end
end
end
defp parse_csv(value) do
case NimbleCSV.RFC4180.parse_string(value, skip_headers: false) do
[] -> {:ok, []}
[line] -> {:ok, line}
_ -> {:error, validation: "expected a CSV with only 1 line"}
end
rescue
e in NimbleCSV.ParseError ->
{:error, parsing: Exception.message(e)}
end
end