Allow convert from JSON to a struct and from a struct to JSON

This commit is contained in:
Cameron Reed 2024-11-04 12:51:58 -07:00
parent 441b50d94b
commit 3d216fbd23
2 changed files with 263 additions and 4 deletions

View File

@ -7,6 +7,13 @@ pub const JSONError = error{
UnexpectedCharacter,
} || std.mem.Allocator.Error;
pub const JSONConvertError = error{
IncorrectArrayLength,
IncorrectType,
MissingValue,
UnsupportedType,
};
pub const JSONType = enum {
Object,
Array,
@ -65,7 +72,7 @@ pub const JSONObject = struct {
return self.children.get(name);
}
pub fn print(self: *JSONObject, writer: std.fs.File.Writer, format: JSONFormat, indent_level: usize) std.fs.File.WriteError!void {
pub fn write(self: *JSONObject, writer: std.fs.File.Writer, format: JSONFormat, indent_level: usize) std.fs.File.WriteError!void {
try writer.writeByte('{');
var iter = self.children.keyIterator();
@ -310,7 +317,7 @@ pub const JSONObjectValue = struct {
fn print(value: *const JSONValue, writer: std.fs.File.Writer, format: JSONFormat, indent_level: usize) std.fs.File.WriteError!void {
const self: *const JSONObjectValue = @fieldParentPtr("value", value);
try self.object.print(writer, format, indent_level);
try self.object.write(writer, format, indent_level);
}
fn toString(value: *const JSONValue, allocator: std.mem.Allocator, format: JSONFormat, indent_level: usize) std.mem.Allocator.Error![]const u8 {
@ -494,6 +501,14 @@ pub fn parseFile(allocator: std.mem.Allocator, fileName: []const u8) !*JSONObjec
return parser.parse();
}
// This will leak memory in it's current state, and if you free the object, the struct may be referencing invalid memory
// This is a problem that can be solved, but I'm going to think on it for a bit
// pub fn parseFileToStruct(allocator: std.mem.Allocator, fileName: []const u8, T: type) !T {
// const obj = try parseFile(allocator, fileName);
// errdefer obj.deinit();
// return try JSONtoNewStruct(obj, T);
// }
pub fn parseString(allocator: std.mem.Allocator, string: []const u8) !*JSONObject {
var parser = try JSONParser.init(allocator, string);
return parser.parse();
@ -504,7 +519,7 @@ pub fn writeToFile(fileName: []const u8, json: *JSONObject, format: JSONFormat)
defer file.close();
const writer = file.writer();
return json.print(writer, format, 0);
return json.write(writer, format, 0);
}
const JSONParser = struct {
@ -855,6 +870,213 @@ const JSONParser = struct {
}
};
pub fn JSONtoNewStruct(object: *JSONObject, T: type) JSONConvertError!T {
var result: T = undefined;
try JSONtoStruct(object, T, &result);
return result;
}
pub fn JSONtoStruct(object: *JSONObject, T: type, mem: *T) JSONConvertError!void {
const result_type_info = @typeInfo(T);
if (result_type_info != .Struct) {
@compileError("Can only convert JSONObject to struct, not " ++ @typeName(T));
}
const fields_info = result_type_info.Struct.fields;
inline for (fields_info) |field| {
try convertJSONtoStructField(field.type, &@field(mem.*, field.name), object.children.get(field.name));
}
}
fn convertJSONtoStructField(field_type1: type, field: *field_type1, value_or_null: ?*JSONValue) JSONConvertError!void {
comptime var field_type = field_type1;
comptime var field_type_info = @typeInfo(field_type);
if (value_or_null == null or value_or_null.?.type == .Null) {
if (field_type_info == .Optional) {
field.* = null;
return;
} else {
return JSONConvertError.MissingValue;
}
}
const value = value_or_null.?;
if (field_type_info == .Optional) {
field_type = field_type_info.Optional.child;
field_type_info = @typeInfo(field_type);
}
switch (field_type_info) {
.Int => {
if (value.type == .Int) {
const int_value = value.getInt() catch @panic("JSON value type does not match what was reported");
field.* = int_value;
} else {
return JSONConvertError.IncorrectType;
}
},
.Float => {
if (value.type == .Float) {
const float_value = value.getFloat() catch @panic("JSON value type does not match what was reported");
field.* = float_value;
} else {
return JSONConvertError.IncorrectType;
}
},
.Bool => {
if (value.type == .Bool) {
field.* = value.getBool() catch @panic("JSON value type does not match what was reported");
} else {
return JSONConvertError.IncorrectType;
}
},
.Pointer => {
if (field_type_info.Pointer.size == .Slice) {
if (field_type_info.Pointer.child == u8 and value.type == .String) {
field.* = value.getString() catch @panic("JSON value type does not match what was reported");
} else if (field_type_info.Pointer.child == *JSONValue and value.type == .Array) {
field.* = value.getArray() catch @panic("JSON value type does not match what was reported");
} else {
return JSONConvertError.UnsupportedType;
}
}
},
.Array => {
if (value.type == .Array) {
const array = value.getArray() catch @panic("JSON value type does not match what was reported");
if (array.len != field_type_info.Array.len) {
return JSONConvertError.IncorrectArrayLength;
}
inline for (0..field_type_info.Array.len) |i| {
try convertJSONtoStructField(field_type_info.Array.child, &field.*[i], array[i]);
}
} else {
return JSONConvertError.IncorrectType;
}
},
.Struct => {
if (field_type == JSONObject) {
if (value.type == .Object) {
field.* = value.getObject() catch @panic("JSON value type does not match what was reported");
} else {
return JSONConvertError.IncorrectType;
}
} else {
if (value.type == .Object) {
const subObject = value.getObject() catch @panic("JSON value type does not match what was reported");
try JSONtoStruct(subObject, field_type, field);
} else {
return JSONConvertError.IncorrectType;
}
}
},
else => {
@compileLog(field_type_info);
@compileError(@typeName(field_type) ++ " is not a supported type for converting from JSON");
},
}
}
pub fn structToJSON(allocator: std.mem.Allocator, src: anytype) (JSONConvertError || JSONError)!*JSONObject {
const src_type = @TypeOf(src);
const type_info = @typeInfo(src_type);
if (type_info != .Struct) {
@compileError("structToJSON: Expected struct argument, found " ++ @typeName(src_type));
}
var object = try JSONObject.create(allocator);
errdefer object.deinit();
inline for (type_info.Struct.fields) |field| {
var field_value = @field(src, field.name);
const value = try convertStructFieldtoJSON(allocator, field.type, &field_value);
errdefer value.deinit();
const name = try allocator.dupe(u8, field.name);
errdefer allocator.free(name);
try object.children.put(name, value);
}
return object;
}
fn convertStructFieldtoJSON(allocator: std.mem.Allocator, field_type: type, value: *field_type) (JSONConvertError || JSONError)!*JSONValue {
const field_type_info = @typeInfo(field_type);
switch (field_type_info) {
.Int => {
return try JSONIntValue.create(allocator, value.*);
},
.Float => {
return try JSONFloatValue.create(allocator, value.*);
},
.Bool => {
return try JSONBoolValue.create(allocator, value.*);
},
.Optional => {
if (value.* == null) {
return try JSONValue.createNull(allocator);
} else {
return convertStructFieldtoJSON(allocator, field_type_info.Optional.child, &value.*.?);
}
},
.Struct => {
if (field_type == JSONObject) {
return try JSONObjectValue.create(allocator, value);
} else {
const obj = try structToJSON(allocator, value.*);
errdefer obj.deinit();
return try JSONObjectValue.create(allocator, obj);
}
},
.Array => {
var values = std.ArrayList(*JSONValue).init(allocator);
errdefer {
for (values.items) |val| val.deinit();
values.deinit();
}
for (0..field_type_info.Array.len) |i| {
const json_value = try convertStructFieldtoJSON(allocator, field_type_info.Array.child, &value.*[i]);
errdefer json_value.deinit();
try values.append(json_value);
}
const values_slice = try values.toOwnedSlice();
errdefer allocator.free(values_slice);
return try JSONArrayValue.create(allocator, values_slice);
},
.Pointer => {
if (field_type_info.Pointer.size == .Slice) {
if (field_type_info.Pointer.child == u8) {
const string = try allocator.dupe(u8, value.*);
errdefer allocator.free(string);
return try JSONStringValue.create(allocator, string);
} else {
var values = std.ArrayList(*JSONValue).init(allocator);
errdefer {
for (values.items) |val| val.deinit();
values.deinit();
}
for (0..value.*.len) |i| {
const json_value = try convertStructFieldtoJSON(allocator, field_type_info.Pointer.child, &value.*[i]);
errdefer json_value.deinit();
try values.append(json_value);
}
const values_slice = try values.toOwnedSlice();
errdefer allocator.free(values_slice);
return try JSONArrayValue.create(allocator, values_slice);
}
} else {
return JSONConvertError.UnsupportedType;
}
},
else => @compileError("Unsupported type: " ++ @typeName(field_type)),
}
}
test "types" {
var root = try parseString(std.testing.allocator, "{\"string\": \"abc\", \"int\": 12, \"float\": 3.4, \"array\": [], \"object\": {}, \"null\": null, \"bool\": true}");
defer root.deinit();

View File

@ -1,6 +1,21 @@
const std = @import("std");
const json = @import("json.zig");
const TestStruct = struct {
hello: []const u8,
item2: struct {
subitem: isize,
otheritem: f64,
subobject: struct {
subsubitem: [4][]const u8,
},
},
array: [5]isize,
online: bool,
active: bool,
null: ?isize,
};
pub fn main() void {
if (std.os.argv.len != 2) {
std.debug.print("Usage: {s} [file.json]\n", .{std.os.argv[0]});
@ -30,11 +45,33 @@ pub fn main() void {
const stdout = std.io.getStdOut();
const writer = stdout.writer();
root.print(writer, .{}, 0) catch {};
root.write(writer, .{}, 0) catch {};
std.debug.print("\n", .{});
const json_str = root.toString(allocator, .{}, 0) catch return;
std.debug.print("{s}\n", .{json_str});
var result = json.JSONtoNewStruct(root, TestStruct) catch |err| {
std.debug.print("Failed to convert JSON to struct: {any}\n", .{err});
return;
};
std.debug.print("hello: {s}\n", .{result.hello});
std.debug.print("subitem: {d}\n", .{result.item2.subitem});
std.debug.print("otheritem: {d}\n", .{result.item2.otheritem});
std.debug.print("subsubitem: {any}\n", .{result.item2.subobject.subsubitem});
std.debug.print("array: {any}\n", .{result.array});
std.debug.print("online: {}\n", .{result.online});
std.debug.print("active: {}\n", .{result.active});
std.debug.print("null: {?}\n", .{result.null});
result.hello = "potato";
result.item2.subitem = -25;
const back2JSON = json.structToJSON(allocator, result) catch |err| {
std.debug.print("Failed to convert struct back to JSON: {any}\n", .{err});
return;
};
back2JSON.write(writer, .{}, 0) catch return;
}
fn printIndent(indent: usize) void {