22
33## Overview
44
5- zig-cli now includes a powerful configuration system supporting three popular formats:
5+ zig-cli includes a powerful configuration system supporting three popular formats:
66- ** TOML** - Simple, readable configuration format
77- ** JSONC** - JSON with Comments (also handles standard JSON)
88- ** JSON5** - JSON with extended syntax (more JavaScript-like)
@@ -34,12 +34,38 @@ Each format has its own strengths:
3434- Special values: ` Infinity ` , ` -Infinity ` , ` NaN `
3535- Multi-line strings
3636
37- ### 2. Auto-discovery
37+ ### 2. Type-Safe Config Loading (Recommended)
38+
39+ Define your config schema as a Zig struct and get compile-time validation:
40+
41+ ``` zig
42+ const AppConfig = struct {
43+ database: struct {
44+ host: []const u8,
45+ port: u16,
46+ },
47+ log_level: enum { debug, info, warn, @"error" } = .info,
48+ debug: bool = false,
49+ };
50+
51+ // Load with full type checking
52+ var config = try cli.config.load(AppConfig, allocator, "config.toml");
53+ defer config.deinit();
54+
55+ // Direct field access - type-safe!
56+ std.debug.print("DB: {s}:{d}\n", .{
57+ config.value.database.host,
58+ config.value.database.port,
59+ });
60+ ```
61+
62+ ### 3. Auto-discovery
3863
3964The config system can automatically discover configuration files:
4065
4166``` zig
42- var config = try cli.config.discover(allocator, "myapp");
67+ var config = try cli.config.discover(AppConfig, allocator, "myapp");
68+ defer config.deinit();
4369```
4470
4571Search locations (in order):
@@ -49,20 +75,27 @@ Search locations (in order):
4975
5076First found file is loaded.
5177
52- ### 3. Type-safe Access
78+ ### 4. Untyped Access
79+
80+ For cases where you don't have a schema, use the raw ` Config ` type:
5381
5482``` zig
83+ var raw_config = cli.config.Config.init(allocator);
84+ defer raw_config.deinit();
85+
86+ try raw_config.loadFromFile("config.toml", .auto);
87+
5588// Typed getters with optional returns
56- const name = config .getString("name"); // ?[]const u8
57- const port = config .getInt("port"); // ?i64
58- const debug = config .getBool("debug"); // ?bool
59- const timeout = config .getFloat("timeout"); // ?f64
89+ const name = raw_config .getString("name"); // ?[]const u8
90+ const port = raw_config .getInt("port"); // ?i64
91+ const debug = raw_config .getBool("debug"); // ?bool
92+ const timeout = raw_config .getFloat("timeout"); // ?f64
6093
6194// Raw value access for complex types
62- const value = config .get("database"); // ?*Value
95+ const value = raw_config .get("database"); // ?*Value
6396```
6497
65- ### 4 . Nested Configuration
98+ ### 5 . Nested Configuration
6699
67100All formats support nested structures:
68101
@@ -89,23 +122,23 @@ timeout = 30
89122}
90123```
91124
92- ### 5 . Format Auto-detection
125+ ### 6 . Format Auto-detection
93126
94127Auto-detect based on file extension:
95128
96129``` zig
97- try config .loadFromFile("config.toml", .auto); // Detects TOML
98- try config .loadFromFile("config.json5", .auto); // Detects JSON5
99- try config .loadFromFile("config.jsonc", .auto); // Detects JSONC
100- try config .loadFromFile("config.json", .auto); // Treats as JSONC
130+ try raw_config .loadFromFile("config.toml", .auto); // Detects TOML
131+ try raw_config .loadFromFile("config.json5", .auto); // Detects JSON5
132+ try raw_config .loadFromFile("config.jsonc", .auto); // Detects JSONC
133+ try raw_config .loadFromFile("config.json", .auto); // Treats as JSONC
101134```
102135
103136Or specify explicitly:
104137
105138``` zig
106- try config .loadFromFile("myfile", .toml);
107- try config .loadFromFile("myfile", .jsonc);
108- try config .loadFromFile("myfile", .json5);
139+ try raw_config .loadFromFile("myfile", .toml);
140+ try raw_config .loadFromFile("myfile", .jsonc);
141+ try raw_config .loadFromFile("myfile", .json5);
109142```
110143
111144## Implementation Details
@@ -152,7 +185,7 @@ pub const Value = union(enum) {
152185
153186### Config Manager
154187
155- The ` Config ` type provides the high -level API:
188+ The ` Config ` type provides the low -level API:
156189
157190``` zig
158191pub const Config = struct {
@@ -176,66 +209,88 @@ pub const Config = struct {
176209};
177210```
178211
212+ ### Typed Config Loader
213+
214+ The ` ConfigLoader ` provides the high-level typed API:
215+
216+ ``` zig
217+ // These are available via cli.config namespace:
218+ pub fn load(comptime T: type, allocator, path) !ConfigLoader(T)
219+ pub fn loadFromString(comptime T: type, allocator, content, format) !ConfigLoader(T)
220+ pub fn discover(comptime T: type, allocator, app_name) !ConfigLoader(T)
221+ ```
222+
179223## Usage Examples
180224
181- ### Basic Usage
225+ ### Basic Typed Usage
182226
183227``` zig
184228const std = @import("std");
185229const cli = @import("zig-cli");
186230
231+ const ServerConfig = struct {
232+ host: []const u8 = "localhost",
233+ port: u16 = 8080,
234+ debug: bool = false,
235+ };
236+
187237pub fn main() !void {
188238 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
189239 defer _ = gpa.deinit();
190240 const allocator = gpa.allocator();
191241
192- // Load config
193- var config = try cli.config.load(allocator, "config .toml");
242+ // Load typed config
243+ var config = try cli.config.load(ServerConfig, allocator, "server .toml");
194244 defer config.deinit();
195245
196- // Use config values
197- const port = config.getInt("port") orelse 8080;
198- const host = config.getString("host") orelse "localhost";
199-
200- std.debug.print("Server: {s}:{d}\n", .{host, port});
246+ std.debug.print("Server: {s}:{d}\n", .{
247+ config.value.host,
248+ config.value.port,
249+ });
201250}
202251```
203252
204253### With CLI Integration
205254
206255``` zig
207- fn serverAction(ctx: *cli.Command .ParseContext) !void {
256+ fn serverAction(ctx: *cli.BaseCommand .ParseContext) !void {
208257 const allocator = ctx.allocator;
209258
210- // Load config
211- var config = cli.config.discover(allocator, "myserver") catch |err| {
259+ const ServerConfig = struct {
260+ host: []const u8 = "localhost",
261+ port: u16 = 8080,
262+ };
263+
264+ // Load typed config
265+ var config = cli.config.load(ServerConfig, allocator, "server.toml") catch |err| {
212266 if (err == error.FileNotFound) {
213- // No config file, use defaults
214- std.debug.print("No config found, using defaults\n", .{});
267+ std.debug.print("Config not found, using defaults\n", .{});
215268 return;
216269 }
217270 return err;
218271 };
219272 defer config.deinit();
220273
221- // CLI options override config
274+ // CLI options can override config values
222275 const port = if (ctx.getOption("port")) |p|
223276 try std.fmt.parseInt(u16, p, 10)
224277 else
225- @intCast( config.getInt(" port") orelse 8080) ;
278+ config.value. port;
226279
227280 std.debug.print("Starting server on port {d}\n", .{port});
228281}
229282```
230283
231- ### Config with Nested Values
284+ ### Untyped Config with Nested Values
232285
233286``` zig
234- var config = try cli.config.load(allocator, "config.toml");
235- defer config.deinit();
287+ var raw_config = cli.config.Config.init(allocator);
288+ defer raw_config.deinit();
289+
290+ try raw_config.loadFromFile("config.toml", .auto);
236291
237292// Access nested database config
238- if (config .get("database")) |db_value| {
293+ if (raw_config .get("database")) |db_value| {
239294 if (db_value.* == .table) {
240295 const db_table = &db_value.table;
241296
@@ -249,7 +304,7 @@ if (config.get("database")) |db_value| {
249304 else
250305 5432;
251306
252- std.debug.print("Database: {s}:{d}\n", .{host, port});
307+ std.debug.print("Database: {s}:{d}\n", .{ host, port });
253308 }
254309}
255310```
@@ -262,7 +317,11 @@ See `examples/configs/` for complete examples:
262317- ` example.jsonc ` - JSONC with comments and trailing commas
263318- ` example.json5 ` - JSON5 with extended syntax
264319
265- Run ` examples/config.zig ` for a live demonstration.
320+ Build and run the config example:
321+
322+ ``` bash
323+ zig build run-config
324+ ```
266325
267326## Performance Considerations
268327
@@ -289,7 +348,7 @@ pub const ParseError = error{
289348Errors are descriptive and can be handled gracefully:
290349
291350``` zig
292- var config = cli.config.load(allocator, "config.toml") catch |err| {
351+ var config = cli.config.load(MyConfig, allocator, "config.toml") catch |err| {
293352 switch (err) {
294353 error.FileNotFound => {
295354 std.debug.print("Config not found, using defaults\n", .{});
@@ -306,32 +365,19 @@ var config = cli.config.load(allocator, "config.toml") catch |err| {
306365
307366## Testing
308367
309- Test the config system:
310-
311368``` bash
369+ # Run all tests including config tests
370+ zig build test
371+
312372# Run config example
313- zig build-exe examples/config.zig --dep zig-cli --mod zig-cli src/root.zig
314- ./config
373+ zig build run-config
315374```
316375
317- ## Future Enhancements
318-
319- Potential additions:
320- - [ ] YAML support
321- - [ ] Environment variable expansion
322- - [ ] Config validation schemas
323- - [ ] Hot-reload support
324- - [ ] Config migration tools
325- - [ ] Deep merge for complex configs
326- - [ ] Dotted path access (` database.host ` )
327-
328376## Summary
329377
330- The configuration system adds approximately :
331- - ** ~ 1,200 lines of code ** across 5 files
332- - ** 3 format parsers ** with full feature support
333- - ** Type-safe API** for accessing configuration
378+ The configuration system provides :
379+ - ** 3 format parsers ** with full feature support (TOML, JSONC, JSON5)
380+ - ** Type-safe API ** via ` cli.config.load(T, ...) ` for compile-time schema validation
381+ - ** Untyped API** via ` cli.config.Config ` for dynamic config access
334382- ** Auto-discovery** for easy setup
335383- ** Zero runtime dependencies** - pure Zig stdlib
336-
337- This brings zig-cli to feature parity with popular CLI frameworks while maintaining Zig's philosophy of explicitness and safety.
0 commit comments