9. Namespace data:: — dados externos

fix:: · Índice · Próximo: Biblioteca padrão →

8 funções para cruzar o conteúdo do PDF com listas e tabelas suas. Tudo local: nenhum dado sai da máquina.


9.1 Onde ficam os arquivos

Glossários e datasets aceitam caminho relativo ao diretório de execução:

data::load_glossary("termos/juridicos.txt")
data::load_dataset("dados/lotes.csv")

As bases de consulta (query_gtin, query_medicamento, query_postal_code) têm nome fixo e são procuradas nesta ordem:

  1. $PDFL_DATA_DIR (variável de ambiente)
  2. ./dados/
  3. ./
  4. Perfis instalados por pdfl add (pdfl_profiles/*/dados/)
  5. Ao lado do PDF analisado
# Apontando explicitamente para a pasta das bases
PDFL_DATA_DIR=/opt/bases pdfl run perfil.pdfl documento.pdf

Se a base não for encontrada, a mensagem de erro diz onde colocá-la.

Para distribuir bases junto com os perfis, use pdfl pack — veja o capítulo 11.


9.2 Glossários

Um glossário é um arquivo de texto com um termo por linha. Linhas vazias e começadas com # são ignoradas.

termos/obrigatorios.txt:

# Termos que toda apólice precisa conter
prazo de carência
cobertura contratada
condições gerais

data::load_glossary(arquivo)

Carrega o glossário como lista de termos.

check "Glossário carregado" {
  termos = data::load_glossary("termos/obrigatorios.txt")
  print("termos no glossário:", termos.length)
  require termos.contains("condições gerais")
}

data::validate_against_reference(arquivo)

O caminho mais direto: devolve a lista dos termos do glossário que não aparecem no documento. Lista vazia significa que está tudo lá.

check "Cláusulas obrigatórias" {
  faltando = data::validate_against_reference("termos/obrigatorios.txt")
  assert faltando.length == 0,
    "cláusulas ausentes na apólice: #{faltando.join("; ")}"
}

A comparação ignora maiúsculas e espaçamento — "CONDIÇÕES GERAIS" satisfaz "condições gerais".


9.3 Datasets (CSV)

data::load_dataset(arquivo)

Carrega um CSV como lista de linhas; cada linha é uma lista de colunas. Aspas são tratadas conforme o padrão (campo entre aspas pode conter vírgula).

dados/lotes.csv:

lote,descricao,validade
L2026-08,Lote homologado agosto/2026,2028-08-01
L2026-09,Lote homologado setembro/2026,2028-09-01
check "Percorrendo a tabela" {
  linhas = data::load_dataset("dados/lotes.csv")

  // A primeira linha é o cabeçalho
  print("colunas:", linhas.first().join(" | "))
  print("registros:", linhas.length - 1)

  // get(n) é 1-based: get(1) é a primeira coluna
  linhas.each { |linha|
    print(linha.get(1), "->", linha.get(2))
  }
}

data::lookup_value(arquivo, chave)

Procura a chave na primeira coluna e devolve o valor da segunda. Devolve null se não encontrar — e como null é falso, dá para testar direto.

check "Lote homologado" {
  lote = text::extract_from_region(1, region(400, 50, 150, 20)).trim()

  descricao = data::lookup_value("dados/lotes.csv", lote)
  assert descricao,
    "lote #{lote} não consta na tabela de homologados"

  print("lote reconhecido:", descricao)
}

9.4 Bases de consulta

Estas funções procuram arquivos de nome fixo nas pastas descritas em 9.1 e devolvem a linha inteira como lista (ou null).

data::query_gtin(codigo)

Consulta gtin.csv. Ignora pontuação do código.

dados/gtin.csv:

gtin,descricao,fabricante
7891234567895,Dipirona Sódica 500mg 20cp,Lab Exemplo
check "Produto homologado" {
  // Cruzando com o código lido da própria embalagem
  codigo = codes::decode_barcode(1)
  produto = data::query_gtin(codigo)

  assert produto,
    "GTIN #{codigo} não consta na base de produtos"

  print("produto:", produto.get(2))
  print("fabricante:", produto.get(3))
}

data::query_medicamento(registro_ou_nome)

Consulta medicamentos.csv. Aceita o número de registro (primeira coluna) ou parte do nome (segunda coluna).

dados/medicamentos.csv:

registro,nome,principio_ativo,tarja
1.0298.0123,Dipirona Sódica,dipirona monoidratada,livre
1.0298.0456,Amoxicilina,amoxicilina tri-hidratada,vermelha
check "Tarja correta na bula" {
  registro = text::extract_from_region(1, region(50, 780, 200, 15)).trim()
  medicamento = data::query_medicamento(registro)

  assert medicamento,
    "registro #{registro} não encontrado na base ANVISA"

  // Se a tarja é vermelha, o texto obrigatório precisa estar na arte
  tarja = medicamento.get(4)
  print("medicamento:", medicamento.get(2), "| tarja:", tarja)

  assert tarja != "vermelha" || text::require_text("VENDA SOB PRESCRIÇÃO"),
    "medicamento de tarja vermelha sem o texto obrigatório"
}

data::query_postal_code(cep)

Consulta ceps.csv. Aceita CEP com ou sem hífen; exige 8 dígitos.

dados/ceps.csv:

cep,logradouro,bairro,cidade,uf
01310100,Avenida Paulista,Bela Vista,Sao Paulo,SP
check "Endereço do fabricante" {
  endereco = data::query_postal_code("01310-100")
  assert endereco, "CEP não encontrado na base"

  print("logradouro:", endereco.get(2))
  print("cidade:", endereco.get(4), "-", endereco.get(5))
}

data::validate_address(cep, "trecho")

Confere se o trecho informado aparece no endereço daquele CEP.

check "Endereço impresso confere com o CEP" {
  // O endereço na embalagem precisa bater com o CEP declarado
  assert data::validate_address("01310100", "Avenida Paulista"),
    "endereço impresso não corresponde ao CEP informado"
}

9.5 Exemplo completo

// bula_com_bases.pdfl — validação cruzando PDF com bases locais
// Uso: PDFL_DATA_DIR=./bases pdfl run bula_com_bases.pdfl bula.pdf
profile "bula-com-referencias"  {

  check "Termos obrigatórios ANVISA" tags: ["glossario"] {
    faltando = data::validate_against_reference("bases/termos_anvisa.txt")
    assert faltando.length == 0,
      "textos obrigatórios ausentes: #{faltando.join("; ")}"
  }

  check "Produto na base" tags: ["dados", "critico"] {
    codigo = codes::decode_barcode(1)
    produto = data::query_gtin(codigo)
    assert produto, "GTIN #{codigo} não homologado"

    // O nome na base tem que aparecer impresso na bula
    nome = produto.get(2)
    assert text::require_text(nome),
      "o nome '#{nome}' da base não aparece na bula"
    print("produto conferido:", nome)
  }

  check "Registro e tarja" tags: ["anvisa"] {
    registro = text::extract_from_region(1, region(50, 780, 200, 15)).trim()
    med = data::query_medicamento(registro)
    assert med, "registro #{registro} não encontrado"

    assert med.get(4) != "vermelha" || text::require_text("VENDA SOB PRESCRIÇÃO"),
      "tarja vermelha exige o texto de prescrição"
  }

  check "Endereço do fabricante" tags: ["dados"] {
    assert data::validate_address("01310100", "Avenida Paulista"),
      "endereço do fabricante não confere com o CEP"
  }
}

fix:: · Índice · Próximo: Biblioteca padrão →