Parser de Configuração JSON em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um parser de configuração JSON que lê arquivos de configuração, valida os campos e fornece acesso tipado aos valores. Este projeto explora o módulo std.json de Zig e padrões de design para sistemas de configuração.
O Que Vamos Construir
Nosso parser vai:
- Ler e parsear arquivos JSON de configuração
- Mapear campos JSON para structs Zig tipados
- Suportar valores padrão para campos ausentes
- Validar campos obrigatórios e tipos
- Permitir configuração aninhada (seções)
- Gerar mensagens de erro descritivas
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com structs e enums em Zig
Passo 1: Estrutura do Projeto
mkdir json-config-parser
cd json-config-parser
zig init
Passo 2: Definindo o Schema de Configuração
const std = @import("std");
const json = std.json;
const mem = std.mem;
const fs = std.fs;
const Allocator = std.mem.Allocator;
/// Configuração do servidor — exemplo de schema tipado.
/// Cada campo tem um tipo preciso e um valor padrão.
/// A função `parse` do std.json mapeia JSON diretamente para esta struct.
const ConfigServidor = struct {
host: []const u8 = "127.0.0.1",
porta: u16 = 8080,
workers: u32 = 4,
max_conexoes: u32 = 1000,
timeout_ms: u64 = 30000,
modo_debug: bool = false,
};
/// Configuração do banco de dados.
const ConfigBancoDados = struct {
driver: []const u8 = "sqlite",
caminho: []const u8 = "data.db",
pool_size: u32 = 5,
log_queries: bool = false,
};
/// Configuração de logging.
const NivelLog = enum {
debug,
info,
warn,
@"error",
pub fn deString(s: []const u8) ?NivelLog {
if (mem.eql(u8, s, "debug")) return .debug;
if (mem.eql(u8, s, "info")) return .info;
if (mem.eql(u8, s, "warn")) return .warn;
if (mem.eql(u8, s, "error")) return .@"error";
return null;
}
};
const ConfigLog = struct {
nivel: []const u8 = "info",
arquivo: []const u8 = "app.log",
max_tamanho_mb: u32 = 10,
rotacao: bool = true,
};
/// Configuração raiz que agrupa todas as seções.
const ConfigApp = struct {
nome: []const u8 = "MeuApp",
versao: []const u8 = "1.0.0",
ambiente: []const u8 = "desenvolvimento",
servidor: ConfigServidor = .{},
banco_dados: ConfigBancoDados = .{},
log: ConfigLog = .{},
};
Decisão de design: Definimos valores padrão diretamente nos campos das structs. Quando std.json.parseFromSlice encontra um campo ausente no JSON, ele usa o valor padrão. Isso elimina a necessidade de lógica especial para campos opcionais.
Passo 3: Parser de Configuração
/// Erros possíveis durante o parsing de configuração.
const ConfigError = error{
ArquivoNaoEncontrado,
JSONInvalido,
CampoObrigatorio,
ValorInvalido,
ArquivoMuitoGrande,
};
/// Resultado do parsing com metadados.
const ResultadoParsing = struct {
config: ConfigApp,
avisos: [32]Aviso,
num_avisos: usize,
fonte: []const u8, // nome do arquivo
const Aviso = struct {
mensagem: [128]u8,
mensagem_len: usize,
pub fn texto(self: *const Aviso) []const u8 {
return self.mensagem[0..self.mensagem_len];
}
};
pub fn adicionarAviso(self: *ResultadoParsing, msg: []const u8) void {
if (self.num_avisos >= 32) return;
var aviso = &self.avisos[self.num_avisos];
const len = @min(msg.len, aviso.mensagem.len);
@memcpy(aviso.mensagem[0..len], msg[0..len]);
aviso.mensagem_len = len;
self.num_avisos += 1;
}
};
/// Lê e parsea um arquivo de configuração JSON.
fn lerConfiguracao(allocator: Allocator, caminho: []const u8) !ResultadoParsing {
var resultado = ResultadoParsing{
.config = .{},
.avisos = undefined,
.num_avisos = 0,
.fonte = caminho,
};
// Lê o arquivo
const arquivo = fs.cwd().openFile(caminho, .{}) catch {
resultado.adicionarAviso("Arquivo nao encontrado, usando valores padrao");
return resultado;
};
defer arquivo.close();
// Lê o conteúdo (limite de 1MB)
const conteudo = arquivo.readToEndAlloc(allocator, 1024 * 1024) catch {
return ConfigError.ArquivoMuitoGrande;
};
defer allocator.free(conteudo);
// Parsea o JSON
const parsed = json.parseFromSlice(ConfigApp, allocator, conteudo, .{
.allocate = .alloc_always,
}) catch {
return ConfigError.JSONInvalido;
};
defer parsed.deinit();
resultado.config = parsed.value;
// Validações adicionais
try validarConfig(&resultado);
return resultado;
}
/// Parsea configuração a partir de uma string JSON.
fn parsearConfigString(allocator: Allocator, json_str: []const u8) !ConfigApp {
const parsed = json.parseFromSlice(ConfigApp, allocator, json_str, .{
.allocate = .alloc_always,
}) catch {
return ConfigError.JSONInvalido;
};
defer parsed.deinit();
return parsed.value;
}
/// Valida campos que precisam de verificação semântica.
fn validarConfig(resultado: *ResultadoParsing) !void {
const config = &resultado.config;
// Validar porta
if (config.servidor.porta == 0) {
resultado.adicionarAviso("Porta 0 detectada, usando 8080");
config.servidor.porta = 8080;
}
// Validar workers
if (config.servidor.workers == 0) {
resultado.adicionarAviso("Workers 0, usando 1");
config.servidor.workers = 1;
}
// Validar nível de log
if (NivelLog.deString(config.log.nivel) == null) {
resultado.adicionarAviso("Nivel de log invalido, usando 'info'");
config.log.nivel = "info";
}
// Validar ambiente
const ambientes_validos = [_][]const u8{ "desenvolvimento", "teste", "producao" };
var ambiente_ok = false;
for (ambientes_validos) |amb| {
if (mem.eql(u8, config.ambiente, amb)) {
ambiente_ok = true;
break;
}
}
if (!ambiente_ok) {
resultado.adicionarAviso("Ambiente desconhecido");
}
}
Passo 4: Serialização (Gerar JSON)
/// Gera uma string JSON formatada a partir da configuração.
fn gerarJSON(config: *const ConfigApp, buf: []u8) ![]const u8 {
var stream = std.io.fixedBufferStream(buf);
const writer = stream.writer();
try writer.print(
\\{{
\\ "nome": "{s}",
\\ "versao": "{s}",
\\ "ambiente": "{s}",
\\ "servidor": {{
\\ "host": "{s}",
\\ "porta": {d},
\\ "workers": {d},
\\ "max_conexoes": {d},
\\ "timeout_ms": {d},
\\ "modo_debug": {any}
\\ }},
\\ "banco_dados": {{
\\ "driver": "{s}",
\\ "caminho": "{s}",
\\ "pool_size": {d},
\\ "log_queries": {any}
\\ }},
\\ "log": {{
\\ "nivel": "{s}",
\\ "arquivo": "{s}",
\\ "max_tamanho_mb": {d},
\\ "rotacao": {any}
\\ }}
\\}}
, .{
config.nome, config.versao, config.ambiente,
config.servidor.host, config.servidor.porta,
config.servidor.workers, config.servidor.max_conexoes,
config.servidor.timeout_ms, config.servidor.modo_debug,
config.banco_dados.driver, config.banco_dados.caminho,
config.banco_dados.pool_size, config.banco_dados.log_queries,
config.log.nivel, config.log.arquivo,
config.log.max_tamanho_mb, config.log.rotacao,
});
return stream.getWritten();
}
Passo 5: Interface CLI
/// Exibe a configuração atual formatada.
fn exibirConfig(config: *const ConfigApp, writer: anytype) !void {
try writer.print(
\\
\\ === Configuracao da Aplicacao ===
\\
\\ Nome: {s}
\\ Versao: {s}
\\ Ambiente: {s}
\\
\\ --- Servidor ---
\\ Host: {s}
\\ Porta: {d}
\\ Workers: {d}
\\ Max Conexoes: {d}
\\ Timeout: {d}ms
\\ Debug: {any}
\\
\\ --- Banco de Dados ---
\\ Driver: {s}
\\ Caminho: {s}
\\ Pool Size: {d}
\\ Log Queries:{any}
\\
\\ --- Log ---
\\ Nivel: {s}
\\ Arquivo: {s}
\\ Max Tam.: {d}MB
\\ Rotacao: {any}
\\
, .{
config.nome, config.versao, config.ambiente,
config.servidor.host, config.servidor.porta,
config.servidor.workers, config.servidor.max_conexoes,
config.servidor.timeout_ms, config.servidor.modo_debug,
config.banco_dados.driver, config.banco_dados.caminho,
config.banco_dados.pool_size, config.banco_dados.log_queries,
config.log.nivel, config.log.arquivo,
config.log.max_tamanho_mb, config.log.rotacao,
});
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const stdout = std.io.getStdOut().writer();
const stdin = std.io.getStdIn().reader();
try stdout.print(
\\
\\ ==========================================
\\ PARSER DE CONFIGURACAO JSON - Zig
\\ ==========================================
\\
, .{});
var config = ConfigApp{};
var buf: [4096]u8 = undefined;
while (true) {
try stdout.print(
\\
\\ [1] Carregar de arquivo
\\ [2] Parsear JSON inline
\\ [3] Exibir configuracao atual
\\ [4] Gerar JSON da config atual
\\ [5] Gerar config padrao (arquivo)
\\ [6] Sair
\\
\\ Opcao:
, .{});
const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
const opcao = mem.trim(u8, opcao_raw, " \t\r\n");
if (mem.eql(u8, opcao, "6")) break;
if (mem.eql(u8, opcao, "1")) {
try stdout.print("\n Caminho do arquivo: ", .{});
const path_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
const path = mem.trim(u8, path_raw, " \t\r\n");
const resultado = lerConfiguracao(allocator, path) catch |err| {
try stdout.print(" Erro: {any}\n", .{err});
continue;
};
config = resultado.config;
if (resultado.num_avisos > 0) {
try stdout.print("\n Avisos:\n", .{});
var i: usize = 0;
while (i < resultado.num_avisos) : (i += 1) {
try stdout.print(" - {s}\n", .{resultado.avisos[i].texto()});
}
}
try stdout.print(" Configuracao carregada com sucesso!\n", .{});
try exibirConfig(&config, stdout);
} else if (mem.eql(u8, opcao, "2")) {
try stdout.print("\n JSON (uma linha): ", .{});
const json_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
const json_str = mem.trim(u8, json_raw, " \t\r\n");
config = parsearConfigString(allocator, json_str) catch |err| {
try stdout.print(" Erro de parsing: {any}\n", .{err});
continue;
};
try stdout.print(" JSON parseado com sucesso!\n", .{});
try exibirConfig(&config, stdout);
} else if (mem.eql(u8, opcao, "3")) {
try exibirConfig(&config, stdout);
} else if (mem.eql(u8, opcao, "4")) {
var json_buf: [4096]u8 = undefined;
const json_str = gerarJSON(&config, &json_buf) catch |err| {
try stdout.print(" Erro ao gerar JSON: {any}\n", .{err});
continue;
};
try stdout.print("\n{s}\n", .{json_str});
} else if (mem.eql(u8, opcao, "5")) {
var json_buf: [4096]u8 = undefined;
const padrao = ConfigApp{};
const json_str = gerarJSON(&padrao, &json_buf) catch continue;
const file = fs.cwd().createFile("config.json", .{}) catch |err| {
try stdout.print(" Erro ao criar arquivo: {any}\n", .{err});
continue;
};
defer file.close();
file.writeAll(json_str) catch |err| {
try stdout.print(" Erro ao escrever: {any}\n", .{err});
continue;
};
try stdout.print(" Arquivo config.json criado!\n", .{});
} else {
try stdout.print(" Opcao invalida.\n", .{});
}
}
try stdout.print("\n Ate logo!\n", .{});
}
Testes
test "config padrao" {
const config = ConfigApp{};
try std.testing.expectEqualStrings("MeuApp", config.nome);
try std.testing.expectEqual(@as(u16, 8080), config.servidor.porta);
}
test "nivel log valido" {
try std.testing.expectEqual(NivelLog.info, NivelLog.deString("info").?);
try std.testing.expectEqual(NivelLog.debug, NivelLog.deString("debug").?);
try std.testing.expect(NivelLog.deString("invalido") == null);
}
test "parsear json simples" {
const json_str =
\\{"nome": "TestApp", "versao": "2.0"}
;
const config = try parsearConfigString(std.testing.allocator, json_str);
try std.testing.expectEqualStrings("TestApp", config.nome);
try std.testing.expectEqualStrings("2.0", config.versao);
// Valores padrão mantidos
try std.testing.expectEqual(@as(u16, 8080), config.servidor.porta);
}
test "gerar json" {
const config = ConfigApp{};
var buf: [4096]u8 = undefined;
const result = try gerarJSON(&config, &buf);
try std.testing.expect(result.len > 0);
try std.testing.expect(mem.indexOf(u8, result, "MeuApp") != null);
}
Compilando e Executando
zig build test
zig build run
Conceitos Aprendidos
- Parsing de JSON com
std.json.parseFromSlice - Mapeamento automático JSON -> struct
- Valores padrão em structs para campos opcionais
- Validação semântica de configuração
- Serialização manual de structs para JSON
- Leitura de arquivos com tratamento de erros
Próximos Passos
- Explore a documentação std.json para parsing avançado
- Aprenda sobre I/O de arquivos em Zig
- Construa o próximo projeto: Markdown para HTML