From 9927fffe4bfda6f9bdae8520ca75a1e91269b59a Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Tue, 18 Jan 2022 23:33:37 +0000 Subject: [PATCH 1/2] Add support (via 'nestedtext_include_tag' decl) for including union tags when creating nestedtext objects --- src/nestedtext.zig | 63 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/src/nestedtext.zig b/src/nestedtext.zig index 9d016e4..ad9df6e 100644 --- a/src/nestedtext.zig +++ b/src/nestedtext.zig @@ -349,16 +349,28 @@ fn fromArbitraryTypeInternal(allocator: Allocator, value: anytype) anyerror!Valu .Enum, .EnumLiteral => return Value{ .String = @tagName(value) }, .ErrorSet => return Value{ .String = @errorName(value) }, .Union => |union_info| { - // TODO: Could be simplified if - // https://github.com/ziglang/zig/issues/9271 is accepted. - if (union_info.tag_type) |UnionTagType| { - inline for (union_info.fields) |u_field| { - if (value == @field(UnionTagType, u_field.name)) { - return fromArbitraryTypeInternal(allocator, @field(value, u_field.name)); + if (union_info.tag_type == null) { + @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); + } + const decl_name = "nestedtext_include_tag"; + const include_tag: bool = if (@hasDecl(T, decl_name)) @field(T, decl_name) else false; + // TODO: Could be simplified when 'inline switch' is implemented? + inline for (union_info.fields) |u_field, i| { + if (@enumToInt(value) == i) { + const field_name = u_field.name; + const field_value = try fromArbitraryTypeInternal( + allocator, + @field(value, field_name), + ); + if (include_tag) { + var map = Map.init(allocator); + errdefer map.deinit(); + try map.put(field_name, field_value); + return Value{ .Object = map }; + } else { + return field_value; } } - } else { - @compileError("Unable to stringify untagged union '" ++ @typeName(T) ++ "'"); } }, .Struct => |struct_info| { @@ -1796,6 +1808,41 @@ test "from type: union" { } } +test "from type: union, include tag" { + const MyUnion = union(enum) { + foo: usize, + bar: bool, + baz, + + pub const nestedtext_include_tag = true; + }; + + { + const tree = try fromArbitraryType(testing.allocator, MyUnion{ .foo = 123 }); + defer tree.deinit(); + try testToJson("{\"foo\":\"123\"}", tree); + } + { + const tree = try fromArbitraryType(testing.allocator, MyUnion{ .bar = true }); + defer tree.deinit(); + try testToJson("{\"bar\":\"true\"}", tree); + } + { + const tree = try fromArbitraryType(testing.allocator, MyUnion{ .baz = {} }); + defer tree.deinit(); + try testToJson("{\"baz\":\"null\"}", tree); + } + { + // Note that without the curly-brace syntax to explicitly pass 'void', + // the value is treated as a field of the underlying enum used by the + // tagged enum. This is treated differently in the conversion, using + // the enum field name rather than the union payload... + const tree = try fromArbitraryType(testing.allocator, MyUnion.baz); + defer tree.deinit(); + try testToJson("\"baz\"", tree); + } +} + test "from type: struct" { const MyStruct = struct { foo: usize, From 0558e3a30d5ae4a19ae4c132c5bfd78125e2a809 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Wed, 19 Jan 2022 00:09:30 +0000 Subject: [PATCH 2/2] Add support (via 'nestedtext_include_tag' decl) for requiring union tags when parsing nestedtext --- src/nestedtext.zig | 75 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/src/nestedtext.zig b/src/nestedtext.zig index ad9df6e..f30169d 100644 --- a/src/nestedtext.zig +++ b/src/nestedtext.zig @@ -744,7 +744,32 @@ pub const Parser = struct { } }, .Union => |union_info| { - if (union_info.tag_type) |_| { + if (union_info.tag_type == null) { + @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); + } + const decl_name = "nestedtext_include_tag"; + const include_tag: bool = if (@hasDecl(T, decl_name)) @field(T, decl_name) else false; + if (include_tag) { + var obj: Map = undefined; + // Check the value type. + switch (value) { + .Object => |_obj| obj = _obj, + else => return error.UnexpectedType, + } + if (obj.count() != 1) return error.UnexpectedType; + const obj_entry = obj.pop(); + // Search for the union field matching the map key. + inline for (union_info.fields) |field| { + if (std.mem.eql(u8, field.name, obj_entry.key)) { + return @unionInit( + T, + field.name, + try p.parseTypedInternal(field.field_type, obj_entry.value), + ); + } + } + return error.UnexpectedType; + } else { // Try each of the union fields until we find one with a type // that the value can successfully be parsed into. inline for (union_info.fields) |field| { @@ -760,8 +785,6 @@ pub const Parser = struct { } } return error.UnexpectedType; - } else { - @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); } }, .Struct => |struct_info| { @@ -1504,7 +1527,37 @@ test "typed parse: union" { try testing.expectEqual(MyUnion{ .foo = 1 }, try p.parseTyped(MyUnion, "> 1")); try testing.expectEqual(MyUnion{ .bar = false }, try p.parseTyped(MyUnion, "> false")); - try testing.expectEqual(MyUnion.baz, try p.parseTyped(MyUnion, "> null")); + try testing.expectEqual(MyUnion{ .baz = {} }, try p.parseTyped(MyUnion, "> null")); + // TODO: + // Note that without the curly-brace syntax to explicitly pass 'void', + // the value is treated as a field of the underlying enum used by the + // tagged union. This should be made consistent with the reverse (converting + // from arbitrary Zig type), so should use the enum field name rather than + // the union payload... + // try testing.expectEqual(MyUnion.baz, try p.parseTyped(MyUnion, "> baz")); +} + +test "typed parse: union, include tag" { + var p = Parser.init(testing.allocator, .{}); + + const MyUnion = union(enum) { + foo: usize, + bar: bool, + baz, + + const nestedtext_include_tag = true; + }; + + try testing.expectEqual(MyUnion{ .foo = 1 }, try p.parseTyped(MyUnion, "foo: 1")); + try testing.expectEqual(MyUnion{ .bar = false }, try p.parseTyped(MyUnion, "bar: false")); + try testing.expectEqual(MyUnion{ .baz = {} }, try p.parseTyped(MyUnion, "baz: null")); + // TODO: + // Note that without the curly-brace syntax to explicitly pass 'void', + // the value is treated as a field of the underlying enum used by the + // tagged union. This should be made consistent with the reverse (converting + // from arbitrary Zig type), so should use the enum field name rather than + // the union payload... + // try testing.expectEqual(MyUnion.baz, try p.parseTyped(MyUnion, "baz")); } test "typed parse: pointer to single elem" { @@ -1800,7 +1853,7 @@ test "from type: union" { { // Note that without the curly-brace syntax to explicitly pass 'void', // the value is treated as a field of the underlying enum used by the - // tagged enum. This is treated differently in the conversion, using + // tagged union. This is treated differently in the conversion, using // the enum field name rather than the union payload... const tree = try fromArbitraryType(testing.allocator, MyUnion.baz); defer tree.deinit(); @@ -1814,32 +1867,32 @@ test "from type: union, include tag" { bar: bool, baz, - pub const nestedtext_include_tag = true; + const nestedtext_include_tag = true; }; { const tree = try fromArbitraryType(testing.allocator, MyUnion{ .foo = 123 }); defer tree.deinit(); - try testToJson("{\"foo\":\"123\"}", tree); + try testStringify("foo: 123", tree); } { const tree = try fromArbitraryType(testing.allocator, MyUnion{ .bar = true }); defer tree.deinit(); - try testToJson("{\"bar\":\"true\"}", tree); + try testStringify("bar: true", tree); } { const tree = try fromArbitraryType(testing.allocator, MyUnion{ .baz = {} }); defer tree.deinit(); - try testToJson("{\"baz\":\"null\"}", tree); + try testStringify("baz: null", tree); } { // Note that without the curly-brace syntax to explicitly pass 'void', // the value is treated as a field of the underlying enum used by the - // tagged enum. This is treated differently in the conversion, using + // tagged union. This is treated differently in the conversion, using // the enum field name rather than the union payload... const tree = try fromArbitraryType(testing.allocator, MyUnion.baz); defer tree.deinit(); - try testToJson("\"baz\"", tree); + try testStringify("> baz", tree); } }