Skip to content

Commit

Permalink
cli: config structure supports tagged unions
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
mitchellh committed Sep 16, 2024
1 parent dfe62ce commit 86aabcd
Showing 1 changed file with 128 additions and 1 deletion.
129 changes: 128 additions & 1 deletion src/cli/args.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
};

Expand All @@ -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");
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 86aabcd

Please sign in to comment.