# `Nfse.Schema.Runtime`
[🔗](https://github.com/ideia4it/nfse/blob/v0.1.0/lib/nfse/schema/runtime.ex#L1)

Uso interno da biblioteca: não faz parte da API pública.

Conversão, validação, escrita canônica e leitura de qualquer tipo complexo do schema.

Cada módulo gerado a partir do XSD guarda só o struct e o descritor. Este módulo lê o descritor e
faz o resto, com as mesmas regras para todos os tipos.

## Contrato do módulo de schema

  * `__schema__(:version)`: a versão do leiaute, como `"1.01"`
  * `__schema__(:fields)`: os elementos, como `Nfse.Schema.Field`, na ordem do XSD
  * `__schema__(:attributes)`: os atributos, também como `Nfse.Schema.Field`
  * `__schema__(:choices)`: `%{index: índice, min: 0 | 1}` de cada `choice`, com o `minOccurs`
    dele
  * `__schema__(:simple_types)`: o módulo de tipos simples

O módulo de tipos simples responde `facets(nome)`, com as facetas já achatadas (as da base
primeiro, depois as do próprio tipo), e `kind(nome)`: `:string`, `:date`, `:datetime` ou
`{:decimal, dígitos_inteiros, casas}`.

## Conversão

`new/2` aceita mapa, lista de palavras-chave ou o próprio struct, também nos campos aninhados. As
chaves são os átomos dos campos. Uma chave que o tipo não declara, inclusive em forma de string, é
`:unknown_field`, e então a validação nem roda. Inteiro vira string num campo de texto, sem
completar com zeros. Decimal passa a ter exatamente as casas do tipo (`49.9` vira `49.90`), sem
arredondar. Lista vazia num campo repetido vale como campo ausente.

## Validação

A ordem é obrigatoriedade, tipo, facetas, `choice` e ocorrências. Os erros voltam juntos em
`details`, cada um com o caminho do campo, como `toma.emails[1]` (o índice começa em zero), ou
`""` quando o erro é do próprio documento.

  * `:string` aceita só string UTF-8 válida com os caracteres que o XML 1.0 permite: tab, LF, CR e
    de U+0020 em diante, menos os substitutos e U+FFFE e U+FFFF. Isso vale também onde o XSD não
    tem `pattern`, e também na leitura. Espaço nunca é aparado, porque cada `whiteSpace` do
    pacote oficial é `preserve`, e string vazia é um valor, não ausência
  * um `choice` com dois ramos preenchidos é sempre erro. Sem ramo preenchido, só é erro quando o
    `choice` tem `min: 1` e cada ramo tem `min: 1`, porque um ramo opcional pode ser escolhido vazio
  * `:date` aceita só `Date`, e `:datetime` só `DateTime`
  * decimal aceita só `Decimal`; casas a mais são `:fraction_digits` e dígitos inteiros a mais são
    `:integer_digits`
  * as facetas valem sobre a forma escrita do valor, e cada `pattern` da lista precisa casar

| Erro | Quando |
|---|---|
| `:unknown_field` | chave, elemento ou atributo que o tipo não declara |
| `:required` | campo obrigatório ausente |
| `:type` | valor de outro tipo, caractere que o XML proíbe, ou texto solto entre elementos |
| `:length`, `:min_length`, `:max_length` | comprimento fora da faceta, em caracteres Unicode |
| `:enumeration` | valor fora da lista do tipo |
| `:pattern` | algum `pattern` do tipo não casa |
| `:integer_digits`, `:fraction_digits` | decimal com dígitos inteiros ou casas a mais |
| `:choice` | mais de um ramo de um `choice`, ou nenhum quando algum é obrigatório |
| `:occurrences` | mais repetições do que o `maxOccurs` permite |

## Escrita

`encode/4` escreve na forma canônica (C14N): os atributos extras, que são as declarações de
namespace, saem primeiro, na ordem dada, e depois os atributos do struct, em ordem de nome. Não há
espaço entre tags, elemento vazio sai `<x></x>` e nunca `<x/>`, campo ausente é omitido e o
escaping é o de `Nfse.Schema.Xml`. Data e hora saem com o offset do próprio `DateTime`
(`2026-11-03T10:00:00-03:00`), sem frações de segundo e nunca com `Z`. Decimal sai com exatamente
as casas do tipo. A assinatura (`:signature`) sai como veio.

`encode/4` espera um struct já validado e levanta `ArgumentError` diante de um valor que só daria
para escrever arredondando ou truncando.

## Leitura

`decode/2` lê a árvore do `Saxy.SimpleForm`. As facetas são conferidas no texto antes de qualquer
conversão, o que mantém um decimal com expoente longe do `Decimal`. Data e hora voltam em UTC.
Elemento ou atributo que o tipo não declara é `:unknown_field`, e `xmlns` e `xmlns:*` são
ignorados. Espaço entre elementos (XML indentado) é ignorado, e texto de verdade nesse lugar é
`:type`. A assinatura volta como o XML do elemento, escrito pelo `Saxy.encode!/1`. Depois da
leitura, o documento passa pela mesma validação de `new/2`.

## Onde o runtime diverge do XSD

  * O padrão do `TSSerieDPS` é `^0{0,4}\d{1,5}$`. No padrão XSD, `^` e `$` são caracteres
    literais, e o `xmllint` recusa a série `"1"` e aceita `"^1$"`. A intenção evidente é ancorar,
    e é o que se espera do validador da Sefin, ainda a confirmar na produção restrita. Por isso o
    runtime tira um `^` inicial e um `$` final antes de compilar o padrão.
  * Cada padrão vira uma regex `\A(?:...)\z` em Unicode, em que `[!-ÿ]` é a faixa de U+0021 a
    U+00FF, e o ponto não casa CR nem LF, como no XSD.
  * Os decimais não passam por `Decimal.round/2`, que segue a precisão do contexto do processo de
    quem chama. As contas são feitas direto sobre o coeficiente e o expoente.

# `attribute`

```elixir
@type attribute() :: {String.t(), String.t()}
```

# `decode`

```elixir
@spec decode(module(), Saxy.SimpleForm.t()) ::
  {:ok, struct()} | {:error, Nfse.Error.t()}
```

Lê o elemento como um struct de `module` e valida o resultado.

# `encode`

```elixir
@spec encode(module(), struct(), String.t(), [attribute()]) :: iodata()
```

Escreve o struct como o elemento `tag`, na forma canônica, com `extra_attributes` antes dos
atributos do struct.

# `new`

```elixir
@spec new(module(), map() | keyword() | struct()) ::
  {:ok, struct()} | {:error, Nfse.Error.t()}
```

Converte `attrs` no struct de `module` e valida o resultado.

# `validate`

```elixir
@spec validate(module(), struct()) :: :ok | {:error, Nfse.Error.t()}
```

Valida um struct de `module` sem convertê-lo.

---

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