const std = @import("std"); pub const Config = struct { pub const Layout = enum { @"inline", stacked }; context: u16 = 0, tscontext: bool = false, tslocations: bool = true, layout: Layout = .stacked, /// 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, .@"enum" => std.meta.stringToEnum(field.type, value) orelse return error.InvalidLayout, 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}), .@"enum" => try writer.writeAll(@tagName(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 layout:stacked", 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); try std.testing.expectEqual(Config.Layout.@"inline", (try config.parse("layout:inline")).layout); } 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")); try std.testing.expectError(error.InvalidLayout, config.parse("context:5 layout:sideways")); 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); }