const std = @import("std"); const mem = std.mem; const Language = @import("language.zig"); const Tokenizer = @import("tokenizer.zig"); const assert = std.debug.assert; const Self = @This(); pub const Error = error{TypeError}; language: *Language, tokenizer: *Tokenizer, pub fn parse(self: *Self, allocator: mem.Allocator) !usize { return self.language.parse(allocator, self.tokenizer); } pub fn init(allocator: mem.Allocator, text: []const u8) !Self { const self = Self{ .language = try allocator.create(Language), .tokenizer = try allocator.create(Tokenizer), }; self.language.* = .init; self.tokenizer.* = try .init(allocator, text); return self; } pub fn deinit(self: *Self, allocator: mem.Allocator) void { self.language.deinit(allocator); self.tokenizer.deinit(allocator); allocator.destroy(self.language); allocator.destroy(self.tokenizer); } /// needs an index but useful otherwise /// root starting from idx pub fn reflectT(self: *Self, comptime T: type, allocator: mem.Allocator, idx: usize) !T { const Schema = @typeInfo(T); const flags = self.language.options.flags; if (std.meta.hasFn(T, "toJson")) { return T.toJson(self, allocator, idx); } switch (self.language.index.get(idx)) { .null => { if (Schema == .null) return null; return error.TypeError; }, .bool => |b| switch (Schema) { .bool => return b, .@"union" => |unionInfo| inline for (unionInfo.fields) |field| { var r: T = undefined; r = @unionInit(T, field.name, b); return r; }, else => return error.TypeError, }, .number => |number| switch (Schema) { .int, .comptime_int => return @intCast(number.int), .float, .comptime_float => return switch (number) { .float => @floatCast(number.float), .int => @floatFromInt(number.int), }, .@"enum" => |enumInfo| { const int: enumInfo.tag_type = @intCast(number.int); return @enumFromInt(int); }, .@"struct" => |structInfo| switch (structInfo.layout) { .@"packed" => { const int: structInfo.backing_integer.? = @intCast(number.int); return @bitCast(int); }, else => return error.TypeError, }, .@"union" => |unionInfo| { inline for (unionInfo.fields) |field| switch (@typeInfo(field.type)) { .int, .comptime_int => return @unionInit(T, field.name, @intCast(number.int)), .float, .comptime_float => return @unionInit(T, field.name, @floatCast(number.float)), else => {}, }; return error.TypeError; }, else => unreachable, }, .string => |string| switch (Schema) { .@"enum" => |enumInfo| { const strslice = string.slice(&self.language.strings); inline for (enumInfo.fields) |field| if (mem.eql(u8, field.name, strslice)) { return std.meta.stringToEnum(T, strslice) orelse error.TypeError; }; }, .@"union" => |unionInfo| inline for (unionInfo.fields) |field| { if (field.type == []const u8) { var r: T = undefined; const strslice = string.slice(&self.language.strings); r = @unionInit(T, field.name, strslice); return r; } }, .array => |arrayInfo| { assert(arrayInfo.child == u8); const strslice = string.slice(&self.language.strings); assert(arrayInfo.len == strslice.len - 1); var r: T = undefined; for (strslice, 0..) |char, i| r[i] = char; return r; }, .pointer => |ptrInfo| switch (ptrInfo.size) { .slice => { assert(ptrInfo.child == u8); const strslice = string.slice(&self.language.strings); var arraylist: std.ArrayList(u8) = .init(allocator); try arraylist.ensureUnusedCapacity(strslice.len); for (strslice) |char| if (char != 0x00) arraylist.appendAssumeCapacity(char); if (ptrInfo.sentinel_ptr) |some| { const sentinel = @as(*align(1) const ptrInfo.child, @ptrCast(some)).*; return try arraylist.toOwnedSliceSentinel(sentinel); } if (ptrInfo.is_const) { arraylist.deinit(); return strslice; } else { arraylist.deinit(); const slice = try allocator.dupe(u8, strslice); return @as(T, slice); } return try arraylist.toOwnedSlice(); }, else => return error.TypeError, }, else => return error.TypeError, }, .array => |slice| switch (Schema) { .array => |arrayInfo| { assert(slice.len == arrayInfo.len); var r: T = undefined; for (0..slice.len) |i| r[i] = try self.reflectT(arrayInfo.child, allocator, slice.tip + i); return r; }, .pointer => |ptrInfo| switch (ptrInfo.size) { .slice => {}, else => return error.TypeError, }, else => return error.TypeError, }, .object => |object| switch (Schema) { .@"struct" => |structInfo| { if (structInfo.is_tuple) return error.TypeError; var tip = object.tip; var map: std.StringArrayHashMapUnmanaged(usize) = .empty; try map.ensureTotalCapacity(allocator, object.len); defer map.deinit(allocator); for (0..object.len) |_| if (self.language.property_map.get(tip)) |pen| { const key = pen.tip.slice(&self.language.properties); map.putAssumeCapacity(key, tip); tip += self.language.skipSlots(tip); }; var r: T = undefined; inline for (structInfo.fields) |field| { if (field.is_comptime) @panic(@typeName(T) ++ "." ++ field.name ++ " may not be a comptime field"); if (map.get(field.name)) |next_i| { @field(r, field.name) = try self.reflectT(field.type, allocator, next_i); } else switch (@typeInfo(field.type)) { .optional => { if (flags.bitfields) @field(r, field.name) = null; @panic("Unknown property: " ++ field.name); }, else => @panic("Unknown property: " ++ field.name), } } return r; }, else => return error.TypeError, }, } unreachable; } test reflectT { const allocator = std.testing.allocator; const text = \\{ \\ "age": 15, \\ "name": "Yuzu", \\ "admin": true, \\ "flags": 0, \\ "union": ":D", \\ "enum": "world" \\} ; var self = try allocator.create(Self); self.* = try init(allocator, text); defer allocator.destroy(self); defer self.deinit(allocator); const idx: usize = try self.parse(allocator); const UserFlags = packed struct { is_cool: bool = false, is_friendly: bool = false, }; const UserSchema = struct { age: f64, name: []const u8, admin: bool, flags: UserFlags, @"union": union { hi: bool, bye: f64, n128: []const u8 }, @"enum": enum { hello, world }, }; const root = try self.reflectT(UserSchema, allocator, idx); std.debug.print("hello? {s}\n", .{@tagName(root.@"enum")}); std.debug.print("friend? {s}\n", .{root.@"union".n128}); }