Nfse.Schema.Runtime (nfse v0.1.0)

Copy Markdown View Source

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
ErroQuando
:unknown_fieldchave, elemento ou atributo que o tipo não declara
:requiredcampo obrigatório ausente
:typevalor de outro tipo, caractere que o XML proíbe, ou texto solto entre elementos
:length, :min_length, :max_lengthcomprimento fora da faceta, em caracteres Unicode
:enumerationvalor fora da lista do tipo
:patternalgum pattern do tipo não casa
:integer_digits, :fraction_digitsdecimal com dígitos inteiros ou casas a mais
:choicemais de um ramo de um choice, ou nenhum quando algum é obrigatório
:occurrencesmais 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.

Summary

Functions

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

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

Converte attrs no struct de module e valida o resultado.

Valida um struct de module sem convertê-lo.

Types

attribute()

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

Functions

decode(module, element)

@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(module, struct, tag, extra_attributes)

@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(module, attrs)

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

Converte attrs no struct de module e valida o resultado.

validate(module, value)

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

Valida um struct de module sem convertê-lo.