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 a389987
Showing 1 changed file with 146 additions and 1 deletion.
147 changes: 146 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,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");
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit a389987

Please sign in to comment.