From 3d216fbd23545e573fc2b1c99b1d1dfefc2bb47a Mon Sep 17 00:00:00 2001 From: Cameron Reed Date: Mon, 4 Nov 2024 12:51:58 -0700 Subject: [PATCH] Allow convert from JSON to a struct and from a struct to JSON --- src/json.zig | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/main.zig | 39 ++++++++- 2 files changed, 263 insertions(+), 4 deletions(-) diff --git a/src/json.zig b/src/json.zig index 225ecb0..b9d6a14 100644 --- a/src/json.zig +++ b/src/json.zig @@ -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(); diff --git a/src/main.zig b/src/main.zig index b5eb517..6074cfa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 {