summaryrefslogtreecommitdiff
path: root/src/locations_config.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/locations_config.zig')
-rw-r--r--src/locations_config.zig69
1 files changed, 69 insertions, 0 deletions
diff --git a/src/locations_config.zig b/src/locations_config.zig
new file mode 100644
index 00000000..ed67f990
--- /dev/null
+++ b/src/locations_config.zig
@@ -0,0 +1,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);
+}