Skip to content

Commit b02cc7f

Browse files
authored
Merge pull request #10 from OrlovEvgeny/fix/hashmap_toml
fix: Deserialise HashMap of YAML/TOML
2 parents ac10e7d + e212334 commit b02cc7f

File tree

6 files changed

+239
-8
lines changed

6 files changed

+239
-8
lines changed

src/formats/toml/deserializer.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ fn deserializeValue(comptime T: type, val: *const Value, allocator: Allocator) D
223223
return ptr;
224224
},
225225
.void => return {},
226+
.map => {
227+
if (val.* != .table) return error.WrongType;
228+
var deser = Deserializer.init(&val.table);
229+
return core_deserialize.deserialize(T, allocator, &deser, .{});
230+
},
226231
else => @compileError("TOML deserialization does not support: " ++ @typeName(T)),
227232
}
228233
}

src/formats/toml/mod.zig

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,3 +729,53 @@ test "deserialize error: type mismatch" {
729729
const result = fromSlice(Cfg, arena.allocator(), "x = \"not a number\"\n");
730730
try testing.expectError(error.WrongType, result);
731731
}
732+
733+
test "deserialize StringHashMap" {
734+
const V = struct { foo: []const u8 };
735+
const T = struct { a: std.StringHashMap(V) };
736+
737+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
738+
defer arena.deinit();
739+
const parsed = try fromSlice(T, arena.allocator(),
740+
\\[a.b]
741+
\\foo = "bar"
742+
);
743+
try testing.expectEqual(@as(usize, 1), parsed.a.count());
744+
const b = parsed.a.get("b") orelse return error.TestUnexpectedResult;
745+
try testing.expectEqualStrings("bar", b.foo);
746+
}
747+
748+
test "roundtrip StringHashMap scalar values" {
749+
var map = std.StringHashMap(i32).init(testing.allocator);
750+
defer map.deinit();
751+
try map.put("x", 1);
752+
try map.put("y", 2);
753+
754+
const Root = struct { data: std.StringHashMap(i32) };
755+
const bytes = try toSlice(testing.allocator, Root{ .data = map });
756+
defer testing.allocator.free(bytes);
757+
758+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
759+
defer arena.deinit();
760+
const result = try fromSlice(Root, arena.allocator(), bytes);
761+
try testing.expectEqual(@as(i32, 1), result.data.get("x").?);
762+
try testing.expectEqual(@as(i32, 2), result.data.get("y").?);
763+
}
764+
765+
test "roundtrip StringHashMap struct values" {
766+
const V = struct { val: i32 };
767+
var map = std.StringHashMap(V).init(testing.allocator);
768+
defer map.deinit();
769+
try map.put("a", .{ .val = 10 });
770+
try map.put("b", .{ .val = 20 });
771+
772+
const Root = struct { data: std.StringHashMap(V) };
773+
const bytes = try toSlice(testing.allocator, Root{ .data = map });
774+
defer testing.allocator.free(bytes);
775+
776+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
777+
defer arena.deinit();
778+
const result = try fromSlice(Root, arena.allocator(), bytes);
779+
try testing.expectEqual(@as(i32, 10), result.data.get("a").?.val);
780+
try testing.expectEqual(@as(i32, 20), result.data.get("b").?.val);
781+
}

src/formats/toml/serializer.zig

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub const StructSerializer = struct {
106106
// Sub-tables and array-of-tables are deferred to appear after scalar fields.
107107
// Unions with payload variants also serialize as sub-tables (external tag
108108
// produces a struct with one key = variant name).
109-
if (k == .@"struct") {
109+
if (k == .@"struct" or k == .map) {
110110
try self.deferSubTable(key, value, false);
111111
return;
112112
}
@@ -133,9 +133,74 @@ pub const StructSerializer = struct {
133133
}
134134

135135
pub fn serializeEntry(self: *StructSerializer, key: anytype, value: anytype) Error!void {
136-
_ = key;
137-
_ = value;
138-
_ = self;
136+
const V = @TypeOf(value);
137+
const k = comptime kind_mod.typeKind(V);
138+
139+
if (k == .optional) {
140+
if (value == null) return;
141+
return self.serializeEntry(key, value.?);
142+
}
143+
144+
if (k == .@"struct" or k == .map or (k == .@"union" and comptime unionHasPayload(V))) {
145+
try self.deferSubTableDynamic(key, value);
146+
return;
147+
}
148+
149+
const K = @TypeOf(key);
150+
if (K == []const u8) {
151+
writeTomlKey(self.out, key) catch return error.WriteFailed;
152+
} else if (comptime @typeInfo(K) == .int) {
153+
self.out.print("{d}", .{key}) catch return error.WriteFailed;
154+
} else {
155+
@compileError("unsupported map key type for TOML: " ++ @typeName(K));
156+
}
157+
self.out.writeAll(" = ") catch return error.WriteFailed;
158+
159+
var child = Serializer{
160+
.out = self.out,
161+
.allocator = self.allocator,
162+
.path = self.path,
163+
};
164+
try core_serialize.serialize(V, value, &child, .{});
165+
self.out.writeByte('\n') catch return error.WriteFailed;
166+
}
167+
168+
fn deferSubTableDynamic(self: *StructSerializer, key: []const u8, value: anytype) Error!void {
169+
var aw: std.io.Writer.Allocating = .init(self.allocator);
170+
171+
const new_path = self.allocator.alloc([]const u8, self.path.len + 1) catch return error.OutOfMemory;
172+
@memcpy(new_path[0..self.path.len], self.path);
173+
new_path[self.path.len] = key;
174+
175+
aw.writer.writeByte('\n') catch return error.WriteFailed;
176+
aw.writer.writeByte('[') catch return error.WriteFailed;
177+
for (new_path, 0..) |seg, i| {
178+
if (i > 0) aw.writer.writeByte('.') catch return error.WriteFailed;
179+
writeTomlKey(&aw.writer, seg) catch return error.WriteFailed;
180+
}
181+
aw.writer.writeAll("]\n") catch return error.WriteFailed;
182+
183+
var child_ser = Serializer{
184+
.out = &aw.writer,
185+
.allocator = self.allocator,
186+
.path = new_path,
187+
};
188+
core_serialize.serialize(@TypeOf(value), value, &child_ser, .{}) catch {
189+
self.allocator.free(new_path);
190+
aw.deinit();
191+
return error.WriteFailed;
192+
};
193+
self.allocator.free(new_path);
194+
195+
const data = aw.toOwnedSlice() catch return error.OutOfMemory;
196+
self.deferred.append(self.allocator, .{
197+
.key = key,
198+
.data = data,
199+
.is_array_of_tables = false,
200+
}) catch {
201+
self.allocator.free(data);
202+
return error.OutOfMemory;
203+
};
139204
}
140205

141206
pub fn end(self: *StructSerializer) Error!void {

src/formats/yaml/deserializer.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ fn deserializeValue(comptime T: type, val: *const Value, allocator: Allocator) D
245245
if (val.* != .null_val) return error.WrongType;
246246
return {};
247247
},
248+
.map => {
249+
if (val.* != .mapping) return error.WrongType;
250+
var deser = Deserializer.init(val);
251+
return core_deserialize.deserialize(T, allocator, &deser, .{});
252+
},
248253
else => @compileError("YAML deserialization does not support: " ++ @typeName(T)),
249254
}
250255
}

src/formats/yaml/mod.zig

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,3 +859,54 @@ test "parseAllValues single document" {
859859
defer arena.allocator().free(docs);
860860
try testing.expectEqual(@as(usize, 1), docs.len);
861861
}
862+
863+
test "deserialize StringHashMap" {
864+
const V = struct { foo: []const u8 };
865+
const T = struct { a: std.StringHashMap(V) };
866+
867+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
868+
defer arena.deinit();
869+
const parsed = try fromSlice(T, arena.allocator(),
870+
\\a:
871+
\\ b:
872+
\\ foo: bar
873+
);
874+
try testing.expectEqual(@as(usize, 1), parsed.a.count());
875+
const b = parsed.a.get("b") orelse return error.TestUnexpectedResult;
876+
try testing.expectEqualStrings("bar", b.foo);
877+
}
878+
879+
test "roundtrip StringHashMap scalar values" {
880+
var map = std.StringHashMap(i32).init(testing.allocator);
881+
defer map.deinit();
882+
try map.put("x", 1);
883+
try map.put("y", 2);
884+
885+
const Root = struct { data: std.StringHashMap(i32) };
886+
const bytes = try toSlice(testing.allocator, Root{ .data = map });
887+
defer testing.allocator.free(bytes);
888+
889+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
890+
defer arena.deinit();
891+
const result = try fromSlice(Root, arena.allocator(), bytes);
892+
try testing.expectEqual(@as(i32, 1), result.data.get("x").?);
893+
try testing.expectEqual(@as(i32, 2), result.data.get("y").?);
894+
}
895+
896+
test "roundtrip StringHashMap struct values" {
897+
const V = struct { val: i32 };
898+
var map = std.StringHashMap(V).init(testing.allocator);
899+
defer map.deinit();
900+
try map.put("a", .{ .val = 10 });
901+
try map.put("b", .{ .val = 20 });
902+
903+
const Root = struct { data: std.StringHashMap(V) };
904+
const bytes = try toSlice(testing.allocator, Root{ .data = map });
905+
defer testing.allocator.free(bytes);
906+
907+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
908+
defer arena.deinit();
909+
const result = try fromSlice(Root, arena.allocator(), bytes);
910+
try testing.expectEqual(@as(i32, 10), result.data.get("a").?.val);
911+
try testing.expectEqual(@as(i32, 20), result.data.get("b").?.val);
912+
}

src/formats/yaml/serializer.zig

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ pub const StructSerializer = struct {
141141

142142
// Compound types start on the next line. Unions with payloads are treated
143143
// as compound because external tagging serializes them as a mapping.
144-
if (k == .@"struct" or (k == .@"union" and comptime unionHasPayload(@TypeOf(value)))) {
144+
if (k == .@"struct" or k == .map or (k == .@"union" and comptime unionHasPayload(@TypeOf(value)))) {
145145
self.out.writeByte('\n') catch return error.WriteFailed;
146146
var child = Serializer{
147147
.out = self.out,
@@ -179,9 +179,64 @@ pub const StructSerializer = struct {
179179
}
180180

181181
pub fn serializeEntry(self: *StructSerializer, key: anytype, value: anytype) Error!void {
182-
_ = key;
183-
_ = value;
184-
_ = self;
182+
const V = @TypeOf(value);
183+
const k = comptime kind_mod.typeKind(V);
184+
185+
if (self.first and self.is_map_value) {
186+
self.first = false;
187+
writeIndent(self.out, self.depth, self.indent_size) catch return error.WriteFailed;
188+
} else {
189+
if (!self.first) {
190+
self.out.writeByte('\n') catch return error.WriteFailed;
191+
}
192+
self.first = false;
193+
writeIndent(self.out, self.depth, self.indent_size) catch return error.WriteFailed;
194+
}
195+
196+
const K = @TypeOf(key);
197+
if (K == []const u8) {
198+
writeYamlKey(self.out, key) catch return error.WriteFailed;
199+
} else if (comptime @typeInfo(K) == .int) {
200+
self.out.print("{d}", .{key}) catch return error.WriteFailed;
201+
} else {
202+
@compileError("unsupported map key type for YAML: " ++ @typeName(K));
203+
}
204+
self.out.writeAll(": ") catch return error.WriteFailed;
205+
206+
if (k == .@"struct" or k == .map or (k == .@"union" and comptime unionHasPayload(V))) {
207+
self.out.writeByte('\n') catch return error.WriteFailed;
208+
var child = Serializer{
209+
.out = self.out,
210+
.depth = self.depth + 1,
211+
.indent_size = self.indent_size,
212+
.is_map_value = true,
213+
.opts = self.opts,
214+
};
215+
core_serialize.serialize(V, value, &child, .{}) catch return error.WriteFailed;
216+
return;
217+
}
218+
219+
if (k == .slice or k == .array) {
220+
self.out.writeByte('\n') catch return error.WriteFailed;
221+
var child = Serializer{
222+
.out = self.out,
223+
.depth = self.depth + 1,
224+
.indent_size = self.indent_size,
225+
.is_map_value = false,
226+
.opts = self.opts,
227+
};
228+
core_serialize.serialize(V, value, &child, .{}) catch return error.WriteFailed;
229+
return;
230+
}
231+
232+
var child = Serializer{
233+
.out = self.out,
234+
.depth = self.depth + 1,
235+
.indent_size = self.indent_size,
236+
.is_map_value = false,
237+
.opts = self.opts,
238+
};
239+
core_serialize.serialize(V, value, &child, .{}) catch return error.WriteFailed;
185240
}
186241

187242
pub fn end(self: *StructSerializer) Error!void {

0 commit comments

Comments
 (0)