Skip to content

Commit b0bc2d1

Browse files
chore: wip
1 parent 5fcffa7 commit b0bc2d1

5 files changed

Lines changed: 672 additions & 672 deletions

File tree

CONFIG_FEATURES.md

Lines changed: 108 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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

3964
The 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

4571
Search locations (in order):
@@ -49,20 +75,27 @@ Search locations (in order):
4975

5076
First 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

67100
All 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

94127
Auto-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

103136
Or 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
158191
pub 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
184228
const std = @import("std");
185229
const cli = @import("zig-cli");
186230
231+
const ServerConfig = struct {
232+
host: []const u8 = "localhost",
233+
port: u16 = 8080,
234+
debug: bool = false,
235+
};
236+
187237
pub 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{
289348
Errors 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

Comments
 (0)