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

La definición del formato, como data.

Un layout es una lista de campos más tres decisiones globales: en qué encoding
viene el archivo, si las posiciones se cuentan en bytes o en caracteres, y qué
largo total se espera por línea. Cambiar de formato es cambiar este mapa, no
escribir código.

    iex> alias AnchoFijo.Layout
    iex> {:ok, layout} = Layout.nuevo(
    ...>   nombre: "nómina banco X",
    ...>   encoding: :latin1,
    ...>   campos: [
    ...>     [nombre: :rut, largo: 10],
    ...>     [nombre: :beneficiario, largo: 30],
    ...>     [nombre: :monto, largo: 12, tipo: :decimal, precision: 2]
    ...>   ]
    ...> )
    iex> Layout.largo(layout)
    52

Las posiciones son 1-based porque así vienen en todas las especificaciones
bancarias del mundo: cuando el anexo dice "posiciones 21 a 32", el layout dice
lo mismo. Si se omiten, cada campo se encadena al anterior.

## Validación de la definición misma

`nuevo/1` revisa el layout antes de ver un archivo. Distingue dos gravedades,
y la distinción es de dominio: un solapamiento es siempre un error de
transcripción del anexo, mientras que un hueco suele ser una zona reservada
legítima del formato. Los huecos quedan en `:advertencias` y no impiden
parsear.

    iex> alias AnchoFijo.Layout
    iex> {:error, [diagnostico]} = Layout.nuevo(campos: [
    ...>   [nombre: :rut, posicion: 1, largo: 10],
    ...>   [nombre: :nombre, posicion: 8, largo: 20]
    ...> ])
    iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
    "layout, campo :nombre (posiciones 8-27): se esperaba que empezara en la posición 11 o después, llegó 8; se solapa con el campo :rut (posiciones 1-10)"

    iex> alias AnchoFijo.Layout
    iex> {:ok, layout} = Layout.nuevo(campos: [
    ...>   [nombre: :rut, posicion: 1, largo: 10],
    ...>   [nombre: :nombre, posicion: 14, largo: 20]
    ...> ])
    iex> AnchoFijo.Diagnostico.mensaje(hd(layout.advertencias))
    "layout, campo :nombre (posiciones 14-33): se esperaba que empezara en la posición 11, llegó 14; quedan 3 posiciones sin declarar entre :rut y :nombre; si el formato tiene relleno ahí, ignore esta advertencia"

## Opciones

  * `:campos` — obligatorio. Lista de definiciones de `AnchoFijo.Campo`
    (keyword lists, mapas o structs ya construidos).
  * `:encoding` — `:utf8` (default) o `:latin1`.
  * `:unidad` — `:bytes` (default) o `:caracteres`. Ver más abajo.
  * `:largo` — largo total esperado por línea. Si se omite, se infiere del
    último campo.
  * `:nombre` — etiqueta libre para identificar el layout en logs.

## Bytes o caracteres

El default es `:bytes` porque un formato de ancho fijo se define sobre el
archivo físico: cuando el banco dice "120 posiciones", cuenta bytes. Con
latin-1 da lo mismo, un byte es un carácter. Con UTF-8 no: si el archivo trae
acentos y el emisor contó caracteres, hay que declarar `unidad: :caracteres` o
cada línea con una "ñ" se corre un byte.

# `t`

```elixir
@type t() :: %AnchoFijo.Layout{
  advertencias: [AnchoFijo.Diagnostico.t()],
  campos: [AnchoFijo.Campo.t()],
  encoding: :utf8 | :latin1,
  largo: pos_integer(),
  nombre: String.t() | nil,
  unidad: :bytes | :caracteres
}
```

# `campo`

```elixir
@spec campo(t(), atom()) :: AnchoFijo.Campo.t() | nil
```

Busca un campo por nombre.

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2], [nombre: :b, largo: 3]])
    iex> AnchoFijo.Layout.campo(layout, :b) |> AnchoFijo.Campo.rango()
    {3, 5}
    iex> AnchoFijo.Layout.campo(layout, :inexistente)
    nil

# `coercer`

```elixir
@spec coercer(t() | Enumerable.t()) ::
  {:ok, t()} | {:error, [AnchoFijo.Diagnostico.t()]}
```

Acepta un layout ya construido o una definición y devuelve siempre `{:ok, layout}`.

Es lo que usan `AnchoFijo.parsear/3` y `AnchoFijo.stream/3` para que el
llamador pueda pasar la definición en línea sin ceremonia.

    iex> {:ok, layout} = AnchoFijo.Layout.coercer(campos: [[nombre: :a, largo: 2]])
    iex> AnchoFijo.Layout.coercer(layout)
    {:ok, layout}

# `contexto`

```elixir
@spec contexto(t(), pos_integer() | nil) :: AnchoFijo.Campo.contexto()
```

Contexto de lectura que el layout entrega a cada campo.

    iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2]], encoding: :latin1)
    ...> |> AnchoFijo.Layout.contexto(9)
    %{encoding: :latin1, unidad: :bytes, linea: 9}

# `largo`

```elixir
@spec largo(t()) :: pos_integer()
```

Largo total de línea que el layout espera.

    iex> AnchoFijo.Layout.largo(AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 7]]))
    7

# `nombres`

```elixir
@spec nombres(t()) :: [atom()]
```

Nombres de los campos, en orden de posición.

    iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2], [nombre: :b, largo: 3]])
    ...> |> AnchoFijo.Layout.nombres()
    [:a, :b]

# `nuevo`

```elixir
@spec nuevo(Enumerable.t()) :: {:ok, t()} | {:error, [AnchoFijo.Diagnostico.t()]}
```

Valida y construye un layout.

Devuelve `{:ok, layout}` —posiblemente con `:advertencias`— o
`{:error, diagnosticos}` con todos los problemas encontrados, no solo el
primero: corregir una definición de 40 campos de a un error por compilación es
una forma lenta de perder el día.

    iex> {:ok, layout} = AnchoFijo.Layout.nuevo(campos: [[nombre: :codigo, largo: 4, tipo: :entero]])
    iex> layout.largo
    4

    iex> {:error, [diagnostico]} = AnchoFijo.Layout.nuevo(campos: [])
    iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
    "layout: se esperaba al menos un campo en :campos, llegó una lista vacía; un layout sin campos no puede leer nada"

# `nuevo!`

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

Igual que `nuevo/1` pero levanta `AnchoFijo.Error` con el reporte completo.

    iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2]]).unidad
    :bytes

    iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 0]])
    ** (AnchoFijo.Error) layout, campo :a: se esperaba un :largo entero mayor que cero, llegó 0; revise la especificación del formato

---

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