Created
March 19, 2024 21:49
-
-
Save dselman/67d031701af22b80fba3e55b3da6dbf0 to your computer and use it in GitHub Desktop.
Zig: Heterogenous array JSON serialisation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"animals" : [ | |
{ | |
"class" : "Cat", | |
"name" : "Tabby" | |
}, | |
{ | |
"class" : "Cat", | |
"name" : "Tiddles", | |
"ferocity" : 10 | |
}, | |
{ | |
"class" : "Dog", | |
"name" : "Fido", | |
"dogBreed" : "Spaniel" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
const assert = std.debug.assert; | |
const Allocator = std.mem.Allocator; | |
const ArrayList = std.ArrayList; | |
pub fn parseStructBody( | |
comptime T: type, | |
allocator: Allocator, | |
source: anytype, | |
options: std.json.ParseOptions, | |
) std.json.ParseError(@TypeOf(source.*))!T { | |
switch (@typeInfo(T)) { | |
.Struct => |structInfo| { | |
var r: T = undefined; | |
var fields_seen = [_]bool{false} ** structInfo.fields.len; | |
while (true) { | |
var name_token: ?std.json.Token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?); | |
const field_name = switch (name_token.?) { | |
inline .string, .allocated_string => |slice| slice, | |
.object_end => { // No more fields. | |
break; | |
}, | |
else => { | |
return error.UnexpectedToken; | |
}, | |
}; | |
inline for (structInfo.fields, 0..) |field, i| { | |
if (field.is_comptime) @compileError("comptime fields are not supported: " ++ @typeName(T) ++ "." ++ field.name); | |
if (std.mem.eql(u8, field.name, field_name)) { | |
// Free the name token now in case we're using an allocator that optimizes freeing the last allocated object. | |
// (Recursing into innerParse() might trigger more allocations.) | |
freeAllocated(allocator, name_token.?); | |
name_token = null; | |
if (fields_seen[i]) { | |
switch (options.duplicate_field_behavior) { | |
.use_first => { | |
// Parse and ignore the redundant value. | |
// We don't want to skip the value, because we want type checking. | |
_ = try std.json.innerParse(field.type, allocator, source, options); | |
break; | |
}, | |
.@"error" => return error.DuplicateField, | |
.use_last => {}, | |
} | |
} | |
// std.debug.print("Setting field: {s}\n", .{field.name}); | |
@field(r, field.name) = try std.json.innerParse(field.type, allocator, source, options); | |
fields_seen[i] = true; | |
break; | |
} | |
} else { | |
// Didn't match anything. | |
freeAllocated(allocator, name_token.?); | |
if (options.ignore_unknown_fields) { | |
try source.skipValue(); | |
} else { | |
return error.UnknownField; | |
} | |
} | |
} | |
try fillDefaultStructValues(T, &r, &fields_seen); | |
return r; | |
}, | |
else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"), | |
} | |
unreachable; | |
} | |
fn freeAllocated(allocator: Allocator, token: std.json.Token) void { | |
switch (token) { | |
.allocated_number, .allocated_string => |slice| { | |
allocator.free(slice); | |
}, | |
else => {}, | |
} | |
} | |
fn fillDefaultStructValues(comptime T: type, r: *T, fields_seen: *[@typeInfo(T).Struct.fields.len]bool) !void { | |
inline for (@typeInfo(T).Struct.fields, 0..) |field, i| { | |
if (!fields_seen[i]) { | |
if (field.default_value) |default_ptr| { | |
const default = @as(*align(1) const field.type, @ptrCast(default_ptr)).*; | |
@field(r, field.name) = default; | |
} else { | |
std.debug.print("Missing field: {s}\n", .{field.name}); | |
return error.MissingField; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
const testing = std.testing; | |
const ArrayList = @import("std").ArrayList; | |
const Allocator = std.mem.Allocator; | |
const StructParser = @import("./structparser.zig"); | |
const DogBreed = enum { | |
Spaniel, | |
Poodle, | |
JackRussel | |
}; | |
const Cat = struct { | |
name: []const u8, | |
ferocity: ?u32=0, | |
}; | |
const Dog = struct { | |
name: []const u8, | |
dogBreed: DogBreed, | |
}; | |
const Animal = union(enum) { | |
Dog: Dog, | |
Cat: Cat, | |
pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() { | |
if (.object_begin != try source.next()) return error.UnexpectedToken; | |
// key | |
_ = try source.nextAlloc(allocator, options.allocate.?); | |
// class value | |
const classNameToken = try source.nextAlloc(allocator, options.allocate.?); | |
const className = switch (classNameToken) { | |
inline .string, .allocated_string => |k| k, | |
else => unreachable, | |
}; | |
// std.debug.print("class: {s}\n", .{className}); | |
const classType = std.meta.stringToEnum(std.meta.Tag(@This()), className) orelse return error.UnexpectedToken; | |
const result = switch(classType) { | |
.Cat => { | |
return Animal { | |
.Cat = try StructParser.parseStructBody(Cat, allocator, source, options), | |
}; | |
}, | |
.Dog => { | |
return Animal { | |
.Dog = try StructParser.parseStructBody(Dog, allocator, source, options), | |
}; | |
}, | |
}; | |
if (.object_end != try source.next()) return error.UnexpectedToken; | |
return result; | |
} | |
}; | |
const PetCollection = struct { | |
animals: []Animal, | |
}; | |
test "create pets from file" { | |
const file_name = "./src/animals.json"; | |
const data = try std.fs.cwd().readFileAlloc(testing.allocator, file_name, std.math.maxInt(usize)); | |
// std.debug.print("{s}\n", .{data}); | |
defer testing.allocator.free(data); | |
const parsed = try std.json.parseFromSlice(PetCollection, testing.allocator, data, | |
.{ | |
.ignore_unknown_fields = false | |
}); | |
defer parsed.deinit(); | |
var buf: [1024]u8 = undefined; | |
var fba = std.heap.FixedBufferAllocator.init(&buf); | |
var string = std.ArrayList(u8).init(fba.allocator()); | |
try std.json.stringify(parsed.value, .{}, string.writer()); | |
std.debug.print("{s}\n", .{string.items}); | |
const result = parsed.value; | |
switch(result.animals[0]) { | |
.Cat => |cat| { | |
const are_equal = std.mem.eql(u8, cat.name, "Tabby"); | |
try testing.expect(are_equal); | |
}, | |
else => unreachable, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Improved...