Skip to content

Commit d8bf0ac

Browse files
committed
Add custom parser/validator support and enum type parsing
- Enable custom parser and validator functions for advanced field parsing and validation. - Add built-in enum type parsing. - Update documentation and tests to demonstrate new features. - Bump version to 0.7.0.
1 parent da5713e commit d8bf0ac

File tree

3 files changed

+530
-570
lines changed

3 files changed

+530
-570
lines changed

README.md

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ A Zig library for parsing environment variables directly into typed structs, pro
1414
1515
## Why
1616

17-
Managing configuration with environment variables is common, but environment variables are always strings and require manual parsing and validation. `env-struct` eliminates boilerplate by mapping environment variables directly to typed Zig structs, providing automatic type conversion and validation at load time. This approach improves safety, reduces errors, and makes configuration handling more robust and maintainable.
17+
Managing configuration with environment variables is common, but environment variables are always strings and require manual parsing and validation. `env-struct` eliminates boilerplate by mapping environment variables directly to typed Zig structs, providing automatic type conversion and validation at load time. This approach improves safety, reduces errors, and makes configuration handling more robust and maintainable.
1818

1919
> [!NOTE]
2020
> This is my first ever Zig project so feel free to contribute and send PRs!
@@ -27,6 +27,7 @@ Managing configuration with environment variables is common, but environment var
2727
-**Flexible mapping**: Fields map to their names by default, optional custom mapping
2828
-**Skip fields**: Map fields to "-" to explicitly skip environment variable lookup
2929
-**Flexible boolean parsing**: Parse "true", "1", "yes" (case-insensitive) as true
30+
-**Custom parsers**: Validation and complex parsing functions for advanced use cases
3031
-**Custom environment maps**: Load from custom maps for testing
3132

3233
## Installation
@@ -46,7 +47,7 @@ const env_struct = b.dependency("env_struct", .{
4647
.target = target,
4748
.optimize = optimize,
4849
});
49-
50+
5051
exe.root_module.addImport("env_struct", env_struct.module("env_struct"));
5152
```
5253

@@ -76,7 +77,7 @@ pub fn main() !void {
7677
const allocator = gpa.allocator();
7778
7879
const config = try env_struct.load(Config, allocator);
79-
80+
8081
std.debug.print("App: {s}\n", .{config.APP_NAME});
8182
std.debug.print("Port: {}\n", .{config.PORT});
8283
}
@@ -114,7 +115,77 @@ export APP_NAME="My App"
114115
export PORT="8080"
115116
```
116117

117-
### Advanced Usage
118+
## Advanced Usage
119+
120+
### Custom Parsers and Validators
121+
122+
The library provides two main approaches for custom parsing:
123+
124+
#### 1. Validators (Recommended for validation)
125+
Use the `validator` function to combine default parsing with custom validation:
126+
127+
```zig
128+
const std = @import("std");
129+
const env_struct = @import("env_struct");
130+
131+
// Simple validation function
132+
fn validatePort(port: u32) !u32 {
133+
if (port > 65535) return error.InvalidPort;
134+
return port;
135+
}
136+
137+
const Config = struct {
138+
port: u32,
139+
140+
const env = .{
141+
.port = .{
142+
.key = "PORT",
143+
.parser = env_struct.validator(u32, validatePort),
144+
},
145+
};
146+
};
147+
```
148+
149+
#### 2. Full Custom Parsers
150+
For complex parsing logic that doesn't use default parsing:
151+
152+
```zig
153+
// Enum parsing function
154+
const LogLevel = enum { debug, info, warn, err };
155+
156+
fn parseLogLevel(raw: []const u8, allocator: std.mem.Allocator) !LogLevel {
157+
_ = allocator; // unused in this case
158+
if (std.mem.eql(u8, raw, "debug")) return .debug;
159+
if (std.mem.eql(u8, raw, "info")) return .info;
160+
if (std.mem.eql(u8, raw, "warn")) return .warn;
161+
if (std.mem.eql(u8, raw, "error")) return .err;
162+
return error.InvalidLogLevel;
163+
}
164+
165+
const Config = struct {
166+
port: u32,
167+
log_level: LogLevel,
168+
169+
const env = .{
170+
.port = .{
171+
.key = "PORT",
172+
.parser = env_struct.validator(u32, validatePort),
173+
},
174+
.log_level = .{
175+
.key = "LOG_LEVEL",
176+
.parser = parseLogLevel,
177+
},
178+
};
179+
};
180+
```
181+
182+
**Key Points:**
183+
- Use `validator()` when you want default parsing + validation
184+
- Use custom parsers for complex parsing that doesn't follow default rules
185+
- All custom parsers use the signature: `fn(raw: []const u8, allocator: Allocator) !T`
186+
- The `parseValue()` function is available for implementing custom parsers that want to reuse default parsing
187+
188+
### Nested Structs & Complex Configuration
118189

119190
```zig
120191
const Config = struct {
@@ -131,7 +202,7 @@ const Config = struct {
131202
};
132203
```
133204

134-
### Nested Structs & Custom Environment Maps
205+
### Custom Environment Maps
135206

136207
```zig
137208
const DatabaseConfig = struct {
@@ -170,17 +241,17 @@ const test_config = try env_struct.loadMap(ServerConfig, custom_env, allocator);
170241
Fields are mapped to environment variables with these behaviors:
171242

172243
- **Default mapping**: Fields automatically map to environment variables with the same name
173-
- **Custom mapping**: Use the `env` declaration to map fields to different environment variable names
244+
- **Custom mapping**: Use the `env` declaration to map fields to different environment variable names
174245
- **Skip mapping**: Map a field to `"-"` to skip environment variable lookup (must have default values or be optional)
175246
- **Field requirements**: Fields without default values must either have corresponding environment variables or be optional
176247
- **Optional env declaration**: The `env` declaration is only needed for custom mappings or skipping fields
177248

178249
```zig
179250
const Config = struct {
180251
app_name: []const u8, // Maps to "app_name" env var
181-
custom_port: u32, // Maps to "PORT" env var
252+
custom_port: u32, // Maps to "PORT" env var
182253
skipped_field: []const u8 = "default", // No env var lookup
183-
254+
184255
const env = .{
185256
.custom_port = "PORT", // Custom mapping
186257
.skipped_field = "-", // Skip mapping
@@ -189,7 +260,7 @@ const Config = struct {
189260
};
190261
```
191262

192-
## Supported Types
263+
## Built-in Supported Types
193264

194265
| Type | Examples | Notes |
195266
|------|----------|-------|
@@ -198,6 +269,7 @@ const Config = struct {
198269
| `u8`, `u16`, `u32`, `u64`, `u128`, `usize` | `"42"`, `"255"` | Unsigned integers |
199270
| `f16`, `f32`, `f64`, `f80`, `f128` | `"3.14"` | Floating point |
200271
| `bool` | `"true"`, `"1"`, `"yes"` | Case-insensitive |
272+
| `enum` | `"debug"`, `"info"` | Matches enum field names |
201273
| `?T` | Any valid `T` or missing | Optional types |
202274
| `struct` | N/A | Nested structs |
203275

@@ -209,6 +281,39 @@ Load configuration from system environment variables.
209281
### `loadMap(comptime T: type, env_map: std.process.EnvMap, allocator: std.mem.Allocator) !T`
210282
Load configuration from a custom environment map.
211283

284+
### `parseValue(comptime T: type, raw_value: []const u8, allocator: std.mem.Allocator) !T`
285+
Parse a raw string value into the specified type. Useful for implementing custom parsers that want to preserve default parsing behavior.
286+
287+
### `validator(comptime T: type, comptime validateFn: anytype) fn([]const u8, std.mem.Allocator) anyerror!T`
288+
Create a validator function that combines default parsing with custom validation. The validation function should have the signature `fn(T) !T`.
289+
290+
### Custom Parser Function Signature
291+
292+
Custom parsers must follow this signature:
293+
294+
```zig
295+
fn parserFunction(raw_value: []const u8, allocator: std.mem.Allocator) !T
296+
```
297+
298+
Where:
299+
- `raw_value`: The raw string from the environment variable
300+
- `allocator`: Memory allocator for dynamic allocations (can be ignored if not needed)
301+
- `T`: The target type to parse into
302+
- Returns the parsed value or an error
303+
304+
### Validator Function Signature
305+
306+
Validator functions used with `validator()` should have this signature:
307+
308+
```zig
309+
fn validatorFunction(value: T) !T
310+
```
311+
312+
Where:
313+
- `value`: The already-parsed value from default parsing
314+
- `T`: The type being validated
315+
- Returns the validated value or an error
316+
212317
## Building
213318

214319
```bash
@@ -219,4 +324,4 @@ zig build
219324

220325
```bash
221326
zig test src/env_struct.zig
222-
```
327+
```

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
// This is a [Semantic Version](https://semver.org/).
1212
// In a future version of Zig it will be used for package deduplication.
13-
.version = "0.6.0",
13+
.version = "0.7.0",
1414

1515
// Together with name, this represents a globally unique package
1616
// identifier. This field is generated by the Zig toolchain when the

0 commit comments

Comments
 (0)