# `AnchoFijo.Diagnostico`
[🔗](https://github.com/galenzo17/ancho_fijo/blob/v0.1.0/lib/ancho_fijo/diagnostico.ex#L1)

Un error que se puede leer, pegar en un correo y mandarle al banco.

Esta librería no devuelve `{:error, :invalid}`. Cada falla es un
`%AnchoFijo.Diagnostico{}` que responde cuatro preguntas: **dónde** pasó
(línea, campo, posiciones), **qué se esperaba**, **qué llegó** y **cuál es la
causa probable**.

    iex> alias AnchoFijo.Diagnostico
    iex> d = Diagnostico.nuevo(
    ...>   tipo: :largo_de_linea,
    ...>   linea: 42,
    ...>   esperado: "120 caracteres",
    ...>   recibido: "118",
    ...>   causa_probable: "posible campo faltante o archivo delimitado"
    ...> )
    iex> Diagnostico.mensaje(d)
    "línea 42: se esperaban 120 caracteres, llegaron 118; posible campo faltante o archivo delimitado"

La causa probable es una hipótesis, no un veredicto. Existe porque en una
integración bancaria el ciclo de feedback es de días: quien lee el error a las
3 AM necesita una pista de dónde mirar, no solo la constatación de que algo
falló.

# `t`

```elixir
@type t() :: %AnchoFijo.Diagnostico{
  campo: atom() | nil,
  causa_probable: String.t() | nil,
  contenido: String.t() | nil,
  esperado: String.t() | nil,
  linea: pos_integer() | nil,
  posicion: {pos_integer(), pos_integer()} | nil,
  recibido: String.t() | nil,
  tipo: tipo()
}
```

# `tipo`

```elixir
@type tipo() :: :layout | :entrada | :largo_de_linea | :encoding | :campo_invalido
```

Familia del problema, para poder filtrar o agrupar sin parsear el mensaje.

* `:layout` — la definición del formato es inconsistente consigo misma.
* `:entrada` — no se pudo leer el archivo o el argumento no es un binario.
* `:largo_de_linea` — la línea no mide lo que el layout declara.
* `:encoding` — hay bytes que no corresponden al encoding declarado.
* `:campo_invalido` — el contenido del campo no calza con su tipo.

# `mensaje`

```elixir
@spec mensaje(t()) :: String.t()
```

Redacta el diagnóstico como una sola línea de texto en español.

    iex> alias AnchoFijo.Diagnostico
    iex> d = Diagnostico.nuevo(
    ...>   tipo: :campo_invalido,
    ...>   linea: 7,
    ...>   campo: :monto,
    ...>   posicion: {21, 32},
    ...>   esperado: "12 dígitos",
    ...>   recibido: "\"0001234A567\"",
    ...>   causa_probable: "hay un carácter no numérico en el monto"
    ...> )
    iex> Diagnostico.mensaje(d)
    "línea 7, campo :monto (posiciones 21-32): se esperaban 12 dígitos, llegaron \"0001234A567\"; hay un carácter no numérico en el monto"

Sin causa probable el mensaje simplemente la omite:

    iex> AnchoFijo.Diagnostico.nuevo(tipo: :entrada, esperado: "un archivo legible", recibido: ":enoent")
    ...> |> AnchoFijo.Diagnostico.mensaje()
    "entrada: se esperaba un archivo legible, llegó :enoent"

# `nuevo`

```elixir
@spec nuevo(Enumerable.t()) :: t()
```

Construye un diagnóstico desde una keyword list o mapa de atributos.

Los valores de `:esperado` y `:recibido` se normalizan a texto: lo que no sea
binario pasa por `inspect/1`, para que un diagnóstico nunca falle al armarse.

    iex> AnchoFijo.Diagnostico.nuevo(tipo: :campo_invalido, campo: :monto, esperado: 12, recibido: nil)
    %AnchoFijo.Diagnostico{tipo: :campo_invalido, campo: :monto, esperado: "12", recibido: nil}

# `reporte`

```elixir
@spec reporte([t()]) :: String.t()
```

Redacta una lista de diagnósticos como texto multilínea, uno por renglón.

    iex> alias AnchoFijo.Diagnostico
    iex> [
    ...>   Diagnostico.nuevo(tipo: :largo_de_linea, linea: 3, esperado: "40 bytes", recibido: "38"),
    ...>   Diagnostico.nuevo(tipo: :largo_de_linea, linea: 9, esperado: "40 bytes", recibido: "41")
    ...> ]
    ...> |> Diagnostico.reporte()
    ...> |> String.split("\n")
    ["línea 3: se esperaban 40 bytes, llegaron 38", "línea 9: se esperaban 40 bytes, llegaron 41"]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
