Skip to content

Commit

Permalink
initial push of all the current code for zdotenv
Browse files Browse the repository at this point in the history
  • Loading branch information
BitlyTwiser committed Sep 20, 2024
0 parents commit 89db4f6
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This is just used for testing of course
PASSWORD_ENV="I AM ALIVE!!"
THING_ENV=""
# A_LONG_WORD_SEPERATED_BY_STUFF_HERE=""
stuff_and_things_env="true"
maybe_env="123"


4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.zig-cache/
zig-out/
.zigmod
deps.zig
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<div align="center">

# Apprunner
<img src="/assets/zdotenv.png" width="450" height="500">
</div>

Zdotenv is a simple .env parser and a port of godotenv and ruby dotenv, but with a smidge more simplicity.

### Usage:
Add zdotenv to your zig project:
```
zig fetch --save https://github.com/BitlyTwiser/zdotenv/archive/refs/tags/0.1.0.tar.gz
```

Zdotenv has 2 pathways:

1. Absolute path to .env
- Expects an absolute path to the .env (unix systems expect a preceding / in the path)
```
const z = try Zdotenv.init(std.heap.page_allocator);
// Must be an absolute path!
try z.loadFromFile("/home/<username>/Documents/gitclones/zdotenv/test-env.env");
```

2. relaltive path:
- Expects the .env to be placed alongside the calling binary
```
const z = try Zdotenv.init(std.heap.page_allocator);
try z.load();
```

## C usage:
Zig (at the time of this writing) does not have a solid way of directly adjusting the env variables. Doing things like:
```
var env_map = std.process.getEnvMap(std.heap.page_allocator);
env_map.put("t", "val");
```

will only adjust the env map for the scope of this execution (i.e. scope of the current calling function). After function exit, the map goes back to its previous state.

Using the package is as simple as the above code examples. import below using zig zon, load the .env, and access the variables as needed using std.process.EnvMap :)
Binary file added assets/zdotenv.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const std = @import("std");

pub fn build(b: *std.Build) void {
// We build a binary for testing and the actual module for use
const target = b.standardTargetOptions(.{});

const optimize = b.standardOptimizeOption(.{});

const exe = b.addExecutable(.{
.name = "zdotenv",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibC();
b.installArtifact(exe);

// Module setup
_ = b.addModule("zdotenv", .{ .root_source_file = b.path("src/main.zig") });
var lib_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.optimize = optimize,
});

const test_step = b.step("test", "Run library tests");
test_step.dependOn(&lib_tests.step);

// Export the library module
_ = b.addModule("zdotenv", .{
.root_source_file = b.path("src/lib.zig"),
});
}
72 changes: 72 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = "zdotenv",

// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",

// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",

// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",

// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},

// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}
184 changes: 184 additions & 0 deletions src/dotenv.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
const std = @import("std");
const zdotenv = @import("lib.zig");
const assert = std.debug.assert;

const file_location_type = union(enum) {
relative,
absolute,
};

const FileError = error{ FileNotFound, FileNotAbsolute, GenericError };

// Zig fails to have a native way to do this, so we call the setenv C library
extern fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: i32) c_int;

/// Zdontenv is the primary interface for loading env values
pub const Zdotenv = struct {
allocator: std.mem.Allocator,
file: ?std.fs.File,

const env_path_relative = ".env";
const Self = @This();

pub fn init(allocator: std.mem.Allocator) !Self {
return Self{
.allocator = allocator,
.file = null,
};
}

pub fn deinit(self: Self) void {
if (self.file) |file| {
file.close();
}
}

/// Load from a specific file on disk. Must be absolute path to a location on disk
pub fn loadFromFile(self: *Self, filename: []const u8) !void {
const file = self.readFile(filename, .absolute) catch |e| {
switch (e) {
error.FileNotFound => {
std.debug.print("file {s} does not exist. Please ensure the file exists and try again\n", .{filename});
},
error.FileNotAbsolute => {
std.debug.print("given filepath {s} is not absolute. Filepath must start with / and be an absolute path on Postix systems\n", .{filename});
},
else => {
std.debug.print("error opening env file. Please check the file exists and try again\n", .{});
},
}

return;
};
// defer file.close();

// Set file
self.file = file;

// This will load the data into the environment of the calling program
try self.parseAndLoadEnv();
}

// Load will just load the default .env at location of the calling binary (i.e. expects a .env to be located next to main func call)
pub fn load(self: *Self) !void {
const file = self.readFile(env_path_relative, .relative) catch |e| {
switch (e) {
error.FileNotFound => {
std.debug.print(".env file does not exist in current directory. Please ensure the file exists and try again\n", .{});
},
else => {
std.debug.print("error opening .env file. Please check the file exists and try again\n", .{});
},
}

return;
};
// defer file.close();

//Set file
self.file = file;

// This will load the data into the environment of the calling program
try self.parseAndLoadEnv();
}

fn parseAndLoadEnv(self: *Self) !void {
var parser = try zdotenv.Parser.init(self.allocator, self.file.?);
defer parser.deinit();

var env_map = try parser.parse();

var iter = env_map.iterator();

while (iter.next()) |entry| {
// Dupe strings with terminating zero for C
const key_z = try self.allocator.dupeZ(u8, entry.key_ptr.*);
const value_z = try self.allocator.dupeZ(u8, entry.value_ptr.*);
if (setenv(key_z, value_z, 1) != 0) {
std.debug.print("Failed to set env var\n", .{});
return;
}
}
}

// Simple wrapper for opening a file passing the memory allocation to the caller. Caller MUST dealloc memory!
fn readFile(self: *Self, filename: []const u8, typ: file_location_type) FileError!std.fs.File {
_ = self;

switch (typ) {
.relative => {
return std.fs.cwd().openFile(filename, .{ .mode = .read_only }) catch |e| {
switch (e) {
error.FileNotFound => {
return FileError.FileNotFound;
},
else => {
return FileError.GenericError;
},
}
return;
};
},
.absolute => {
if (!std.fs.path.isAbsolute(filename)) return error.FileNotAbsolute;
return std.fs.openFileAbsolute(filename, .{ .mode = .read_only }) catch |e| {
switch (e) {
error.FileNotFound => {
return FileError.FileNotFound;
},
else => {
return FileError.GenericError;
},
}

return;
};
},
}
}
};

test "loading env from absolute file location" {
var z = try Zdotenv.init(std.heap.page_allocator);
// Must be an absolute path!
try z.loadFromFile("/home/butterz/Documents/gitclones/zdotenv/test-env.env");
}

test "loading generic .env" {
var z = try Zdotenv.init(std.heap.page_allocator);
try z.load();
}

// --library c
test "parse env 1" {
// use better allocators than this when not testing
const allocator = std.heap.page_allocator;
var z = try Zdotenv.init(allocator);
try z.load();

var parser = try zdotenv.Parser.init(
allocator,
z.file.?,
);
defer parser.deinit();

var env_map_global = try std.process.getEnvMap(allocator);
const password = env_map_global.get("PASSWORD_ENV") orelse "bad";

assert(std.mem.eql(u8, password, "I AM ALIVE!!"));
}

// --library c
test "parse env 2" {
// use better allocators than this when not testing
const allocator = std.heap.page_allocator;

var z = try Zdotenv.init(allocator);
try z.loadFromFile("/home/butterz/Documents/gitclones/zdotenv/test-env.env");
var parser = try zdotenv.Parser.init(allocator, z.file.?);
defer parser.deinit();

var env_map_global = try std.process.getEnvMap(allocator);
const password = env_map_global.get("PASSWORD") orelse "bad";
assert(std.mem.eql(u8, password, "asdasd123123AS@#$"));
}
7 changes: 7 additions & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Lib.zig is the package interface where all modules are collected for export

pub const parser = @import("parser.zig");
pub const Parser = parser.Parser;

pub const zdotenv = @import("dotenv.zig");
pub const Zdotenv = zdotenv.Zdotenv;
4 changes: 4 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const std = @import("std");

/// The binary main is used for testing the package to showcase the API
pub fn main() !void {}
Loading

0 comments on commit 89db4f6

Please sign in to comment.