mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
commit
bf62fdcf5b
@ -39,7 +39,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
lib.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
|
||||
jetzig_module.addImport("zmpl", zmpl_dep.module("zmpl"));
|
||||
lib.root_module.addImport("args", zig_args_dep.module("args"));
|
||||
jetzig_module.addImport("args", zig_args_dep.module("args"));
|
||||
|
||||
// This is the way to make it look nice in the zig build script
|
||||
// If we would do it the other way around, we would have to do
|
||||
|
@ -7,8 +7,8 @@
|
||||
.hash = "1220e5ede084ca6b94defd466a8f8779aab151d37bf688fefb928fded6f02cde4135",
|
||||
},
|
||||
.args = .{
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
|
||||
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829",
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
|
||||
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -5,8 +5,8 @@
|
||||
|
||||
.dependencies = .{
|
||||
.args = .{
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
|
||||
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829",
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
|
||||
.hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
14
cli/cli.zig
14
cli/cli.zig
@ -4,6 +4,7 @@ const init = @import("commands/init.zig");
|
||||
const update = @import("commands/update.zig");
|
||||
const generate = @import("commands/generate.zig");
|
||||
const server = @import("commands/server.zig");
|
||||
const bundle = @import("commands/bundle.zig");
|
||||
|
||||
const Options = struct {
|
||||
help: bool = false,
|
||||
@ -19,6 +20,7 @@ const Options = struct {
|
||||
.update = "Update current project to latest version of Jetzig",
|
||||
.generate = "Generate scaffolding",
|
||||
.server = "Run a development server",
|
||||
.bundle = "Create a deployment bundle",
|
||||
.help = "Print help and exit",
|
||||
},
|
||||
};
|
||||
@ -29,8 +31,10 @@ const Verb = union(enum) {
|
||||
update: update.Options,
|
||||
generate: generate.Options,
|
||||
server: server.Options,
|
||||
bundle: bundle.Options,
|
||||
g: generate.Options,
|
||||
s: server.Options,
|
||||
b: bundle.Options,
|
||||
};
|
||||
|
||||
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
|
||||
@ -52,7 +56,7 @@ pub fn main() !void {
|
||||
}
|
||||
};
|
||||
|
||||
if (options.options.help or options.verb == null) {
|
||||
if ((!options.options.help and options.verb == null) or (options.options.help and options.verb == null)) {
|
||||
try args.printHelp(Options, "jetzig", writer);
|
||||
try writer.writeAll(
|
||||
\\
|
||||
@ -62,6 +66,7 @@ pub fn main() !void {
|
||||
\\ update Update current project to latest version of Jetzig.
|
||||
\\ generate Generate scaffolding.
|
||||
\\ server Run a development server.
|
||||
\\ bundle Create a deployment bundle.
|
||||
\\
|
||||
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
|
||||
\\
|
||||
@ -100,6 +105,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
|
||||
options.positionals,
|
||||
.{ .help = options.options.help },
|
||||
),
|
||||
.b, .bundle => |opts| bundle.run(
|
||||
allocator,
|
||||
opts,
|
||||
writer,
|
||||
options.positionals,
|
||||
.{ .help = options.options.help },
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
139
cli/commands/bundle.zig
Normal file
139
cli/commands/bundle.zig
Normal file
@ -0,0 +1,139 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const args = @import("args");
|
||||
|
||||
const util = @import("../util.zig");
|
||||
|
||||
/// Command line options for the `bundle` command.
|
||||
pub const Options = struct {
|
||||
optimize: enum { Debug, ReleaseFast, ReleaseSmall } = .ReleaseFast,
|
||||
arch: enum { x86_64, aarch64, default } = .default,
|
||||
os: enum { linux, macos, windows, default } = .default,
|
||||
|
||||
pub const meta = .{
|
||||
.full_text =
|
||||
\\Creates a deployment bundle.
|
||||
\\
|
||||
\\On Windows, `tar.exe` is used to generate a `.zip` file.
|
||||
\\
|
||||
\\On other operating systems, `tar` is used to generate a `.tar.gz` file.
|
||||
\\
|
||||
\\The deployment bundle contains a compiled executable with the `public/` and `static/`
|
||||
\\directories included. This bundle can be copied to a deployment server, unpacked, and
|
||||
\\launched in place.
|
||||
,
|
||||
.option_docs = .{
|
||||
.optimize = "Set optimization level, must be one of { Debug, ReleaseFast, ReleaseSmall } (default: ReleaseFast)",
|
||||
.arch = "Set build target CPU architecture, must be one of { x86_64, aarch64 } (default: Current CPU arch)",
|
||||
.os = "Set build target operating system, must be one of { linux, macos, windows } (default: Current OS)",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/// Run the deployment bundle generator. Create an archive containing the Jetzig executable,
|
||||
/// with `public/` and `static/` directories.
|
||||
pub fn run(
|
||||
allocator: std.mem.Allocator,
|
||||
options: Options,
|
||||
writer: anytype,
|
||||
positionals: [][]const u8,
|
||||
other_options: struct { help: bool },
|
||||
) !void {
|
||||
_ = positionals;
|
||||
if (other_options.help) {
|
||||
try args.printHelp(Options, "jetzig bundle", writer);
|
||||
return;
|
||||
}
|
||||
|
||||
std.debug.print("Compiling bundle...\n", .{});
|
||||
var cwd = try util.detectJetzigProjectDir();
|
||||
defer cwd.close();
|
||||
|
||||
const path = try cwd.realpathAlloc(allocator, ".");
|
||||
defer allocator.free(path);
|
||||
|
||||
if (try util.locateExecutable(allocator, cwd, .{ .relative = true })) |executable| {
|
||||
defer allocator.free(executable);
|
||||
|
||||
var tar_argv = std.ArrayList([]const u8).init(allocator);
|
||||
defer tar_argv.deinit();
|
||||
|
||||
var install_argv = std.ArrayList([]const u8).init(allocator);
|
||||
defer install_argv.deinit();
|
||||
|
||||
try install_argv.appendSlice(&[_][]const u8{ "zig", "build" });
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.windows => try tar_argv.appendSlice(&[_][]const u8{
|
||||
"tar.exe",
|
||||
"-a",
|
||||
"-c",
|
||||
"-f",
|
||||
"bundle.zip",
|
||||
executable,
|
||||
}),
|
||||
else => try tar_argv.appendSlice(&[_][]const u8{
|
||||
"tar",
|
||||
"--transform=s,^,jetzig/,",
|
||||
"--transform=s,^jetzig/zig-out/bin/,jetzig/,",
|
||||
"-zcf",
|
||||
"bundle.tar.gz",
|
||||
executable,
|
||||
}),
|
||||
}
|
||||
|
||||
switch (options.optimize) {
|
||||
.ReleaseFast => try install_argv.append("-Doptimize=ReleaseFast"),
|
||||
.ReleaseSmall => try install_argv.append("-Doptimize=ReleaseSmall"),
|
||||
.Debug => try install_argv.append("-Doptimize=Debug"),
|
||||
}
|
||||
|
||||
var target_buf = std.ArrayList([]const u8).init(allocator);
|
||||
defer target_buf.deinit();
|
||||
|
||||
try target_buf.append("-Dtarget=");
|
||||
switch (options.arch) {
|
||||
.x86_64 => try target_buf.append("x86_64"),
|
||||
.aarch64 => try target_buf.append("aarch64"),
|
||||
.default => try target_buf.append(@tagName(builtin.cpu.arch)),
|
||||
}
|
||||
|
||||
try target_buf.append("-");
|
||||
|
||||
switch (options.os) {
|
||||
.linux => try target_buf.append("linux"),
|
||||
.macos => try target_buf.append("macos"),
|
||||
.windows => try target_buf.append("windows"),
|
||||
.default => try target_buf.append(@tagName(builtin.os.tag)),
|
||||
}
|
||||
|
||||
const target = try std.mem.concat(allocator, u8, target_buf.items);
|
||||
defer allocator.free(target);
|
||||
|
||||
try install_argv.append(target);
|
||||
try install_argv.append("install");
|
||||
|
||||
var public_dir: ?std.fs.Dir = cwd.openDir("public", .{}) catch null;
|
||||
defer if (public_dir) |*dir| dir.close();
|
||||
|
||||
var static_dir: ?std.fs.Dir = cwd.openDir("static", .{}) catch null;
|
||||
defer if (static_dir) |*dir| dir.close();
|
||||
|
||||
if (public_dir != null) try tar_argv.append("public");
|
||||
if (static_dir != null) try tar_argv.append("static");
|
||||
|
||||
try util.runCommand(allocator, path, install_argv.items);
|
||||
try util.runCommand(allocator, path, tar_argv.items);
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.windows => std.debug.print("Bundle `bundle.zip` generated successfully.", .{}),
|
||||
else => std.debug.print("Bundle `bundle.tar.gz` generated successfully.", .{}),
|
||||
}
|
||||
util.printSuccess();
|
||||
} else {
|
||||
std.debug.print("Unable to locate compiled executable. Exiting.", .{});
|
||||
util.printFailure();
|
||||
std.os.exit(1);
|
||||
}
|
||||
}
|
@ -4,12 +4,13 @@ const view = @import("generate/view.zig");
|
||||
const partial = @import("generate/partial.zig");
|
||||
const layout = @import("generate/layout.zig");
|
||||
const middleware = @import("generate/middleware.zig");
|
||||
const secret = @import("generate/secret.zig");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
/// Command line options for the `generate` command.
|
||||
pub const Options = struct {
|
||||
pub const meta = .{
|
||||
.usage_summary = "[view|partial|layout|middleware] [options]",
|
||||
.usage_summary = "[view|partial|layout|middleware|secret] [options]",
|
||||
.full_text =
|
||||
\\Generates scaffolding for views, middleware, and other objects in future.
|
||||
\\
|
||||
@ -44,7 +45,7 @@ pub fn run(
|
||||
try args.printHelp(Options, "jetzig generate", writer);
|
||||
return;
|
||||
}
|
||||
var generate_type: ?enum { view, partial, layout, middleware } = null;
|
||||
var generate_type: ?enum { view, partial, layout, middleware, secret } = null;
|
||||
var sub_args = std.ArrayList([]const u8).init(allocator);
|
||||
defer sub_args.deinit();
|
||||
|
||||
@ -57,6 +58,8 @@ pub fn run(
|
||||
generate_type = .layout;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
|
||||
generate_type = .middleware;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "secret")) {
|
||||
generate_type = .secret;
|
||||
} else if (generate_type == null) {
|
||||
std.debug.print("Unknown generator command: {s}\n", .{arg});
|
||||
return error.JetzigCommandError;
|
||||
@ -71,6 +74,7 @@ pub fn run(
|
||||
.partial => partial.run(allocator, cwd, sub_args.items),
|
||||
.layout => layout.run(allocator, cwd, sub_args.items),
|
||||
.middleware => middleware.run(allocator, cwd, sub_args.items),
|
||||
.secret => secret.run(allocator, cwd, sub_args.items),
|
||||
};
|
||||
} else {
|
||||
std.debug.print("Missing sub-command. Expected: [view|partial|layout|middleware]\n", .{});
|
||||
|
16
cli/commands/generate/secret.zig
Normal file
16
cli/commands/generate/secret.zig
Normal file
@ -0,0 +1,16 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Generate a secure random secret and output to stdout.
|
||||
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
|
||||
_ = allocator;
|
||||
_ = args;
|
||||
_ = cwd;
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
var secret: [44]u8 = undefined;
|
||||
|
||||
for (0..44) |index| {
|
||||
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
|
||||
}
|
||||
|
||||
std.debug.print("{s}\n", .{secret});
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
const std = @import("std");
|
||||
|
||||
const args = @import("args");
|
||||
|
||||
const util = @import("../util.zig");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000;
|
||||
|
||||
/// Command line options for the `update` command.
|
||||
/// Command line options for the `server` command.
|
||||
pub const Options = struct {
|
||||
reload: bool = true,
|
||||
|
||||
@ -68,7 +69,7 @@ pub fn run(
|
||||
&[_][]const u8{ "zig", "build", "-Djetzig_runner=true", "install" },
|
||||
);
|
||||
|
||||
const exe_path = try locateExecutable(allocator, cwd);
|
||||
const exe_path = try util.locateExecutable(allocator, cwd, .{});
|
||||
if (exe_path == null) {
|
||||
std.debug.print("Unable to locate compiled executable. Exiting.\n", .{});
|
||||
std.os.exit(1);
|
||||
@ -137,34 +138,3 @@ fn totalMtime(allocator: std.mem.Allocator, cwd: std.fs.Dir, sub_path: []const u
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
fn locateExecutable(allocator: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 {
|
||||
const file = dir.openFile(".jetzig", .{}) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
const content = try file.readToEndAlloc(allocator, 1024);
|
||||
defer allocator.free(content);
|
||||
|
||||
const exe_name = util.strip(content);
|
||||
const suffix = if (builtin.os.tag == .windows) ".exe" else "";
|
||||
const full_name = try std.mem.concat(allocator, u8, &[_][]const u8{ exe_name, suffix });
|
||||
defer allocator.free(full_name);
|
||||
|
||||
// XXX: Will fail if user sets a custom install path.
|
||||
var bin_dir = try dir.openDir("zig-out/bin", .{ .iterate = true });
|
||||
defer bin_dir.close();
|
||||
|
||||
var walker = try bin_dir.walk(allocator);
|
||||
defer walker.deinit();
|
||||
|
||||
while (try walker.next()) |entry| {
|
||||
if (entry.kind == .file and std.mem.eql(u8, entry.path, full_name)) {
|
||||
return try bin_dir.realpathAlloc(allocator, entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
41
cli/util.zig
41
cli/util.zig
@ -1,4 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's
|
||||
/// build.zig which are stored in the executable as a module.
|
||||
@ -167,3 +168,43 @@ pub fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Attempt to locate the main application executable in `zig-out/bin/`
|
||||
pub fn locateExecutable(
|
||||
allocator: std.mem.Allocator,
|
||||
dir: std.fs.Dir,
|
||||
options: struct { relative: bool = false },
|
||||
) !?[]const u8 {
|
||||
const file = dir.openFile(".jetzig", .{}) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
const content = try file.readToEndAlloc(allocator, 1024);
|
||||
defer allocator.free(content);
|
||||
|
||||
const exe_name = strip(content);
|
||||
const suffix = if (builtin.os.tag == .windows) ".exe" else "";
|
||||
const full_name = try std.mem.concat(allocator, u8, &[_][]const u8{ exe_name, suffix });
|
||||
defer allocator.free(full_name);
|
||||
|
||||
// XXX: Will fail if user sets a custom install path.
|
||||
var bin_dir = try dir.openDir("zig-out/bin", .{ .iterate = true });
|
||||
defer bin_dir.close();
|
||||
|
||||
var walker = try bin_dir.walk(allocator);
|
||||
defer walker.deinit();
|
||||
|
||||
while (try walker.next()) |entry| {
|
||||
if (entry.kind == .file and std.mem.eql(u8, entry.path, full_name)) {
|
||||
if (options.relative) {
|
||||
return try std.fs.path.join(allocator, &[_][]const u8{ "zig-out", "bin", entry.path });
|
||||
} else {
|
||||
return try bin_dir.realpathAlloc(allocator, entry.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -33,14 +33,17 @@ pub fn init(request: *jetzig.http.Request) !*Self {
|
||||
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
|
||||
/// request, including any other middleware in the chain.
|
||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
request.server.logger.debug("[DemoMiddleware:afterRequest] my_custom_value: {s}", .{self.my_custom_value});
|
||||
try request.server.logger.DEBUG(
|
||||
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
|
||||
.{self.my_custom_value},
|
||||
);
|
||||
self.my_custom_value = @tagName(request.method);
|
||||
}
|
||||
|
||||
/// Invoked immediately before the response renders to the client.
|
||||
/// The response can be modified here if needed.
|
||||
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
request.server.logger.debug(
|
||||
try request.server.logger.DEBUG(
|
||||
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
|
||||
.{ self.my_custom_value, @tagName(response.status_code) },
|
||||
);
|
||||
@ -51,7 +54,7 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet
|
||||
pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
_ = self;
|
||||
_ = response;
|
||||
request.server.logger.debug("[DemoMiddleware:afterResponse] response completed", .{});
|
||||
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
|
||||
}
|
||||
|
||||
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
|
||||
|
@ -3,13 +3,33 @@ const std = @import("std");
|
||||
pub const jetzig = @import("jetzig");
|
||||
pub const routes = @import("routes").routes;
|
||||
|
||||
// Override default settings in `jetzig.config` here:
|
||||
pub const jetzig_options = struct {
|
||||
/// Middleware chain. Add any custom middleware here, or use middleware provided in
|
||||
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
|
||||
pub const middleware: []const type = &.{
|
||||
// htmx middleware skips layouts when `HX-Target` header is present and issues
|
||||
// `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called.
|
||||
jetzig.middleware.HtmxMiddleware,
|
||||
// Demo middleware included with new projects. Remove once you are familiar with Jetzig's
|
||||
// middleware system.
|
||||
@import("app/middleware/DemoMiddleware.zig"),
|
||||
};
|
||||
|
||||
// Maximum bytes to allow in request body.
|
||||
// pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
|
||||
|
||||
// Maximum filesize for `public/` content.
|
||||
// pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
|
||||
|
||||
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
|
||||
// pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
|
||||
|
||||
// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
||||
// pub const public_content_path = "public";
|
||||
|
||||
// HTTP buffer. Must be large enough to store all headers. This should typically not be modified.
|
||||
// pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
|
109
src/jetzig.zig
109
src/jetzig.zig
@ -5,16 +5,19 @@ pub const zmpl = @import("zmpl").zmpl;
|
||||
pub const http = @import("jetzig/http.zig");
|
||||
pub const loggers = @import("jetzig/loggers.zig");
|
||||
pub const data = @import("jetzig/data.zig");
|
||||
pub const caches = @import("jetzig/caches.zig");
|
||||
pub const views = @import("jetzig/views.zig");
|
||||
pub const colors = @import("jetzig/colors.zig");
|
||||
pub const middleware = @import("jetzig/middleware.zig");
|
||||
pub const util = @import("jetzig/util.zig");
|
||||
pub const types = @import("jetzig/types.zig");
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
pub const App = @import("jetzig/App.zig");
|
||||
|
||||
/// Configuration options for the application server with command-line argument parsing.
|
||||
pub const Environment = @import("jetzig/Environment.zig");
|
||||
|
||||
/// An HTTP request which is passed to (dynamic) view functions and provides access to params,
|
||||
/// headers, and functions to render a response.
|
||||
pub const Request = http.Request;
|
||||
@ -32,47 +35,60 @@ pub const Data = data.Data;
|
||||
/// generate a `View`.
|
||||
pub const View = views.View;
|
||||
|
||||
const root = @import("root");
|
||||
|
||||
/// Global configuration. Override these values by defining in `src/main.zig` with:
|
||||
/// ```zig
|
||||
/// pub const jetzig_options = struct {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
/// All constants defined below can be overridden.
|
||||
pub const config = struct {
|
||||
/// Maximum bytes to allow in request body.
|
||||
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
|
||||
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 16);
|
||||
|
||||
/// Maximum filesize for `public/` content.
|
||||
pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
|
||||
|
||||
/// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
|
||||
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
|
||||
|
||||
/// Path relative to cwd() to serve public content from. Symlinks are not followed.
|
||||
pub const public_content_path = "public";
|
||||
|
||||
/// Middleware chain. Add any custom middleware here, or use middleware provided in
|
||||
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
|
||||
pub const middleware = &.{};
|
||||
|
||||
/// HTTP buffer. Must be large enough to store all headers. This should typically not be
|
||||
/// modified.
|
||||
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
|
||||
pub const public_content = .{ .path = "public" };
|
||||
|
||||
/// Reconciles a configuration value from user-defined values and defaults provided by Jetzig.
|
||||
pub fn get(T: type, comptime key: []const u8) T {
|
||||
const self = @This();
|
||||
if (!@hasDecl(self, key)) @panic("Unknown config option: " ++ key);
|
||||
|
||||
if (@hasDecl(root, "jetzig_options") and @hasDecl(root.jetzig_options, key)) {
|
||||
return @field(root.jetzig_options, key);
|
||||
} else {
|
||||
return @field(self, key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a new Jetzig app. Call this from `src/main.zig` and then call
|
||||
/// `start(@import("routes").routes)` on the returned value.
|
||||
pub fn init(allocator: std.mem.Allocator) !App {
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
const host: []const u8 = if (args.len > 1)
|
||||
try allocator.dupe(u8, args[1])
|
||||
else
|
||||
try allocator.dupe(u8, "127.0.0.1");
|
||||
|
||||
// TODO: Fix this up with proper arg parsing
|
||||
const port: u16 = if (args.len > 2) try std.fmt.parseInt(u16, args[2], 10) else 8080;
|
||||
const use_cache: bool = args.len > 3 and std.mem.eql(u8, args[3], "--cache");
|
||||
const server_cache = switch (use_cache) {
|
||||
true => caches.Cache{ .memory_cache = caches.MemoryCache.init(allocator) },
|
||||
false => caches.Cache{ .null_cache = caches.NullCache.init(allocator) },
|
||||
};
|
||||
var logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator) };
|
||||
const secret = try generateSecret(allocator);
|
||||
logger.debug(
|
||||
"Running in development mode, using auto-generated cookie encryption key:\n {s}",
|
||||
.{secret},
|
||||
);
|
||||
|
||||
const server_options = http.Server.ServerOptions{
|
||||
.cache = server_cache,
|
||||
.logger = logger,
|
||||
.secret = secret,
|
||||
};
|
||||
const environment = Environment.init(allocator);
|
||||
|
||||
return .{
|
||||
.server_options = server_options,
|
||||
.server_options = try environment.getServerOptions(),
|
||||
.allocator = allocator,
|
||||
.host = host,
|
||||
.port = port,
|
||||
};
|
||||
}
|
||||
|
||||
@ -154,36 +170,3 @@ pub fn route(comptime routes: anytype) []views.Route {
|
||||
|
||||
return &detected;
|
||||
}
|
||||
|
||||
pub fn generateSecret(allocator: std.mem.Allocator) ![]const u8 {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
var secret: [64]u8 = undefined;
|
||||
|
||||
for (0..64) |index| {
|
||||
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
|
||||
}
|
||||
|
||||
return try allocator.dupe(u8, &secret);
|
||||
}
|
||||
|
||||
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
|
||||
const encoder = std.base64.Base64Encoder.init(
|
||||
std.base64.url_safe_no_pad.alphabet_chars,
|
||||
std.base64.url_safe_no_pad.pad_char,
|
||||
);
|
||||
const size = encoder.calcSize(string.len);
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
_ = encoder.encode(ptr, string);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
|
||||
const decoder = std.base64.Base64Decoder.init(
|
||||
std.base64.url_safe_no_pad.alphabet_chars,
|
||||
std.base64.url_safe_no_pad.pad_char,
|
||||
);
|
||||
const size = try decoder.calcSizeForSlice(string);
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
try decoder.decode(ptr, string);
|
||||
return ptr;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const args = @import("args");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
const mime_types = @import("mime_types").mime_types; // Generated at build time.
|
||||
|
||||
@ -7,8 +9,6 @@ const Self = @This();
|
||||
|
||||
server_options: jetzig.http.Server.ServerOptions,
|
||||
allocator: std.mem.Allocator,
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
|
||||
pub fn deinit(self: Self) void {
|
||||
_ = self;
|
||||
@ -48,30 +48,40 @@ pub fn start(self: Self, comptime_routes: []jetzig.views.Route) !void {
|
||||
self.allocator.destroy(route);
|
||||
};
|
||||
|
||||
if (self.server_options.detach) {
|
||||
const argv = try std.process.argsAlloc(self.allocator);
|
||||
defer std.process.argsFree(self.allocator, argv);
|
||||
var child_argv = std.ArrayList([]const u8).init(self.allocator);
|
||||
for (argv) |arg| {
|
||||
if (!std.mem.eql(u8, "-d", arg) and !std.mem.eql(u8, "--detach", arg)) {
|
||||
try child_argv.append(arg);
|
||||
}
|
||||
}
|
||||
var child = std.process.Child.init(child_argv.items, self.allocator);
|
||||
try child.spawn();
|
||||
std.debug.print("Spawned child process. PID: {}. Exiting.\n", .{child.id});
|
||||
std.process.exit(0);
|
||||
}
|
||||
|
||||
var server = jetzig.http.Server.init(
|
||||
self.allocator,
|
||||
self.host,
|
||||
self.port,
|
||||
self.server_options,
|
||||
routes.items,
|
||||
&mime_map,
|
||||
);
|
||||
|
||||
defer server.deinit();
|
||||
defer self.allocator.free(self.host);
|
||||
defer self.allocator.free(server.options.secret);
|
||||
|
||||
server.listen() catch |err| {
|
||||
switch (err) {
|
||||
error.AddressInUse => {
|
||||
server.logger.debug(
|
||||
try server.logger.ERROR(
|
||||
"Socket unavailable: {s}:{} - unable to start server.\n",
|
||||
.{ self.host, self.port },
|
||||
.{ self.server_options.bind, self.server_options.port },
|
||||
);
|
||||
return;
|
||||
},
|
||||
else => {
|
||||
server.logger.debug("Encountered error: {}\nExiting.\n", .{err});
|
||||
try server.logger.ERROR("Encountered error: {}\nExiting.\n", .{err});
|
||||
return err;
|
||||
},
|
||||
}
|
||||
|
154
src/jetzig/Environment.zig
Normal file
154
src/jetzig/Environment.zig
Normal file
@ -0,0 +1,154 @@
|
||||
const std = @import("std");
|
||||
|
||||
const args = @import("args");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
const Environment = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Options = struct {
|
||||
help: bool = false,
|
||||
bind: []const u8 = "127.0.0.1",
|
||||
port: u16 = 8080,
|
||||
// TODO:
|
||||
// environment: []const u8 = "development",
|
||||
log: []const u8 = "-",
|
||||
@"log-error": []const u8 = "-",
|
||||
@"log-level": jetzig.loggers.LogLevel = .INFO,
|
||||
@"log-format": jetzig.loggers.LogFormat = .development,
|
||||
detach: bool = false,
|
||||
|
||||
pub const shorthands = .{
|
||||
.h = "help",
|
||||
.b = "bind",
|
||||
.p = "port",
|
||||
// TODO:
|
||||
// .e = "environment",
|
||||
.d = "detach",
|
||||
};
|
||||
|
||||
pub const wrap_len = 80;
|
||||
|
||||
pub const meta = .{
|
||||
.option_docs = .{
|
||||
.bind = "IP address/hostname to bind to (default: 127.0.0.1)",
|
||||
.port = "Port to listen on (default: 8080)",
|
||||
// TODO:
|
||||
// .environment = "Load an environment configuration from src/app/environments/<environment>.zig",
|
||||
.log = "Path to log file. Use '-' for stdout (default: '-')",
|
||||
.@"log-error" =
|
||||
\\Optional path to separate error log file. Use '-' for stderr. If omitted, errors are logged to the location specified by the `log` option (or stderr if `log` is '-').
|
||||
,
|
||||
.@"log-level" =
|
||||
\\Minimum log level. Log events below the given level are ignored. Must be one of: { TRACE, DEBUG, INFO, WARN, ERROR, FATAL } (default: DEBUG)
|
||||
,
|
||||
.@"log-format" =
|
||||
\\Output logs in the given format. Must be one of: { development, json } (default: development)
|
||||
,
|
||||
.detach =
|
||||
\\Run the server in the background. Must be used in conjunction with --log (default: false)
|
||||
,
|
||||
.help = "Print help and exit",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Environment {
|
||||
return .{ .allocator = allocator };
|
||||
}
|
||||
|
||||
/// Generate server initialization options using command line args with defaults.
|
||||
pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
||||
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
|
||||
|
||||
if (options.options.help) {
|
||||
const writer = std.io.getStdErr().writer();
|
||||
try args.printHelp(Options, options.executable_name orelse "<app-name>", writer);
|
||||
std.process.exit(0);
|
||||
}
|
||||
|
||||
var logger = switch (options.options.@"log-format") {
|
||||
.development => jetzig.loggers.Logger{
|
||||
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
||||
self.allocator,
|
||||
options.options.@"log-level",
|
||||
try getLogFile(.stdout, options.options),
|
||||
try getLogFile(.stderr, options.options),
|
||||
),
|
||||
},
|
||||
.json => jetzig.loggers.Logger{
|
||||
.json_logger = jetzig.loggers.JsonLogger.init(
|
||||
self.allocator,
|
||||
options.options.@"log-level",
|
||||
try getLogFile(.stdout, options.options),
|
||||
try getLogFile(.stderr, options.options),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) {
|
||||
try logger.ERROR("Must pass `--log` when using `--detach`.", .{});
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
// TODO: Generate nonce per session - do research to confirm correct best practice.
|
||||
const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length;
|
||||
const secret = try self.getSecret(&logger, secret_len);
|
||||
|
||||
if (secret.len != secret_len) {
|
||||
try logger.ERROR("Expected secret length: {}, found: {}.", .{ secret_len, secret.len });
|
||||
try logger.ERROR("Use `jetzig generate secret` to create a secure secret value.", .{});
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
return .{
|
||||
.logger = logger,
|
||||
.secret = secret,
|
||||
.bind = try self.allocator.dupe(u8, options.options.bind),
|
||||
.port = options.options.port,
|
||||
.detach = options.options.detach,
|
||||
};
|
||||
}
|
||||
|
||||
fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File {
|
||||
const path = switch (stream) {
|
||||
.stdout => options.log,
|
||||
.stderr => options.@"log-error",
|
||||
};
|
||||
|
||||
if (std.mem.eql(u8, path, "-")) return switch (stream) {
|
||||
.stdout => std.io.getStdOut(),
|
||||
.stderr => if (std.mem.eql(u8, options.log, "-"))
|
||||
std.io.getStdErr()
|
||||
else
|
||||
try std.fs.createFileAbsolute(options.log, .{ .truncate = false }),
|
||||
};
|
||||
|
||||
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
|
||||
try file.seekFromEnd(0);
|
||||
return file;
|
||||
}
|
||||
|
||||
fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u10) ![]const u8 {
|
||||
return std.process.getEnvVarOwned(self.allocator, "JETZIG_SECRET") catch |err| {
|
||||
switch (err) {
|
||||
error.EnvironmentVariableNotFound => {
|
||||
// TODO: Make this a failure when running in non-development mode.
|
||||
const secret = try jetzig.util.generateSecret(self.allocator, len);
|
||||
try logger.WARN(
|
||||
"Running in development mode, using auto-generated cookie encryption key: {s}",
|
||||
.{secret},
|
||||
);
|
||||
try logger.WARN(
|
||||
"Run `jetzig generate secret` and set `JETZIG_SECRET` to remove this warning.",
|
||||
.{},
|
||||
);
|
||||
|
||||
return secret;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const http = @import("http.zig");
|
||||
|
||||
pub const Result = @import("caches/Result.zig");
|
||||
pub const MemoryCache = @import("caches/MemoryCache.zig");
|
||||
pub const NullCache = @import("caches/NullCache.zig");
|
||||
|
||||
pub const Cache = union(enum) {
|
||||
memory_cache: MemoryCache,
|
||||
null_cache: NullCache,
|
||||
|
||||
pub fn deinit(self: *Cache) void {
|
||||
switch (self.*) {
|
||||
inline else => |*case| case.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *Cache, key: []const u8) ?Result {
|
||||
return switch (self.*) {
|
||||
inline else => |*case| case.get(key),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn put(self: *Cache, key: []const u8, value: http.Response) !Result {
|
||||
return switch (self.*) {
|
||||
inline else => |*case| case.put(key, value),
|
||||
};
|
||||
}
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Result = @import("Result.zig");
|
||||
pub const MemoryCache = @import("MemoryCache.zig");
|
||||
pub const NullCache = @import("NullCache.zig");
|
@ -1,39 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const http = @import("../http.zig");
|
||||
const Result = @import("Result.zig");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
cache: std.StringHashMap(http.Response),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
const cache = std.StringHashMap(http.Response).init(allocator);
|
||||
|
||||
return .{ .allocator = allocator, .cache = cache };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
var iterator = self.cache.keyIterator();
|
||||
while (iterator.next()) |key| {
|
||||
self.allocator.free(key.*);
|
||||
}
|
||||
self.cache.deinit();
|
||||
}
|
||||
|
||||
pub fn get(self: *Self, key: []const u8) ?Result {
|
||||
if (self.cache.get(key)) |value| {
|
||||
return Result.init(self.allocator, value, true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put(self: *Self, key: []const u8, value: http.Response) !Result {
|
||||
const key_dupe = try self.allocator.dupe(u8, key);
|
||||
const value_dupe = try value.dupe();
|
||||
try self.cache.put(key_dupe, value_dupe);
|
||||
|
||||
return Result.init(self.allocator, value_dupe, true);
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const http = @import("../http.zig");
|
||||
const Result = @import("Result.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return Self{ .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn get(self: *const Self, key: []const u8) ?Result {
|
||||
_ = key;
|
||||
_ = self;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn put(self: *const Self, key: []const u8, value: http.Response) !Result {
|
||||
_ = key;
|
||||
return Result{ .value = value, .cached = false, .allocator = self.allocator };
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
value: jetzig.http.Response,
|
||||
cached: bool,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, value: jetzig.http.Response, cached: bool) Self {
|
||||
return .{ .allocator = allocator, .cached = cached, .value = value };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Self) void {
|
||||
if (!self.cached) self.value.deinit();
|
||||
}
|
@ -30,10 +30,12 @@ rendered: bool = false,
|
||||
redirected: bool = false,
|
||||
rendered_multiple: bool = false,
|
||||
rendered_view: ?jetzig.views.View = null,
|
||||
start_time: i128,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
server: *jetzig.http.Server,
|
||||
start_time: i128,
|
||||
std_http_request: std.http.Server.Request,
|
||||
response: *jetzig.http.Response,
|
||||
) !Self {
|
||||
@ -69,6 +71,7 @@ pub fn init(
|
||||
.query_data = query_data,
|
||||
.query = query,
|
||||
.std_http_request = std_http_request,
|
||||
.start_time = start_time,
|
||||
};
|
||||
}
|
||||
|
||||
@ -101,7 +104,7 @@ pub fn process(self: *Self) !void {
|
||||
self.session.parse() catch |err| {
|
||||
switch (err) {
|
||||
error.JetzigInvalidSessionCookie => {
|
||||
self.server.logger.debug("Invalid session cookie detected. Resetting session.", .{});
|
||||
try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
|
||||
try self.session.reset();
|
||||
},
|
||||
else => return err,
|
||||
@ -109,7 +112,7 @@ pub fn process(self: *Self) !void {
|
||||
};
|
||||
|
||||
const reader = try self.std_http_request.reader();
|
||||
self.body = try reader.readAllAlloc(self.allocator, jetzig.config.max_bytes_request_body);
|
||||
self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body"));
|
||||
self.processed = true;
|
||||
}
|
||||
|
||||
@ -310,7 +313,9 @@ pub fn hash(self: *Self) ![]const u8 {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn fmtMethod(self: *Self) []const u8 {
|
||||
pub fn fmtMethod(self: *const Self, colorized: bool) []const u8 {
|
||||
if (!colorized) return @tagName(self.method);
|
||||
|
||||
return switch (self.method) {
|
||||
.GET => jetzig.colors.cyan("GET"),
|
||||
.PUT => jetzig.colors.yellow("PUT"),
|
||||
|
@ -3,45 +3,32 @@ const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
const zmpl = @import("zmpl");
|
||||
|
||||
const root_file = @import("root");
|
||||
|
||||
pub const jetzig_server_options = if (@hasDecl(root_file, "jetzig_options"))
|
||||
root_file.jetzig_options
|
||||
else
|
||||
struct {};
|
||||
|
||||
pub const ServerOptions = struct {
|
||||
cache: jetzig.caches.Cache,
|
||||
logger: jetzig.loggers.Logger,
|
||||
bind: []const u8,
|
||||
port: u16,
|
||||
secret: []const u8,
|
||||
detach: bool,
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
port: u16,
|
||||
host: []const u8,
|
||||
cache: jetzig.caches.Cache,
|
||||
logger: jetzig.loggers.Logger,
|
||||
options: ServerOptions,
|
||||
start_time: i128 = undefined,
|
||||
routes: []*jetzig.views.Route,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
std_net_server: std.net.Server = undefined,
|
||||
initialized: bool = false,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
host: []const u8,
|
||||
port: u16,
|
||||
options: ServerOptions,
|
||||
routes: []*jetzig.views.Route,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.host = host,
|
||||
.port = port,
|
||||
.cache = options.cache,
|
||||
.logger = options.logger,
|
||||
.options = options,
|
||||
.routes = routes,
|
||||
@ -50,15 +37,18 @@ pub fn init(
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.std_net_server.deinit();
|
||||
if (self.initialized) self.std_net_server.deinit();
|
||||
self.allocator.free(self.options.secret);
|
||||
self.allocator.free(self.options.bind);
|
||||
}
|
||||
|
||||
pub fn listen(self: *Self) !void {
|
||||
const address = try std.net.Address.parseIp("127.0.0.1", 8080);
|
||||
const address = try std.net.Address.parseIp(self.options.bind, self.options.port);
|
||||
self.std_net_server = try address.listen(.{ .reuse_port = true });
|
||||
|
||||
const cache_status = if (self.options.cache == .null_cache) "disabled" else "enabled";
|
||||
self.logger.debug("Listening on http://{s}:{} [cache:{s}]", .{ self.host, self.port, cache_status });
|
||||
self.initialized = true;
|
||||
|
||||
try self.logger.INFO("Listening on http://{s}:{}", .{ self.options.bind, self.options.port });
|
||||
try self.processRequests();
|
||||
}
|
||||
|
||||
@ -71,13 +61,12 @@ fn processRequests(self: *Self) !void {
|
||||
|
||||
const connection = try self.std_net_server.accept();
|
||||
|
||||
var buf: [jetzig.config.http_buffer_size]u8 = undefined;
|
||||
var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined;
|
||||
var std_http_server = std.http.Server.init(connection, &buf);
|
||||
errdefer std_http_server.connection.stream.close();
|
||||
|
||||
self.processNextRequest(allocator, &std_http_server) catch |err| {
|
||||
if (isBadHttpError(err)) {
|
||||
std.debug.print("Encountered HTTP error: {s}\n", .{@errorName(err)});
|
||||
std_http_server.connection.stream.close();
|
||||
continue;
|
||||
} else return err;
|
||||
@ -89,13 +78,13 @@ fn processRequests(self: *Self) !void {
|
||||
}
|
||||
|
||||
fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void {
|
||||
self.start_time = std.time.nanoTimestamp();
|
||||
const start_time = std.time.nanoTimestamp();
|
||||
|
||||
const std_http_request = try std_http_server.receiveHead();
|
||||
if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError;
|
||||
|
||||
var response = try jetzig.http.Response.init(allocator);
|
||||
var request = try jetzig.http.Request.init(allocator, self, std_http_request, &response);
|
||||
var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response);
|
||||
|
||||
try request.process();
|
||||
|
||||
@ -111,9 +100,7 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server
|
||||
try jetzig.http.middleware.afterResponse(&middleware_data, &request);
|
||||
jetzig.http.middleware.deinit(&middleware_data, &request);
|
||||
|
||||
const log_message = try self.requestLogMessage(&request);
|
||||
defer self.allocator.free(log_message);
|
||||
self.logger.debug("{s}", .{log_message});
|
||||
try self.logger.logRequest(&request);
|
||||
}
|
||||
|
||||
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
|
||||
@ -210,7 +197,7 @@ fn renderView(
|
||||
// `return request.render(.ok)`, but the actual rendered view is stored in
|
||||
// `request.rendered_view`.
|
||||
_ = route.render(route.*, request) catch |err| {
|
||||
self.logger.debug("Encountered error: {s}", .{@errorName(err)});
|
||||
try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
|
||||
if (isUnhandledError(err)) return err;
|
||||
if (isBadRequest(err)) return try self.renderBadRequest(request);
|
||||
return try self.renderInternalServerError(request, err);
|
||||
@ -230,7 +217,7 @@ fn renderView(
|
||||
return .{ .view = rendered_view, .content = "" };
|
||||
}
|
||||
} else {
|
||||
self.logger.debug("`request.render` was not invoked. Rendering empty content.", .{});
|
||||
try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
|
||||
request.response_data.reset();
|
||||
return .{
|
||||
.view = .{ .data = request.response_data, .status_code = .no_content },
|
||||
@ -254,7 +241,7 @@ fn renderTemplateWithLayout(
|
||||
if (zmpl.manifest.find(prefixed_name)) |layout| {
|
||||
return try template.renderWithLayout(layout, view.data);
|
||||
} else {
|
||||
self.logger.debug("Unknown layout: {s}", .{layout_name});
|
||||
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
|
||||
return try template.render(view.data);
|
||||
}
|
||||
} else return try template.render(view.data);
|
||||
@ -299,7 +286,7 @@ fn renderInternalServerError(self: *Self, request: *jetzig.http.Request, err: an
|
||||
try object.put("error", request.response_data.string(@errorName(err)));
|
||||
|
||||
const stack = @errorReturnTrace();
|
||||
if (stack) |capture| try self.logStackTrace(capture, request, object);
|
||||
if (stack) |capture| try self.logStackTrace(capture, request);
|
||||
|
||||
return .{
|
||||
.view = jetzig.views.View{ .data = request.response_data, .status_code = .internal_server_error },
|
||||
@ -324,40 +311,13 @@ fn logStackTrace(
|
||||
self: *Self,
|
||||
stack: *std.builtin.StackTrace,
|
||||
request: *jetzig.http.Request,
|
||||
object: *jetzig.data.Value,
|
||||
) !void {
|
||||
_ = self;
|
||||
std.debug.print("\nStack Trace:\n{}", .{stack});
|
||||
var array = std.ArrayList(u8).init(request.allocator);
|
||||
const writer = array.writer();
|
||||
try self.logger.ERROR("\nStack Trace:\n{}", .{stack});
|
||||
var buf = std.ArrayList(u8).init(request.allocator);
|
||||
defer buf.deinit();
|
||||
const writer = buf.writer();
|
||||
try stack.format("", .{}, writer);
|
||||
// TODO: Generate an array of objects with stack trace in useful data structure instead of
|
||||
// dumping the whole formatted backtrace as a JSON string:
|
||||
try object.put("backtrace", request.response_data.string(array.items));
|
||||
}
|
||||
|
||||
fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
|
||||
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
|
||||
inline else => |status_code| @unionInit(
|
||||
jetzig.http.status_codes.TaggedStatusCode,
|
||||
@tagName(status_code),
|
||||
.{},
|
||||
),
|
||||
};
|
||||
|
||||
const formatted_duration = try jetzig.colors.duration(self.allocator, self.duration());
|
||||
defer self.allocator.free(formatted_duration);
|
||||
|
||||
return try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
|
||||
formatted_duration,
|
||||
request.fmtMethod(),
|
||||
status.format(),
|
||||
request.path.path,
|
||||
});
|
||||
}
|
||||
|
||||
fn duration(self: *Self) i64 {
|
||||
return @intCast(std.time.nanoTimestamp() - self.start_time);
|
||||
try self.logger.ERROR("{s}\n", .{buf.items});
|
||||
}
|
||||
|
||||
fn matchRoute(self: *Self, request: *jetzig.http.Request, static: bool) !?*jetzig.views.Route {
|
||||
@ -398,7 +358,7 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResour
|
||||
if (request.method != .GET) return null;
|
||||
|
||||
var iterable_dir = std.fs.cwd().openDir(
|
||||
jetzig.config.public_content.path,
|
||||
jetzig.config.get([]const u8, "public_content_path"),
|
||||
.{ .iterate = true, .no_follow = true },
|
||||
) catch |err| {
|
||||
switch (err) {
|
||||
@ -418,7 +378,7 @@ fn matchPublicContent(self: *Self, request: *jetzig.http.Request) !?StaticResour
|
||||
const content = try iterable_dir.readFileAlloc(
|
||||
request.allocator,
|
||||
file.path,
|
||||
jetzig.config.max_bytes_static_content,
|
||||
jetzig.config.get(usize, "max_bytes_public_content"),
|
||||
);
|
||||
const extension = std.fs.path.extension(file.path);
|
||||
const mime_type = if (self.mime_map.get(extension)) |mime| mime else "application/octet-stream";
|
||||
@ -450,7 +410,7 @@ fn matchStaticContent(self: *Self, request: *jetzig.http.Request) !?[]const u8 {
|
||||
return static_dir.readFileAlloc(
|
||||
request.allocator,
|
||||
capture,
|
||||
jetzig.config.max_bytes_static_content,
|
||||
jetzig.config.get(usize, "max_bytes_static_content"),
|
||||
) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
|
@ -3,7 +3,7 @@ const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
pub const cookie_name = "_jetzig-session";
|
||||
const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
|
||||
pub const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
encryption_key: ?[]const u8,
|
||||
@ -99,7 +99,7 @@ fn save(self: *Self) !void {
|
||||
}
|
||||
self.encrypted = try self.encrypt(json);
|
||||
|
||||
const encoded = try jetzig.base64Encode(self.allocator, self.encrypted.?);
|
||||
const encoded = try jetzig.util.base64Encode(self.allocator, self.encrypted.?);
|
||||
defer self.allocator.free(encoded);
|
||||
|
||||
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
|
||||
@ -113,7 +113,7 @@ fn save(self: *Self) !void {
|
||||
|
||||
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
|
||||
self.data = jetzig.data.Data.init(self.allocator);
|
||||
const decoded = try jetzig.base64Decode(self.allocator, cookie_value);
|
||||
const decoded = try jetzig.util.base64Decode(self.allocator, cookie_value);
|
||||
defer self.allocator.free(decoded);
|
||||
|
||||
const buf = self.decrypt(decoded) catch |err| {
|
||||
|
@ -1,12 +1,7 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const server_options = jetzig.http.Server.jetzig_server_options;
|
||||
|
||||
const middlewares: []const type = if (@hasDecl(server_options, "middleware"))
|
||||
server_options.middleware
|
||||
else
|
||||
&.{};
|
||||
const middlewares: []const type = jetzig.config.get([]const type, "middleware");
|
||||
|
||||
const MiddlewareData = std.BoundedArray(*anyopaque, middlewares.len);
|
||||
|
||||
|
@ -73,11 +73,12 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn format(self: Self) []const u8 {
|
||||
pub fn format(self: Self, colorized: bool) []const u8 {
|
||||
_ = self;
|
||||
|
||||
const full_message = code ++ " " ++ message;
|
||||
|
||||
if (!colorized) return full_message;
|
||||
|
||||
if (std.mem.startsWith(u8, code, "2")) {
|
||||
return jetzig.colors.green(full_message);
|
||||
} else if (std.mem.startsWith(u8, code, "3")) {
|
||||
@ -158,9 +159,15 @@ pub const TaggedStatusCode = union(StatusCode) {
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn format(self: Self) []const u8 {
|
||||
pub fn format(self: Self, colorized: bool) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |capture| capture.format(),
|
||||
inline else => |capture| capture.format(colorized),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getCode(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |capture| capture.code,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -1,21 +1,64 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig");
|
||||
pub const JsonLogger = @import("loggers/JsonLogger.zig");
|
||||
|
||||
const LogLevel = enum {
|
||||
debug,
|
||||
};
|
||||
pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
|
||||
pub const LogFormat = enum { development, json };
|
||||
|
||||
pub const Logger = union(enum) {
|
||||
development_logger: DevelopmentLogger,
|
||||
json_logger: JsonLogger,
|
||||
|
||||
pub fn debug(self: *Logger, comptime message: []const u8, args: anytype) void {
|
||||
/// Log a TRACE level message to the configured logger.
|
||||
pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*case| case.debug(message, args) catch |err| {
|
||||
std.debug.print("{}\n", .{err});
|
||||
},
|
||||
inline else => |*logger| try logger.log(.TRACE, message, args),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a DEBUG level message to the configured logger.
|
||||
pub fn DEBUG(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*logger| try logger.log(.DEBUG, message, args),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an INFO level message to the configured logger.
|
||||
pub fn INFO(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*logger| try logger.log(.INFO, message, args),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a WARN level message to the configured logger.
|
||||
pub fn WARN(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*logger| try logger.log(.WARN, message, args),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log an ERROR level message to the configured logger.
|
||||
pub fn ERROR(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*logger| try logger.log(.ERROR, message, args),
|
||||
}
|
||||
}
|
||||
|
||||
/// Log a FATAL level message to the configured logger.
|
||||
pub fn FATAL(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*logger| try logger.log(.FATAL, message, args),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn logRequest(self: *const Logger, request: *const jetzig.http.Request) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*logger| try logger.logRequest(request),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,19 +1,105 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Self = @This();
|
||||
const Timestamp = @import("../types/Timestamp.zig");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const DevelopmentLogger = @This();
|
||||
|
||||
const Timestamp = jetzig.types.Timestamp;
|
||||
const LogLevel = jetzig.loggers.LogLevel;
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
stdout_colorized: bool,
|
||||
stderr_colorized: bool,
|
||||
level: LogLevel,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return .{ .allocator = allocator };
|
||||
/// Initialize a new Development Logger.
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
level: LogLevel,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
) DevelopmentLogger {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.level = level,
|
||||
.stdout = stdout,
|
||||
.stderr = stderr,
|
||||
.stdout_colorized = stdout.isTty(),
|
||||
.stderr_colorized = stderr.isTty(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn debug(self: *Self, comptime message: []const u8, args: anytype) !void {
|
||||
/// Generic log function, receives log level, message (format string), and args for format string.
|
||||
pub fn log(
|
||||
self: DevelopmentLogger,
|
||||
comptime level: LogLevel,
|
||||
comptime message: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
if (@intFromEnum(level) < @intFromEnum(self.level)) return;
|
||||
|
||||
const output = try std.fmt.allocPrint(self.allocator, message, args);
|
||||
defer self.allocator.free(output);
|
||||
|
||||
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
|
||||
const iso8601 = try timestamp.iso8601();
|
||||
defer self.allocator.free(iso8601);
|
||||
std.debug.print("[{s}] {s}\n", .{ iso8601, output });
|
||||
|
||||
const colorized = switch (level) {
|
||||
.TRACE, .DEBUG, .INFO => self.stdout_colorized,
|
||||
.WARN, .ERROR, .FATAL => self.stderr_colorized,
|
||||
};
|
||||
const file = switch (level) {
|
||||
.TRACE, .DEBUG, .INFO => self.stdout,
|
||||
.WARN, .ERROR, .FATAL => self.stderr,
|
||||
};
|
||||
const writer = file.writer();
|
||||
const level_formatted = if (colorized) colorizedLogLevel(level) else @tagName(level);
|
||||
|
||||
try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output });
|
||||
|
||||
if (!file.isTty()) try file.sync();
|
||||
}
|
||||
|
||||
/// Log a one-liner including response status code, path, method, duration, etc.
|
||||
pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) !void {
|
||||
const formatted_duration = if (self.stdout_colorized)
|
||||
try jetzig.colors.duration(self.allocator, jetzig.util.duration(request.start_time))
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{}",
|
||||
.{std.fmt.fmtDurationSigned(jetzig.util.duration(request.start_time))},
|
||||
);
|
||||
defer self.allocator.free(formatted_duration);
|
||||
|
||||
const status: jetzig.http.status_codes.TaggedStatusCode = switch (request.response.status_code) {
|
||||
inline else => |status_code| @unionInit(
|
||||
jetzig.http.status_codes.TaggedStatusCode,
|
||||
@tagName(status_code),
|
||||
.{},
|
||||
),
|
||||
};
|
||||
|
||||
const message = try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
|
||||
formatted_duration,
|
||||
request.fmtMethod(self.stdout_colorized),
|
||||
status.format(self.stdout_colorized),
|
||||
request.path.path,
|
||||
});
|
||||
defer self.allocator.free(message);
|
||||
try self.log(.INFO, "{s}", .{message});
|
||||
}
|
||||
|
||||
fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
|
||||
return switch (level) {
|
||||
.TRACE => jetzig.colors.white(@tagName(level)),
|
||||
.DEBUG => jetzig.colors.cyan(@tagName(level)),
|
||||
.INFO => jetzig.colors.blue(@tagName(level) ++ " "),
|
||||
.WARN => jetzig.colors.yellow(@tagName(level) ++ " "),
|
||||
.ERROR => jetzig.colors.red(@tagName(level)),
|
||||
.FATAL => jetzig.colors.red(@tagName(level)),
|
||||
};
|
||||
}
|
||||
|
114
src/jetzig/loggers/JsonLogger.zig
Normal file
114
src/jetzig/loggers/JsonLogger.zig
Normal file
@ -0,0 +1,114 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const JsonLogger = @This();
|
||||
|
||||
const Timestamp = jetzig.types.Timestamp;
|
||||
const LogLevel = jetzig.loggers.LogLevel;
|
||||
const LogMessage = struct {
|
||||
level: []const u8,
|
||||
timestamp: []const u8,
|
||||
message: []const u8,
|
||||
};
|
||||
const RequestLogMessage = struct {
|
||||
level: []const u8,
|
||||
timestamp: []const u8,
|
||||
method: []const u8,
|
||||
status: []const u8,
|
||||
path: []const u8,
|
||||
duration: i64,
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
level: LogLevel,
|
||||
|
||||
/// Initialize a new JSON Logger.
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
level: LogLevel,
|
||||
stdout: std.fs.File,
|
||||
stderr: std.fs.File,
|
||||
) JsonLogger {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.level = level,
|
||||
.stdout = stdout,
|
||||
.stderr = stderr,
|
||||
};
|
||||
}
|
||||
|
||||
/// Generic log function, receives log level, message (format string), and args for format string.
|
||||
pub fn log(
|
||||
self: JsonLogger,
|
||||
comptime level: LogLevel,
|
||||
comptime message: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
if (@intFromEnum(level) < @intFromEnum(self.level)) return;
|
||||
|
||||
const output = try std.fmt.allocPrint(self.allocator, message, args);
|
||||
defer self.allocator.free(output);
|
||||
|
||||
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
|
||||
const iso8601 = try timestamp.iso8601();
|
||||
defer self.allocator.free(iso8601);
|
||||
|
||||
const file = self.getFile(level);
|
||||
const writer = file.writer();
|
||||
const log_message = LogMessage{ .level = @tagName(level), .timestamp = iso8601, .message = output };
|
||||
|
||||
const json = try std.json.stringifyAlloc(self.allocator, log_message, .{ .whitespace = .minified });
|
||||
defer self.allocator.free(json);
|
||||
|
||||
try writer.writeAll(json);
|
||||
try writer.writeByte('\n');
|
||||
|
||||
if (!file.isTty()) try file.sync(); // Make configurable ?
|
||||
}
|
||||
|
||||
/// Log a one-liner including response status code, path, method, duration, etc.
|
||||
pub fn logRequest(self: JsonLogger, request: *const jetzig.http.Request) !void {
|
||||
const level: LogLevel = .INFO;
|
||||
|
||||
const duration = jetzig.util.duration(request.start_time);
|
||||
|
||||
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
|
||||
const iso8601 = try timestamp.iso8601();
|
||||
defer self.allocator.free(iso8601);
|
||||
|
||||
const status = switch (request.response.status_code) {
|
||||
inline else => |status_code| @unionInit(
|
||||
jetzig.http.status_codes.TaggedStatusCode,
|
||||
@tagName(status_code),
|
||||
.{},
|
||||
),
|
||||
};
|
||||
const message = RequestLogMessage{
|
||||
.level = @tagName(level),
|
||||
.timestamp = iso8601,
|
||||
.method = @tagName(request.method),
|
||||
.status = status.getCode(),
|
||||
.path = request.path.path,
|
||||
.duration = duration,
|
||||
};
|
||||
const json = try std.json.stringifyAlloc(self.allocator, message, .{ .whitespace = .minified });
|
||||
defer self.allocator.free(json);
|
||||
|
||||
const file = self.getFile(level);
|
||||
const writer = file.writer();
|
||||
|
||||
try writer.writeAll(json);
|
||||
try writer.writeByte('\n');
|
||||
|
||||
if (!file.isTty()) try file.sync(); // Make configurable ?
|
||||
}
|
||||
|
||||
fn getFile(self: JsonLogger, level: LogLevel) std.fs.File {
|
||||
return switch (level) {
|
||||
.TRACE, .DEBUG, .INFO => self.stdout,
|
||||
.WARN, .ERROR, .FATAL => self.stderr,
|
||||
};
|
||||
}
|
@ -16,7 +16,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
|
||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
_ = self;
|
||||
if (request.getHeader("HX-Target")) |target| {
|
||||
request.server.logger.debug(
|
||||
try request.server.logger.DEBUG(
|
||||
"[middleware-htmx] htmx request detected, disabling layout. (#{s})",
|
||||
.{target},
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Compare two strings with case-insensitive matching.
|
||||
pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool {
|
||||
if (expected.len != actual.len) return false;
|
||||
for (expected, actual) |expected_char, actual_char| {
|
||||
@ -7,3 +8,44 @@ pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) boo
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Encode arbitrary input to Base64.
|
||||
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
|
||||
const encoder = std.base64.Base64Encoder.init(
|
||||
std.base64.url_safe_no_pad.alphabet_chars,
|
||||
std.base64.url_safe_no_pad.pad_char,
|
||||
);
|
||||
const size = encoder.calcSize(string.len);
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
_ = encoder.encode(ptr, string);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// Decode arbitrary input from Base64.
|
||||
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
|
||||
const decoder = std.base64.Base64Decoder.init(
|
||||
std.base64.url_safe_no_pad.alphabet_chars,
|
||||
std.base64.url_safe_no_pad.pad_char,
|
||||
);
|
||||
const size = try decoder.calcSizeForSlice(string);
|
||||
const ptr = try allocator.alloc(u8, size);
|
||||
try decoder.decode(ptr, string);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/// Generate a secure random string of `len` characters (for cryptographic purposes).
|
||||
pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
var secret: [len]u8 = undefined;
|
||||
|
||||
for (0..len) |index| {
|
||||
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
|
||||
}
|
||||
|
||||
return try allocator.dupe(u8, &secret);
|
||||
}
|
||||
|
||||
/// Calculate a duration from a given start time (in nanoseconds) to the current time.
|
||||
pub fn duration(start_time: i128) i64 {
|
||||
return @intCast(std.time.nanoTimestamp() - start_time);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user