Skip to content

Commit 68d138f

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. - Fix version-tag.yml workflow - Bump version to 0.7.0.
1 parent da5713e commit 68d138f

File tree

4 files changed

+605
-642
lines changed

4 files changed

+605
-642
lines changed

.github/workflows/version-tag.yml

Lines changed: 75 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,85 +2,88 @@ name: Create Version Tag
22

33
on:
44
push:
5-
branches: main
5+
branches: [main]
66
paths:
7-
- 'build.zig.zon'
7+
- "build.zig.zon"
88

99
jobs:
1010
check-version-and-tag:
1111
runs-on: ubuntu-latest
12-
12+
permissions:
13+
contents: write
14+
1315
steps:
14-
- name: Checkout code
15-
uses: actions/checkout@v4
16-
with:
17-
fetch-depth: 2
18-
19-
- name: Check if version changed
20-
id: version-check
21-
run: |
22-
# Extract current version from build.zig.zon
23-
CURRENT_VERSION=$(grep -E '^\s*\.version\s*=\s*"[^"]*"' build.zig.zon | sed -E 's/.*"([^"]*)".*$/\1/')
24-
echo "Current version: $CURRENT_VERSION"
25-
26-
# Check if this is the first commit or if we can get the previous version
27-
if git show HEAD~1:build.zig.zon > /dev/null 2>&1; then
28-
PREVIOUS_VERSION=$(git show HEAD~1:build.zig.zon | grep -E '^\s*\.version\s*=\s*"[^"]*"' | sed -E 's/.*"([^"]*)".*$/\1/')
29-
echo "Previous version: $PREVIOUS_VERSION"
30-
31-
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
32-
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
with:
19+
fetch-depth: 2
20+
token: ${{ secrets.GITHUB_TOKEN }}
21+
22+
- name: Check if version changed
23+
id: version-check
24+
run: |
25+
# Extract current version from build.zig.zon
26+
CURRENT_VERSION=$(grep -E '^\s*\.version\s*=\s*"[^"]*"' build.zig.zon | sed -E 's/.*"([^"]*)".*$/\1/')
27+
echo "Current version: $CURRENT_VERSION"
28+
29+
# Check if this is the first commit or if we can get the previous version
30+
if git show HEAD~1:build.zig.zon > /dev/null 2>&1; then
31+
PREVIOUS_VERSION=$(git show HEAD~1:build.zig.zon | grep -E '^\s*\.version\s*=\s*"[^"]*"' | sed -E 's/.*"([^"]*)".*$/\1/')
32+
echo "Previous version: $PREVIOUS_VERSION"
33+
34+
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
35+
echo "Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
36+
echo "version-changed=true" >> $GITHUB_OUTPUT
37+
echo "new-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
38+
else
39+
echo "Version unchanged"
40+
echo "version-changed=false" >> $GITHUB_OUTPUT
41+
fi
42+
else
43+
echo "First commit or previous version not found, creating tag for current version"
3344
echo "version-changed=true" >> $GITHUB_OUTPUT
3445
echo "new-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
46+
fi
47+
48+
- name: Update README.md with new version
49+
if: steps.version-check.outputs.version-changed == 'true'
50+
run: |
51+
VERSION="${{ steps.version-check.outputs.new-version }}"
52+
53+
# Update the zig fetch command in README.md
54+
sed -i "s|zig fetch --save \"git+https://github.com/xarunoba/env-struct.zig#v[^\"]*\"|zig fetch --save \"git+https://github.com/xarunoba/env-struct.zig#v${VERSION}\"|g" README.md
55+
56+
# Check if README.md was actually modified
57+
if git diff --quiet README.md; then
58+
echo "No changes needed in README.md"
59+
else
60+
echo "Updated README.md with version v${VERSION}"
61+
62+
# Commit and push the README update
63+
git config --local user.email "action@github.com"
64+
git config --local user.name "GitHub Action"
65+
git add README.md
66+
git commit -m "Update README.md with version v${VERSION}"
67+
git push
68+
fi
69+
70+
- name: Create or update tag
71+
if: steps.version-check.outputs.version-changed == 'true'
72+
run: |
73+
VERSION="${{ steps.version-check.outputs.new-version }}"
74+
75+
# Check if tag already exists
76+
if git tag | grep -q "^v${VERSION}$"; then
77+
echo "Tag v${VERSION} already exists, deleting and recreating..."
78+
# Delete the tag locally and remotely
79+
git tag -d "v${VERSION}" || true
80+
git push --delete origin "v${VERSION}" || true
3581
else
36-
echo "Version unchanged"
37-
echo "version-changed=false" >> $GITHUB_OUTPUT
82+
echo "Creating new tag v${VERSION}"
3883
fi
39-
else
40-
echo "First commit or previous version not found, creating tag for current version"
41-
echo "version-changed=true" >> $GITHUB_OUTPUT
42-
echo "new-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
43-
fi
44-
45-
- name: Update README.md with new version
46-
if: steps.version-check.outputs.version-changed == 'true'
47-
run: |
48-
VERSION="${{ steps.version-check.outputs.new-version }}"
49-
50-
# Update the zig fetch command in README.md
51-
sed -i "s|zig fetch --save \"git+https://github.com/xarunoba/env-struct.zig#v[^\"]*\"|zig fetch --save \"git+https://github.com/xarunoba/env-struct.zig#v${VERSION}\"|g" README.md
52-
53-
# Check if README.md was actually modified
54-
if git diff --quiet README.md; then
55-
echo "No changes needed in README.md"
56-
else
57-
echo "Updated README.md with version v${VERSION}"
58-
59-
# Commit and push the README update
60-
git config --local user.email "action@github.com"
61-
git config --local user.name "GitHub Action"
62-
git add README.md
63-
git commit -m "Update README.md with version v${VERSION}"
64-
git push
65-
fi
66-
67-
- name: Create or update tag
68-
if: steps.version-check.outputs.version-changed == 'true'
69-
run: |
70-
VERSION="${{ steps.version-check.outputs.new-version }}"
71-
72-
# Check if tag already exists
73-
if git tag | grep -q "^v${VERSION}$"; then
74-
echo "Tag v${VERSION} already exists, deleting and recreating..."
75-
# Delete the tag locally and remotely
76-
git tag -d "v${VERSION}" || true
77-
git push --delete origin "v${VERSION}" || true
78-
else
79-
echo "Creating new tag v${VERSION}"
80-
fi
81-
82-
# Create and push the tag
83-
git tag -a "v${VERSION}" -m "${VERSION} Release"
84-
git push origin "v${VERSION}"
85-
86-
echo "Successfully created/updated and pushed tag v${VERSION}"
84+
85+
# Create and push the tag
86+
git tag -a "v${VERSION}" -m "${VERSION} Release"
87+
git push origin "v${VERSION}"
88+
89+
echo "Successfully created/updated and pushed tag v${VERSION}"

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)