mod(zlog): pretty printing for user defined types
All checks were successful
Run Tests / test (push) Successful in 9m32s
Run Tests / lint (push) Successful in 9m43s

This commit is contained in:
2024-08-28 17:40:41 +02:00
parent ffb28f8e5b
commit 510b3ddfcf
3 changed files with 137 additions and 63 deletions

View File

@@ -1,3 +1,85 @@
# zlog # zlog
Standard Library log wrapper. Standard Library log wrapper. `zlog` provides adjusted `std.log` output and a pretty print function for easy overwriting of user defined types (`struct`, `enum`, `union` and `vector`).
## Usage
The following snippet shows an example usage of `zlog` to change the default log format and add pretty printing to both user defined types (`Options` and `Struct`):
```zig
const std = @import("std");
const zlog = @import("zlog");
// use this to overwrite the default log format of `std.log`
pub const std_options = zlog.std_options;
const Options = enum {
a,
b,
c,
// copy and paste this function into your user defined types to enable pretty printing for these types
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) anyerror!void {
try zlog.pretty_format(value, fmt, options, writer);
}
};
const Struct = struct {
a: usize = 42,
b: bool = true,
c: [5]u16 = .{ 1, 2, 3, 4, 5 },
d: []const u8 = "string",
e: Options = Options.b,
// same function as above...
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) anyerror!void {
try zlog.pretty_format(value, fmt, options, writer);
}
};
pub fn main() void {
// initialize zlog with the scope of `main`
const log = std.log.scoped(.main);
// NOTE: the scope of `default` will result in no scoping being printed in
// the resulting output (default behavior of `std.log.defaultLog`)
// some variables to log
const int_var = 42;
const array_var = [_]i32{ 1, 2, 3, 4 };
const string_var = "This is a test";
const option_var = Options.a;
const struct_var: Struct = .{};
// NOTE: Depending on the optimization target some of these log messages
// will not show, which is inline with the behavior of `std.log`.
log.debug("Debug message {any}", .{int_var});
log.info("Info message {any}", .{array_var});
log.info("Info message \"{s}\"", .{string_var});
log.warn("Warning message {any}", .{option_var});
log.err("Error message {any}", .{struct_var});
}
```
This will result in the following output:
```
debug(main): Debug message 42
info(main): Info message { 1, 2, 3, 4 }
info(main): Info message "This is a test"
warning(main): Warning message main.Options = enum {
a,
b,
c,
} = a
error(main): Error message main.Struct = struct {
.a = 42,
.b = true,
.c = []u16: { 1, 2, 3, 4, 5 },
.d = { 115, 116, 114, 105, 110, 103 },
.e = main.Options = enum {
a,
b,
c,
} = b,
}
```

View File

@@ -1,10 +1,17 @@
const std = @import("std"); const std = @import("std");
const zlog = @import("zlog"); const zlog = @import("zlog");
pub const std_options = zlog.std_options;
const Options = enum { const Options = enum {
a, a,
b, b,
c, c,
// copy and paste this function into your user defined types to enable pretty printing for these types
pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) anyerror!void {
try zlog.pretty_format(value, fmt, options, writer);
}
}; };
const Struct = struct { const Struct = struct {
@@ -14,44 +21,17 @@ const Struct = struct {
d: []const u8 = "string", d: []const u8 = "string",
e: Options = Options.b, e: Options = Options.b,
// pub fn format(value: Struct, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { // same function as above...
// // TODO: make the creation of this function comptime pub fn format(value: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) anyerror!void {
// try writer.writeAll("main.Struct{ "); try zlog.pretty_format(value, fmt, options, writer);
// try writer.writeAll(".a: usize = ");
// try std.fmt.formatIntValue(value.a, fmt, options, writer);
// try writer.writeAll(", ");
// try writer.writeAll(".b: bool = ");
// try std.fmt.formatBuf(if (value.b) "true" else "false", options, writer);
// try writer.writeAll(", ");
// try writer.writeAll(".c: [5]u16 = ");
// try std.fmt.format(writer, "{any}", .{value.c});
// try writer.writeAll(", ");
// try writer.writeAll(".d: []const u8 = ");
// try std.fmt.format(writer, "\"{s}\"", .{value.d});
// try writer.writeAll(", ");
// try writer.writeAll(".e: main.Options = ");
// try std.fmt.format(writer, "{any}", .{value.e});
// try writer.writeAll(" }");
// }
// TODO: can this become comptime entirely? - i.e. create the entire struct through a helper function and provide the created one with the format function already attached?
pub fn format(value: Struct, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) anyerror!void {
try zlog.generic_format(value, fmt, options, writer);
} }
}; };
pub const std_options = zlog.std_options;
pub fn main() void { pub fn main() void {
// initialize zlog with the scope of `main` // initialize zlog with the scope of `main`
const log = std.log.scoped(.main); const log = std.log.scoped(.main);
// NOTE: the scope of `default` will result in no scoping being printed in // NOTE: the scope of `default` will result in no scoping being printed in
// the resulting output // the resulting output (default behavior of `std.log.defaultLog`)
// some variables to log // some variables to log
const int_var = 42; const int_var = 42;

View File

@@ -1,7 +1,5 @@
const std = @import("std"); const std = @import("std");
const max_depth = 5;
// pub fn format(value: ?, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void // pub fn format(value: ?, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void
// TODO: provide comptime helper function to add format function for user types: // TODO: provide comptime helper function to add format function for user types:
// - pretty printing // - pretty printing
@@ -30,7 +28,7 @@ fn logFn(
// - should there be spacing? // - should there be spacing?
// - should the messages be aligned (left, center, right)? // - should the messages be aligned (left, center, right)?
const level_txt = comptime message_level.asText(); const level_txt = comptime message_level.asText();
const prefix2 = if (scope == .default) ":\t" else "(" ++ @tagName(scope) ++ "):\t"; const prefix = if (scope == .default) ":\t" else "(" ++ @tagName(scope) ++ "):\t";
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
var bw = std.io.bufferedWriter(stderr); var bw = std.io.bufferedWriter(stderr);
const writer = bw.writer(); const writer = bw.writer();
@@ -38,51 +36,65 @@ fn logFn(
std.debug.lockStdErr(); std.debug.lockStdErr();
defer std.debug.unlockStdErr(); defer std.debug.unlockStdErr();
nosuspend { nosuspend {
writer.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return; writer.print(level_txt ++ prefix ++ format ++ "\n", args) catch return;
bw.flush() catch return; bw.flush() catch return;
} }
} }
pub fn generic_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { pub fn pretty_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
_ = fmt; try inner_format(object, fmt, options, writer, 0);
_ = options; }
fn inner_format(object: anytype, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype, comptime depth: u8) !void {
const Object = @TypeOf(object); const Object = @TypeOf(object);
const object_info = @typeInfo(Object); const object_info = @typeInfo(Object);
switch (object_info) { switch (object_info) {
.Struct => |s| { .Struct => |s| {
try writer.writeAll(@typeName(Object)); try writer.writeAll(@typeName(Object));
try writer.writeAll(" = {\n"); try writer.writeAll(" = struct {\n");
inline for (s.fields) |field| { inline for (s.fields) |field| {
try writer.writeAll("\t ."); try writer.writeAll("\t" ** (depth + 1));
try writer.writeAll(".");
try writer.writeAll(field.name); try writer.writeAll(field.name);
try writer.writeAll(" = "); try writer.writeAll(" = ");
// TODO check corresponding type and try to adapt fmt accordingly! // TODO check corresponding type and try to adapt fmt accordingly!
try std.fmt.format(writer, "{any}", .{@field(object, field.name)}); // TODO: pass along the already parsed formatting options too
try inner_format(@field(object, field.name), fmt, options, writer, depth + 1);
try writer.writeAll(",\n"); try writer.writeAll(",\n");
} }
try writer.writeAll("\t" ** depth);
try writer.writeAll("}"); try writer.writeAll("}");
}, },
else => {}, .Enum => |e| {
} try writer.writeAll(@typeName(Object));
} try writer.writeAll(" = enum {\n");
inline for (e.fields) |field| {
pub fn enhance_format(comptime object: type) type { try writer.writeAll("\t" ** (depth + 1));
// TODO: append a function to the type if it is a user defined type try writer.writeAll(field.name);
try writer.writeAll(",\n");
// NOTE: this has the clear disadvantage that the type is not the same anymore! (but maybe this is still fine) }
const object_info = @typeInfo(object); try writer.writeAll("\t" ** depth);
switch (object_info) { try writer.writeAll("} = ");
.Struct => |s| { try writer.writeAll(@tagName(object));
return @Type(.{ },
.Struct = .{ // TODO: implement prett_printing for other user defined types (`union` and `vector`)
.layout = .auto, // TODO: recognize []const u8 types and print them as strings
.fields = s.fields, .Array => |a| {
.decls = s.decls, if (a.child == @TypeOf([:0]const u8)) {
.is_tuple = false, try std.fmt.format(writer, "\"{s}\"", .{object});
}, } else {
}); try std.fmt.format(writer, "[]{s}: {any}", .{ @typeName(a.child), object });
}
},
.Vector => |v| {
if (v.child == @TypeOf([:0]const u8)) {
try std.fmt.format(writer, "\"{s}\"", .{object});
} else {
try std.fmt.format(writer, "[]{s}: {any}", .{ @typeName(v.child), object });
}
},
else => {
try std.fmt.format(writer, "{any}", .{object});
}, },
else => {},
} }
return object;
} }