diff --git a/src/cli/args.zig b/src/cli/args.zig index 0f21ea79b4..2575b63d9a 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,44 @@ 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. + const colon_idx = mem.indexOf(u8, v, ":") orelse return error.InvalidValue; + const tag_str = std.mem.trim(u8, v[0..colon_idx], whitespace); + + // Find the field in the union that matches the tag. + inline for (info.fields) |field| { + if (mem.eql(u8, field.name, tag_str)) { + // 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, v[colon_idx + 1 ..]); + + // 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 +786,89 @@ 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, + } = 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); +} + +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.