config: store some basic errors on parse

This commit is contained in:
Mitchell Hashimoto
2023-09-11 08:54:37 -07:00
parent cc13f0fe49
commit 58a43f1980
3 changed files with 79 additions and 2 deletions

View File

@ -4,10 +4,20 @@ const assert = std.debug.assert;
const Allocator = mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ErrorList = @import("config/ErrorList.zig");
// TODO:
// - Only `--long=value` format is accepted. Do we want to allow
// `--long value`? Not currently allowed.
/// The base errors for arg parsing. Additional errors can be returned due
/// to type-specific parsing but these are always possible.
pub const Error = error{
ValueRequired,
InvalidField,
InvalidValue,
};
/// Parse the command line arguments from iter into dst.
///
/// dst must be a struct. The fields and their types will be used to determine
@ -19,6 +29,10 @@ const ArenaAllocator = std.heap.ArenaAllocator;
/// an arena allocator will be created (or reused if set already) for any
/// allocations. Allocations are necessary for certain types, like `[]const u8`.
///
/// If the destination type has a field "_errors" of type "ErrorList" then
/// errors will be added to that list. In this case, the only error returned by
/// parse are allocation errors.
///
/// Note: If the arena is already non-null, then it will be used. In this
/// case, in the case of an error some memory might be leaked into the arena.
pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
@ -62,11 +76,45 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
break :value null;
};
try parseIntoField(T, arena_alloc, dst, key, value);
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
if (comptime !canTrackErrors(T)) return err;
switch (err) {
error.InvalidField => try dst._errors.add(arena_alloc, .{
.message = try std.fmt.allocPrintZ(
arena_alloc,
"unknown field: {s}",
.{key},
),
}),
error.ValueRequired => try dst._errors.add(arena_alloc, .{
.message = try std.fmt.allocPrintZ(
arena_alloc,
"{s}: value required",
.{key},
),
}),
error.InvalidValue => try dst._errors.add(arena_alloc, .{
.message = try std.fmt.allocPrintZ(
arena_alloc,
"{s}: invalid value",
.{key},
),
}),
else => return err,
}
};
}
}
}
/// Returns true if this type can track errors.
fn canTrackErrors(comptime T: type) bool {
return @hasField(T, "_errors");
}
/// Parse a single key/value pair into the destination type T.
///
/// This may result in allocations. The allocations can only be freed by freeing
@ -240,6 +288,29 @@ test "parse: quoted value" {
try testing.expectEqualStrings("hello!", data.b);
}
test "parse: error tracking" {
const testing = std.testing;
var data: struct {
a: []const u8 = "",
b: enum { one } = .one,
_arena: ?ArenaAllocator = null,
_errors: ErrorList = .{},
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
testing.allocator,
"--what --a=42",
);
defer iter.deinit();
try parse(@TypeOf(data), testing.allocator, &data, &iter);
try testing.expect(data._arena != null);
try testing.expectEqualStrings("42", data.a);
try testing.expect(!data._errors.empty());
}
test "parseIntoField: ignore underscore-prefixed fields" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);

View File

@ -14,6 +14,7 @@ const cli_args = @import("../cli_args.zig");
const Key = @import("key.zig").Key;
const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig");
const log = std.log.scoped(.config);
@ -341,6 +342,11 @@ keybind: Keybinds = .{},
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
/// List of errors that occurred while loading. This can be accessed directly
/// by callers. It is only underscore-prefixed so it can't be set by the
/// configuration file.
_errors: ErrorList = .{},
pub fn deinit(self: *Config) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;

View File

@ -393,7 +393,7 @@ pub const Set = struct {
alloc: Allocator,
t: Trigger,
action: Action,
) !void {
) Allocator.Error!void {
// unbind should never go into the set, it should be handled prior
assert(action != .unbind);