Skip to content

zig-utils/zig-config

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZigConfig

A zero-dependency configuration loader for Zig, inspired by bunfig.

Features

  • 🔍 Multi-source loading - Local files, home directory, environment variables, defaults
  • 🎯 Type-aware env vars - Automatic parsing of booleans, numbers, arrays, and JSON
  • 🔗 Deep merging - Three strategies: replace, concat, and smart object array merging
  • 🛡️ Circular reference detection - Prevents infinite loops during merge
  • 📁 Multiple formats - JSON and Zig files (extensible)
  • 🎨 Simple API - Clean, ergonomic interface

Installation

Add zig-config as a dependency in your build.zig:

const zig-config = b.dependency("zig-config", .{
    .target = target,
    .optimize = optimize,
});

exe.root_module.addImport("zig-config", zig-config.module("zig-config"));

Quick Start

const std = @import("std");
const zig_config = @import("zig-config");

// Define your configuration structure with full type safety!
const AppConfig = struct {
    port: u16 = 8080,
    debug: bool = false,
    database: struct {
        host: []const u8 = "localhost",
        port: u16 = 5432,
    } = .{},
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Load configuration with compile-time type checking
    var config = try zig_config.loadConfig(AppConfig, allocator, .{
        .name = "myapp",
    });
    defer config.deinit(allocator);

    // Access values with full type safety - no runtime type checking needed!
    const port: u16 = config.value.port;           // Compile-time type safe!
    const debug: bool = config.value.debug;         // IDE autocomplete works!
    const db_host = config.value.database.host;     // Nested structs supported!

    std.debug.print("Server running on port {d}\n", .{port});
}

Configuration Sources

ZigConfig loads configuration from multiple sources with the following priority (highest to lowest):

  1. Environment variables (highest priority)
  2. Local project file (./myapp.json, ./config/myapp.json, ./.config/myapp.json)
  3. Home directory (~/.config/myapp.json)
  4. Defaults (provided in code)

Environment Variables

Environment variables are automatically parsed with type awareness:

# Boolean values
export MYAPP_DEBUG=true        # → bool
export MYAPP_VERBOSE=1         # → bool (true)
export MYAPP_QUIET=false       # → bool
export MYAPP_COLORS=yes        # → bool (true)

# Numbers
export MYAPP_PORT=3000         # → integer
export MYAPP_TIMEOUT=30.5      # → float

# Arrays (comma-separated)
export MYAPP_HOSTS=localhost,api.example.com,cdn.example.com  # → array of strings

# JSON objects/arrays
export MYAPP_DATABASE='{"host":"localhost","port":5432}'     # → object
export MYAPP_TAGS='["production","web"]'                      # → array

# Strings (default)
export MYAPP_NAME="My Application"                           # → string

Environment variable naming:

  • Prefix: Uppercase version of config name (or custom env_prefix)
  • Nested keys: Separated by underscores
  • Hyphens: Converted to underscores

Examples:

  • database.hostMYAPP_DATABASE_HOST
  • api-keyMYAPP_API_KEY
  • cache.ttl-secondsMYAPP_CACHE_TTL_SECONDS

Type Safety Features

✅ Compile-Time Type Checking

  • No runtime type errors - all types validated at compile time
  • IDE autocomplete - full IntelliSense support for config fields
  • Default values - define defaults directly in your struct
  • Optional fields - use ?T for optional configuration
  • Nested structures - full support for complex nested configs

Example with All Features

const Config = struct {
    // Required fields (no default)
    app_name: []const u8,

    // Optional fields
    api_key: ?[]const u8 = null,

    // Fields with defaults
    port: u16 = 8080,
    debug: bool = false,

    // Nested structures
    database: struct {
        host: []const u8 = "localhost",
        port: u16 = 5432,
        pool_size: u32 = 10,
    } = .{},

    // Arrays
    allowed_hosts: [][]const u8 = &.{},

    // Complex types
    timeouts: struct {
        connect_ms: u32 = 5000,
        read_ms: u32 = 30000,
    } = .{},
};

var config = try zig_config.loadConfig(Config, allocator, .{
    .name = "myapp",
});
defer config.deinit(allocator);

// All fields are type-safe!
std.debug.print("App: {s}, Port: {d}\n", .{
    config.value.app_name,
    config.value.port,
});

Examples

Basic Usage (Minimal Example)

const Config = struct {
    port: u16 = 8080,
    debug: bool = false,
};

var config = try zig_config.loadConfig(Config, allocator, .{
    .name = "myapp",
});
defer config.deinit(allocator);

// Fully typed access!
const port: u16 = config.value.port;

Custom Working Directory

const Config = struct {
    port: u16 = 8080,
};

var config = try zig_config.loadConfig(Config, allocator, .{
    .name = "myapp",
    .cwd = "/path/to/project",
});
defer config.deinit(allocator);

Custom Environment Prefix

const Config = struct {
    port: u16 = 8080,
};

var config = try zig_config.loadConfig(Config, allocator, .{
    .name = "myapp",
    .env_prefix = "CUSTOM",  // Uses CUSTOM_* instead of MYAPP_*
});
defer config.deinit(allocator);

Deep Merging

const zig-config = @import("zig-config");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var target = std.json.ObjectMap.init(allocator);
    defer target.deinit();
    try target.put("a", .{ .integer = 1 });

    var source = std.json.ObjectMap.init(allocator);
    defer source.deinit();
    try source.put("b", .{ .integer = 2 });

    const merged = try zig-config.deepMerge(
        allocator,
        .{ .object = target },
        .{ .object = source },
        .{ .strategy = .smart },  // or .replace, .concat
    );
    defer {
        var iter = merged.object.iterator();
        while (iter.next()) |entry| allocator.free(entry.key_ptr.*);
        var obj = merged.object;
        obj.deinit();
    }

    // Result: { "a": 1, "b": 2 }
}

Merge Strategies

Replace (default for primitives/arrays)

.{ .strategy = .replace }
// Arrays are completely replaced
// [1, 2] + [3, 4] = [3, 4]

Concat (for arrays)

.{ .strategy = .concat }
// Arrays are concatenated with deduplication
// [1, 2] + [2, 3] = [1, 2, 3]

Smart (for object arrays)

.{ .strategy = .smart }
// Object arrays are merged by key (id, name, key, path, type)
// [{"id": 1, "name": "a"}] + [{"id": 1, "name": "b"}] 
// = [{"id": 1, "name": "b"}]  // merged by id

Configuration Result

The ConfigResult struct contains:

pub const ConfigResult = struct {
    config: std.json.Value,        // The loaded configuration
    source: ConfigSource,          // Primary source (.file_local, .file_home, .env_vars, .defaults)
    sources: []SourceInfo,         // All sources that contributed
    loaded_at: i64,               // Timestamp
    allocator: std.mem.Allocator, // Allocator used
    
    pub fn deinit(self: *ConfigResult) void;
};

File Discovery

ZigConfig searches for configuration files in this order:

  1. Project root: ./myapp.json, ./myapp.zig
  2. Config directory: ./config/myapp.json, ./config/myapp.zig
  3. Hidden config: ./.config/myapp.json, ./.config/myapp.zig
  4. Home directory: ~/.config/myapp.json, ~/.config/myapp.zig

Extension priority: .json > .zig

Error Handling

ZigConfig provides detailed error types:

pub const ZigConfigError = error{
    ConfigFileNotFound,
    ConfigFileInvalid,
    ConfigFilePermissionDenied,
    ConfigFileSyntaxError,
    ConfigValidationFailed,
    ConfigSchemaViolation,
    EnvVarParseError,
    CircularReferenceDetected,
    MergeStrategyInvalid,
    CacheError,
};

Example error handling:

const config = zig-config.loadConfig(allocator, .{
    .name = "myapp",
}) catch |err| switch (err) {
    error.ConfigFileNotFound => {
        // Use defaults or create new config
        std.debug.print("No config found, using defaults\n", .{});
        return;
    },
    error.ConfigFileSyntaxError => {
        std.debug.print("Invalid JSON in config file\n", .{});
        return error.InvalidConfig;
    },
    else => return err,
};
defer config.deinit();

Testing

zig build test

All 20 tests passing! Note: There are 4 known memory "leaks" from Zig's JSON parser's internal arena allocator - these are expected and don't affect runtime behavior.

API Reference

Main Functions

loadConfig

pub fn loadConfig(
    allocator: std.mem.Allocator,
    options: types.LoadOptions,
) !types.ConfigResult

Load configuration with full error handling.

tryLoadConfig

pub fn tryLoadConfig(
    allocator: std.mem.Allocator,
    options: types.LoadOptions,
) ?types.ConfigResult

Load configuration, returning null on error (no exceptions).

deepMerge

pub fn deepMerge(
    allocator: std.mem.Allocator,
    target: std.json.Value,
    source: std.json.Value,
    options: types.MergeOptions,
) !std.json.Value

Deep merge two JSON values.

Types

pub const LoadOptions = struct {
    name: []const u8,
    defaults: ?std.json.Value = null,
    cwd: ?[]const u8 = null,
    validate: bool = true,
    cache: bool = true,
    cache_ttl: u64 = 300_000,
    env_prefix: ?[]const u8 = null,
    merge_strategy: MergeStrategy = .smart,
};

pub const MergeStrategy = enum {
    replace,
    concat,
    smart,
};

pub const ConfigSource = enum {
    file_local,
    file_home,
    package_json,
    env_vars,
    defaults,
};

License

MIT

Contributing

Contributions welcome! Please ensure:

  • All tests pass (zig build test)
  • Code follows Zig style guidelines
  • New features include tests

Acknowledgments

Inspired by bunfig by the Stacks team.

About

A zero-dependency, smart configuration loader for Zig.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Languages