161 lines
5.9 KiB
Zig
161 lines
5.9 KiB
Zig
const std = @import("std");
|
|
|
|
/// Structure that holds environment variables loaded from .env file
|
|
pub const DotEnv = struct {
|
|
map: std.StringHashMap([]const u8),
|
|
allocator: std.mem.Allocator,
|
|
|
|
/// Loads the .env file from the given path and returns a DotEnv instance
|
|
pub fn load(allocator: std.mem.Allocator, path: []const u8) !DotEnv {
|
|
var map = std.StringHashMap([]const u8).init(allocator);
|
|
|
|
const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
|
|
error.FileNotFound => return error.DotEnvFileNotFound,
|
|
else => return err,
|
|
};
|
|
defer file.close();
|
|
|
|
const max_size = 10 * 1024 * 1024; // 10MB max
|
|
const content = try file.readToEndAlloc(allocator, max_size);
|
|
defer allocator.free(content);
|
|
|
|
var lines = std.mem.splitScalar(u8, content, '\n');
|
|
while (lines.next()) |raw_line| {
|
|
var line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
|
|
// skip empty lines and comments
|
|
if (line.len == 0 or line[0] == '#') continue;
|
|
|
|
// find the first '=' character
|
|
if (std.mem.indexOfScalar(u8, line, '=')) |eq_pos| {
|
|
const key = std.mem.trim(u8, line[0..eq_pos], " \t");
|
|
var value = std.mem.trim(u8, line[eq_pos + 1 ..], " \t\r\n");
|
|
|
|
// optionally remove surrounding single or double quotes
|
|
if (value.len >= 2 and ((value[0] == '"' and value[value.len - 1] == '"') or
|
|
(value[0] == '\'' and value[value.len - 1] == '\'')))
|
|
{
|
|
value = value[1 .. value.len - 1];
|
|
}
|
|
|
|
// Duplicate key and value to own them in the map
|
|
const key_dup = try allocator.dupe(u8, key);
|
|
errdefer allocator.free(key_dup);
|
|
|
|
const value_dup = try allocator.dupe(u8, value);
|
|
errdefer allocator.free(value_dup);
|
|
|
|
try map.put(key_dup, value_dup);
|
|
}
|
|
}
|
|
return DotEnv{
|
|
.map = map,
|
|
.allocator = allocator,
|
|
};
|
|
}
|
|
|
|
/// Frees all allocated memory (keys and values)
|
|
pub fn deinit(self: *DotEnv) void {
|
|
var it = self.map.iterator();
|
|
while (it.next()) |entry| {
|
|
self.allocator.free(entry.key_ptr.*);
|
|
self.allocator.free(entry.value_ptr.*);
|
|
}
|
|
self.map.deinit();
|
|
}
|
|
|
|
/// Returns the value for key, or null if not present
|
|
pub fn get(self: DotEnv, key: []const u8) ?[]const u8 {
|
|
return self.map.get(key);
|
|
}
|
|
|
|
/// Returns the value for key, or a default value if not present
|
|
pub fn getOr(self: DotEnv, key: []const u8, default: []const u8) []const u8 {
|
|
return self.get(key) orelse default;
|
|
}
|
|
|
|
/// Returns the value for a key, or errors if the required variable is missing
|
|
pub fn require(self: DotEnv, key: []const u8) ![]const u8 {
|
|
return self.get(key) orelse error.MissingRequiredEnvVar;
|
|
}
|
|
};
|
|
|
|
pub const Error = error{ DotEnvFileNotFound, MissingRequiredEnvVar };
|
|
|
|
// ╔══════════════════════════════════════════════════════════╗
|
|
// ║ UNIT TESTS ║
|
|
// ╚══════════════════════════════════════════════════════════╝
|
|
|
|
test "load .env file - basic functionality" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
const content =
|
|
\\API_KEY=12345
|
|
\\DEBUG=true
|
|
\\NAME=zig tester
|
|
\\#COMMENT=ignored
|
|
\\ SPACED = value with spaces
|
|
\\QUOTED="quoted value"
|
|
\\EMPTY=
|
|
;
|
|
|
|
try tmp.dir.writeFile(.{ .data = content, .sub_path = "test.env" });
|
|
|
|
const tmp_path = try tmp.dir.realpathAlloc(allocator, ".");
|
|
defer allocator.free(tmp_path);
|
|
|
|
const full_path = try std.fs.path.join(allocator, &[_][]const u8{
|
|
tmp_path,
|
|
"test.env",
|
|
});
|
|
defer allocator.free(full_path);
|
|
|
|
var env = try DotEnv.load(allocator, full_path);
|
|
defer env.deinit();
|
|
|
|
try std.testing.expectEqualStrings("12345", env.require("API_KEY") catch unreachable);
|
|
try std.testing.expectEqualStrings("true", env.get("DEBUG").?);
|
|
try std.testing.expectEqualStrings("zig tester", env.get("NAME").?);
|
|
try std.testing.expectEqualStrings("value with spaces", env.get("SPACED").?);
|
|
try std.testing.expectEqualStrings("quoted value", env.get("QUOTED").?);
|
|
try std.testing.expect(env.get("COMMENT") == null);
|
|
try std.testing.expectEqualStrings("", env.get("EMPTY").?);
|
|
}
|
|
|
|
test "require() errors on missing required var" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
|
|
try tmp.dir.writeFile(.{ .data = "OTHER=123", .sub_path = "empty.env" });
|
|
|
|
const tmp_path = try tmp.dir.realpathAlloc(allocator, ".");
|
|
defer allocator.free(tmp_path);
|
|
|
|
const full_path = try std.fs.path.join(allocator, &[_][]const u8{ tmp_path, "empty.env" });
|
|
defer allocator.free(full_path);
|
|
|
|
var env = try DotEnv.load(allocator, full_path);
|
|
defer env.deinit();
|
|
|
|
try std.testing.expectError(error.MissingRequiredEnvVar, env.require("MISSING"));
|
|
}
|
|
|
|
test "missing file should return error" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
try std.testing.expectError(error.DotEnvFileNotFound, DotEnv.load(allocator, "this-file-not-exists.env"));
|
|
}
|
|
|
|
test "getOr() returns default on missing key" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
var env: DotEnv = DotEnv{ .allocator = allocator, .map = std.StringHashMap([]const u8).init(allocator) };
|
|
|
|
try std.testing.expectEqualStrings("default-value", env.getOr("NOT EXIST", "default-value"));
|
|
try std.testing.expect(env.get("NOT EXIST") == null);
|
|
}
|