summaryrefslogtreecommitdiff
path: root/src/cli/args.zig
diff options
context:
space:
mode:
authorMitchell Hashimoto <m@mitchellh.com>2024-09-16 10:51:40 -0700
committerMitchell Hashimoto <m@mitchellh.com>2024-09-16 15:53:59 -0700
commita389987ada7fcd976c443c950390b4743ca1b545 (patch)
tree63af96554425fbb1127bab7296601fba58d5b33d /src/cli/args.zig
parentdfe62cec8555aa3ced880655ec08296108668e92 (diff)
cli: config structure supports tagged unions
The syntax of tagged unions is `tag:value`. This matches the tagged union parsing syntax for keybindings (i.e. `new_split:right`). I'm adding this now on its own without a user-facing feature because I can see some places we might use this and I want to separate this out. There is already a PR open now that can utilize this (#2231).
Diffstat (limited to 'src/cli/args.zig')
-rw-r--r--src/cli/args.zig147
1 files changed, 146 insertions, 1 deletions
diff --git a/src/cli/args.zig b/src/cli/args.zig
index 0f21ea79b..2244a801d 100644
--- a/src/cli/args.zig
+++ b/src/cli/args.zig
@@ -268,7 +268,13 @@ fn parseIntoField(
value orelse return error.ValueRequired,
),
- else => unreachable,
+ .Union => try parseTaggedUnion(
+ Field,
+ alloc,
+ value orelse return error.ValueRequired,
+ ),
+
+ else => @compileError("unsupported field type"),
},
};
@@ -279,6 +285,52 @@ fn parseIntoField(
return error.InvalidField;
}
+fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
+ const info = @typeInfo(T).Union;
+ assert(@typeInfo(info.tag_type.?) == .Enum);
+
+ // Get the union tag that is being set. We support values with no colon
+ // if the value is void so its not an error to have no colon.
+ const colon_idx = mem.indexOf(u8, v, ":") orelse v.len;
+ const tag_str = std.mem.trim(u8, v[0..colon_idx], whitespace);
+ const value = if (colon_idx < v.len) v[colon_idx + 1 ..] else "";
+
+ // Find the field in the union that matches the tag.
+ inline for (info.fields) |field| {
+ if (mem.eql(u8, field.name, tag_str)) {
+ // Special case void types where we don't need a value.
+ if (field.type == void) {
+ if (value.len > 0) return error.InvalidValue;
+ return @unionInit(T, field.name, {});
+ }
+
+ // We need to create a struct that looks like this union field.
+ // This lets us use parseIntoField as if its a dedicated struct.
+ const Target = @Type(.{ .Struct = .{
+ .layout = .auto,
+ .fields = &.{.{
+ .name = field.name,
+ .type = field.type,
+ .default_value = null,
+ .is_comptime = false,
+ .alignment = @alignOf(field.type),
+ }},
+ .decls = &.{},
+ .is_tuple = false,
+ } });
+
+ // Parse the value into the struct
+ var t: Target = undefined;
+ try parseIntoField(Target, alloc, &t, field.name, value);
+
+ // Build our union
+ return @unionInit(T, field.name, @field(t, field.name));
+ }
+ }
+
+ return error.InvalidValue;
+}
+
fn parsePackedStruct(comptime T: type, v: []const u8) !T {
const info = @typeInfo(T).Struct;
assert(info.layout == .@"packed");
@@ -742,6 +794,99 @@ test "parseIntoField: struct with parse func with unsupported error tracking" {
);
}
+test "parseIntoField: tagged union" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var data: struct {
+ value: union(enum) {
+ a: u8,
+ b: u8,
+ c: void,
+ d: []const u8,
+ } = undefined,
+ } = .{};
+
+ // Set one field
+ try parseIntoField(@TypeOf(data), alloc, &data, "value", "a:1");
+ try testing.expectEqual(1, data.value.a);
+
+ // Set another
+ try parseIntoField(@TypeOf(data), alloc, &data, "value", "b:2");
+ try testing.expectEqual(2, data.value.b);
+
+ // Set void field
+ try parseIntoField(@TypeOf(data), alloc, &data, "value", "c");
+ try testing.expectEqual({}, data.value.c);
+
+ // Set string field
+ try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
+ try testing.expectEqualStrings("hello", data.value.d);
+}
+
+test "parseIntoField: tagged union unknown filed" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var data: struct {
+ value: union(enum) {
+ a: u8,
+ b: u8,
+ } = undefined,
+ } = .{};
+
+ try testing.expectError(
+ error.InvalidValue,
+ parseIntoField(@TypeOf(data), alloc, &data, "value", "c:1"),
+ );
+}
+
+test "parseIntoField: tagged union invalid field value" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var data: struct {
+ value: union(enum) {
+ a: u8,
+ b: u8,
+ } = undefined,
+ } = .{};
+
+ try testing.expectError(
+ error.InvalidValue,
+ parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"),
+ );
+}
+
+test "parseIntoField: tagged union missing tag" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var data: struct {
+ value: union(enum) {
+ a: u8,
+ b: u8,
+ } = undefined,
+ } = .{};
+
+ try testing.expectError(
+ error.InvalidValue,
+ parseIntoField(@TypeOf(data), alloc, &data, "value", "a"),
+ );
+ try testing.expectError(
+ error.InvalidValue,
+ parseIntoField(@TypeOf(data), alloc, &data, "value", ":a"),
+ );
+}
+
/// Returns an iterator (implements "next") that reads CLI args by line.
/// Each CLI arg is expected to be a single line. This is used to implement
/// configuration files.