summaryrefslogtreecommitdiff
path: root/src/locations_config.zig
blob: ed67f99069efb94a1544e27b1e135cfae93826e1 (plain) (blame)
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);
}