1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
const std = @import("std");
pub const Config = struct {
context: u16 = 0,
tscontext: bool = false,
tslocations: bool = true,
/// Apply only supplied fields. A malformed token rejects the whole update.
/// Repeating a field uses its last supplied value.
pub fn parse(current: Config, text: []const u8) !Config {
var result = current;
var tokens = std.mem.tokenizeAny(u8, text, " \t\r\n");
while (tokens.next()) |token| {
const colon = std.mem.indexOfScalar(u8, token, ':') orelse return error.ExpectedKeyValue;
const key = token[0..colon];
const value = token[colon + 1 ..];
var found = false;
inline for (@typeInfo(Config).@"struct".fields) |field| {
if (std.mem.eql(u8, key, field.name)) {
@field(result, field.name) = switch (@typeInfo(field.type)) {
.bool => if (std.mem.eql(u8, value, "on")) true else if (std.mem.eql(u8, value, "off")) false else return error.ExpectedOnOrOff,
.int => std.fmt.parseInt(field.type, value, 10) catch return error.InvalidContext,
else => @compileError("unsupported location setting type"),
};
found = true;
}
}
if (!found) return error.UnknownLocationSetting;
}
return result;
}
pub fn write(config: Config, writer: *std.Io.Writer) !void {
inline for (@typeInfo(Config).@"struct".fields, 0..) |field, index| {
if (index > 0) try writer.writeByte(' ');
try writer.writeAll(field.name ++ ":");
const value = @field(config, field.name);
switch (@typeInfo(field.type)) {
.bool => try writer.writeAll(if (value) "on" else "off"),
.int => try writer.print("{d}", .{value}),
else => @compileError("unsupported location setting type"),
}
}
}
};
test "LocationsConfig reflection round trip and partial updates" {
var config: Config = .{};
config = try config.parse("context:5 tscontext:on");
config = try config.parse("tslocations:off");
try std.testing.expectEqual(Config{ .context = 5, .tscontext = true, .tslocations = false }, config);
var out: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer out.deinit();
try config.write(&out.writer);
try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off", out.written());
try std.testing.expectEqual(config, try (Config{}).parse(out.written()));
try std.testing.expectEqual(config, try config.parse(" \n\t"));
try std.testing.expectEqual(@as(u16, 2), (try config.parse("context:1 context:2")).context);
}
test "LocationsConfig rejects invalid updates atomically" {
const config: Config = .{ .context = 3, .tscontext = true };
try std.testing.expectError(error.UnknownLocationSetting, config.parse("context:5 unknown:on"));
try std.testing.expectError(error.ExpectedOnOrOff, config.parse("context:5 tscontext:true"));
try std.testing.expectError(error.ExpectedKeyValue, config.parse("context"));
for ([_][]const u8{ "context:", "context:-1", "context:65536", "context:five" }) |input|
try std.testing.expectError(error.InvalidContext, config.parse(input));
try std.testing.expectEqual(@as(u16, 3), config.context);
}
|