Parser de Configuração JSON em Zig — Tutorial Passo a Passo

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

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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.