commit f986fc76ecc5cbd87d111d1cbb7178dbc03194e4 Author: Cameron Reed Date: Thu Aug 1 11:41:50 2024 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ad6b22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-out/ +zig-cache/ +.zig-cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ae6902 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Cameron Reed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..8a4992d --- /dev/null +++ b/build.zig @@ -0,0 +1,96 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "zigjson", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + const exe = b.addExecutable(.{ + .name = "zigjson", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); + + // Configure zls to run this step on save for better error checking with zls + const check_step = b.step("check", "Check compilation status"); + check_step.dependOn(&exe.step); + check_step.dependOn(&lib.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..af0d710 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "zigjson", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/json.zig b/src/json.zig new file mode 100644 index 0000000..02d5ee3 --- /dev/null +++ b/src/json.zig @@ -0,0 +1,894 @@ +const std = @import("std"); + +pub const JSONError = error{ + IncorrectType, + UnexpectedEOF, + UnexpectedCharacter, +} || std.mem.Allocator.Error; + +pub const JSONType = enum { + Object, + Array, + String, + Int, + Float, + Bool, + Null, +}; + +pub const JSONObject = struct { + allocator: std.mem.Allocator, + children: MapType, + + const MapType = std.StringHashMap(*JSONValue); + + pub fn get(self: *JSONObject, name: []const u8) ?*JSONValue { + return self.children.get(name); + } + + fn init(allocator: std.mem.Allocator) JSONError!*JSONObject { + var self = try allocator.create(JSONObject); + self.allocator = allocator; + self.children = MapType.init(allocator); + + return self; + } + + pub fn deinit(self: *JSONObject) void { + var iter = self.children.valueIterator(); + while (iter.next()) |child| { + child.*.deinit(); + } + + var names = self.children.keyIterator(); + while (names.next()) |name| { + self.allocator.free(name.*); + } + self.children.deinit(); + self.allocator.destroy(self); + } +}; + +pub const JSONValue = struct { + allocator: std.mem.Allocator, + type: JSONType, + vtable: VTable, + + const VTable = struct { + getString: *const fn (self: *const JSONValue) JSONError![]const u8 = incorrectValue([]const u8), + getArray: *const fn (self: *const JSONValue) JSONError![]*JSONValue = incorrectValue([]*JSONValue), + getObject: *const fn (self: *const JSONValue) JSONError!*JSONObject = incorrectValue(*JSONObject), + getInt: *const fn (self: *const JSONValue) JSONError!isize = incorrectValue(isize), + getFloat: *const fn (self: *const JSONValue) JSONError!f64 = incorrectValue(f64), + getBool: *const fn (self: *const JSONValue) JSONError!bool = incorrectValue(bool), + + deinit: *const fn (self: *JSONValue) void, + }; + + fn incorrectValue(comptime T: type) *const fn (*const JSONValue) JSONError!T { + return struct { + fn func(_: *const JSONValue) JSONError!T { + return JSONError.IncorrectType; + } + }.func; + } + + pub fn getBool(self: *const JSONValue) JSONError!bool { + return self.vtable.getBool(self); + } + + pub fn getInt(self: *const JSONValue) JSONError!isize { + return self.vtable.getInt(self); + } + + pub fn getFloat(self: *const JSONValue) JSONError!f64 { + return self.vtable.getFloat(self); + } + + pub fn getString(self: *const JSONValue) JSONError![]const u8 { + return self.vtable.getString(self); + } + + pub fn getArray(self: *const JSONValue) JSONError![]*JSONValue { + return self.vtable.getArray(self); + } + + pub fn getObject(self: *const JSONValue) JSONError!*JSONObject { + return self.vtable.getObject(self); + } + + pub fn deinit(self: *JSONValue) void { + self.vtable.deinit(self); + } + + fn createNull(allocator: std.mem.Allocator) JSONError!*JSONValue { + var self = try allocator.create(JSONValue); + self.type = .Null; + self.vtable = .{ .deinit = basicDeinit }; + self.allocator = allocator; + + return self; + } + + fn basicDeinit(self: *JSONValue) void { + self.allocator.destroy(self); + } +}; + +const JSONArrayValue = struct { + value: JSONValue, + array: []*JSONValue, + + fn init(allocator: std.mem.Allocator, array: []*JSONValue) !*JSONValue { + var self = try allocator.create(JSONArrayValue); + self.array = array; + self.value = .{ + .type = .Array, + .vtable = .{ .getArray = getValue, .deinit = deinit }, + .allocator = allocator, + }; + + return &self.value; + } + + fn getValue(value: *const JSONValue) JSONError![]*JSONValue { + const self: *const JSONArrayValue = @fieldParentPtr("value", value); + return self.array; + } + + fn deinit(value: *JSONValue) void { + var self: *JSONArrayValue = @fieldParentPtr("value", value); + for (self.array) |val| { + val.deinit(); + } + self.value.allocator.free(self.array); + self.value.allocator.destroy(self); + } +}; + +const JSONObjectValue = struct { + value: JSONValue, + object: *JSONObject, + + fn init(allocator: std.mem.Allocator, object: *JSONObject) !*JSONValue { + var self = try allocator.create(JSONObjectValue); + self.object = object; + self.value = .{ + .type = .Object, + .vtable = .{ .getObject = getValue, .deinit = deinit }, + .allocator = allocator, + }; + + return &self.value; + } + + fn getValue(value: *const JSONValue) JSONError!*JSONObject { + const self: *const JSONObjectValue = @fieldParentPtr("value", value); + return self.object; + } + + fn deinit(value: *JSONValue) void { + var self: *const JSONObjectValue = @fieldParentPtr("value", value); + self.object.deinit(); + self.value.allocator.destroy(self); + } +}; + +const JSONIntValue = struct { + value: JSONValue, + int: isize, + + fn init(allocator: std.mem.Allocator, value: isize) !*JSONValue { + var self = try allocator.create(JSONIntValue); + self.int = value; + self.value = .{ + .type = .Int, + .vtable = .{ .getInt = getValue, .deinit = deinit }, + .allocator = allocator, + }; + + return &self.value; + } + + fn getValue(value: *const JSONValue) JSONError!isize { + const self: *const JSONIntValue = @fieldParentPtr("value", value); + return self.int; + } + + fn deinit(value: *JSONValue) void { + var self: *JSONIntValue = @fieldParentPtr("value", value); + self.value.allocator.destroy(self); + } +}; + +const JSONFloatValue = struct { + value: JSONValue, + float: f64, + + fn init(allocator: std.mem.Allocator, value: f64) !*JSONValue { + var self = try allocator.create(JSONFloatValue); + self.float = value; + self.value = .{ + .type = .Float, + .vtable = .{ .getFloat = getValue, .deinit = deinit }, + .allocator = allocator, + }; + + return &self.value; + } + + fn getValue(value: *const JSONValue) JSONError!f64 { + const self: *const JSONFloatValue = @fieldParentPtr("value", value); + return self.float; + } + + fn deinit(value: *JSONValue) void { + var self: *JSONIntValue = @fieldParentPtr("value", value); + self.value.allocator.destroy(self); + } +}; + +const JSONBoolValue = struct { + value: JSONValue, + boolean: bool, + + fn init(allocator: std.mem.Allocator, value: bool) !*JSONValue { + var self = try allocator.create(JSONBoolValue); + self.boolean = value; + self.value = .{ + .type = .Bool, + .vtable = .{ .getBool = getValue, .deinit = deinit }, + .allocator = allocator, + }; + + return &self.value; + } + + fn getValue(value: *const JSONValue) JSONError!bool { + const self: *const JSONBoolValue = @fieldParentPtr("value", value); + return self.boolean; + } + + fn deinit(value: *JSONValue) void { + var self: *JSONBoolValue = @fieldParentPtr("value", value); + self.value.allocator.destroy(self); + } +}; + +pub const JSONStringValue = struct { + value: JSONValue, + string: []const u8, + + pub fn init(allocator: std.mem.Allocator, string: []const u8) !*JSONValue { + var self = try allocator.create(JSONStringValue); + self.string = string; + self.value = .{ + .type = .String, + .vtable = .{ .getString = getValue, .deinit = deinit }, + .allocator = allocator, + }; + + return &self.value; + } + + fn getValue(value: *const JSONValue) JSONError![]const u8 { + const self: *const JSONStringValue = @fieldParentPtr("value", value); + return self.string; + } + + fn deinit(value: *JSONValue) void { + var self: *JSONStringValue = @fieldParentPtr("value", value); + self.value.allocator.free(self.string); + self.value.allocator.destroy(self); + } +}; + +pub fn parseFile(allocator: std.mem.Allocator, fileName: []const u8) !*JSONObject { + const fileContents = blk: { + var file = try std.fs.cwd().openFile(fileName, .{ .mode = .read_only }); + defer file.close(); + break :blk try file.readToEndAlloc(allocator, 64 * 1024 * 1024); + }; + defer allocator.free(fileContents); + + var parser = try JSONParser.init(allocator, fileContents); + return parser.parse(); +} + +pub fn parseString(allocator: std.mem.Allocator, string: []const u8) !*JSONObject { + var parser = try JSONParser.init(allocator, string); + return parser.parse(); +} + +const JSONParser = struct { + allocator: std.mem.Allocator, + iter: std.unicode.Utf8Iterator, + current: u21 = 0, + end: bool = false, + + fn init(allocator: std.mem.Allocator, input: []const u8) !JSONParser { + return JSONParser{ + .allocator = allocator, + .iter = (try std.unicode.Utf8View.init(input)).iterator(), + }; + } + + fn next(self: *JSONParser) void { + if (self.iter.nextCodepoint()) |code_point| { + self.current = code_point; + } else { + self.end = true; + } + } + + fn consume(self: *JSONParser) u21 { + const code_point = self.current; + self.next(); + return code_point; + } + + fn skipWhitespace(self: *JSONParser) JSONError!void { + while (!self.end) { + switch (self.current) { + ' ', '\t', '\n', '\r' => {}, + else => return, + } + self.next(); + } + + return JSONError.UnexpectedEOF; + } + + fn expectNext(self: *JSONParser, code_point: u21) JSONError!void { + //std.debug.print("Expecting {u} ({X}) to be {u} ({X})\n", .{ self.current, self.current, code_point, code_point }); + if (self.current != code_point or self.end) { + return JSONError.UnexpectedCharacter; + } + self.next(); + } + + fn parse(self: *JSONParser) JSONError!*JSONObject { + self.next(); + try self.skipWhitespace(); + return self.parseObject(); + } + + fn parseObject(self: *JSONParser) JSONError!*JSONObject { + const obj = try JSONObject.init(self.allocator); + errdefer obj.deinit(); + + try self.expectNext('{'); + + try self.skipWhitespace(); + + if (self.current != '}') { + while (true) { + try self.skipWhitespace(); + const valueName = try self.parseString(); + errdefer self.allocator.free(valueName); + try self.skipWhitespace(); + + //std.debug.print("Value name: {s}\n", .{valueName}); + + try self.expectNext(':'); + + const value = try self.parseValue(); + errdefer value.deinit(); + + try obj.children.put(valueName, value); + + // Do-while would have been better here + if (self.current != ',') { + break; + } + self.next(); + } + } + + try self.expectNext('}'); + + return obj; + } + + fn parseArray(self: *JSONParser) JSONError![]*JSONValue { + var array = std.ArrayList(*JSONValue).init(self.allocator); + errdefer { + for (array.items) |value| { + value.deinit(); + } + array.deinit(); + } + + try self.expectNext('['); + + try self.skipWhitespace(); + + if (self.current != ']') { + while (true) { + try array.append(try self.parseValue()); + if (self.current != ',') { + break; + } + self.next(); + } + } + + try self.expectNext(']'); + + return array.toOwnedSlice(); + } + + fn parseValue(self: *JSONParser) JSONError!*JSONValue { + try self.skipWhitespace(); + + var value: *JSONValue = undefined; + + switch (self.current) { + '"' => { + //std.debug.print("Parsing string\n", .{}); + const str = try self.parseString(); + value = try JSONStringValue.init(self.allocator, str); + }, + '-', '0'...'9' => { + //std.debug.print("Parsing number\n", .{}); + value = try self.parseNumberValue(); + }, + '{' => { + //std.debug.print("Parsing object\n", .{}); + const obj = try self.parseObject(); + value = try JSONObjectValue.init(self.allocator, obj); + }, + '[' => { + //std.debug.print("Parsing array\n", .{}); + const arr = try self.parseArray(); + value = try JSONArrayValue.init(self.allocator, arr); + }, + 't', 'f' => { + //std.debug.print("Parsing bool\n", .{}); + const b = try self.parseBool(); + value = try JSONBoolValue.init(self.allocator, b); + }, + 'n' => { + //std.debug.print("Parsing null\n", .{}); + self.next(); + try self.expectNext('u'); + try self.expectNext('l'); + try self.expectNext('l'); + + value = try JSONValue.createNull(self.allocator); + }, + else => { + std.debug.print("Unexpected character: {u}\n", .{self.current}); + return JSONError.UnexpectedCharacter; + }, + } + + self.skipWhitespace() catch |err| { + value.deinit(); + return err; + }; + + return value; + } + + fn parseBool(self: *JSONParser) JSONError!bool { + var b = false; + + switch (self.current) { + 't' => { + self.next(); + try self.expectNext('r'); + try self.expectNext('u'); + try self.expectNext('e'); + + b = true; + }, + 'f' => { + self.next(); + try self.expectNext('a'); + try self.expectNext('l'); + try self.expectNext('s'); + try self.expectNext('e'); + + b = false; + }, + else => return JSONError.UnexpectedCharacter, + } + + return b; + } + + fn parseString(self: *JSONParser) JSONError![]const u8 { + var string = std.ArrayList(u8).init(self.allocator); + defer string.deinit(); + + try self.expectNext('"'); + + while (self.current != '"' and !self.end) { + switch (self.consume()) { + '\\' => { + if (self.end) return JSONError.UnexpectedEOF; + const code_point = try self.escapedCharacter(); + var utf8: [4]u8 = undefined; + const len = std.unicode.utf8Encode(code_point, &utf8) catch return JSONError.UnexpectedCharacter; + try string.appendSlice(utf8[0..len]); + }, + 0...std.ascii.control_code.us, std.ascii.control_code.del => return JSONError.UnexpectedCharacter, + else => |c| { + var buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(c, &buf) catch return JSONError.UnexpectedCharacter; + try string.appendSlice(buf[0..len]); + }, + } + } + + try self.expectNext('"'); + + return string.toOwnedSlice(); + } + + fn escapedCharacter(self: *JSONParser) JSONError!u21 { + return switch (self.consume()) { + '"' => '"', + '\\' => '\\', + '/' => '/', + 'b' => '\x08', + 'f' => '\x0C', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'u' => { + // I hate this + if (self.end) return JSONError.UnexpectedEOF; + const c1 = self.consume(); + var b1: [1]u8 = undefined; + _ = std.unicode.utf8Encode(c1, &b1) catch return JSONError.UnexpectedCharacter; + if (self.end) return JSONError.UnexpectedEOF; + const c2 = self.consume(); + var b2: [1]u8 = undefined; + _ = std.unicode.utf8Encode(c2, &b2) catch return JSONError.UnexpectedCharacter; + if (self.end) return JSONError.UnexpectedEOF; + const c3 = self.consume(); + var b3: [1]u8 = undefined; + _ = std.unicode.utf8Encode(c3, &b3) catch return JSONError.UnexpectedCharacter; + if (self.end) return JSONError.UnexpectedEOF; + const c4 = self.consume(); + var b4: [1]u8 = undefined; + _ = std.unicode.utf8Encode(c4, &b4) catch return JSONError.UnexpectedCharacter; + + return std.fmt.parseInt(u21, &[4]u8{ b1[0], b2[0], b3[0], b4[0] }, 16) catch JSONError.UnexpectedCharacter; + }, + else => JSONError.UnexpectedCharacter, + }; + } + + fn parseNumberValue(self: *JSONParser) JSONError!*JSONValue { + var buf = std.ArrayList(u8).init(self.allocator); + defer buf.deinit(); + var float = false; + var exp: isize = 0; + + if (self.current == '-') { + self.next(); + try buf.append('-'); + } + + if (self.end) { + return JSONError.UnexpectedEOF; + } + + if (self.current != '0') { + while (!self.end) { + switch (self.current) { + '0'...'9' => |c| { + try buf.append(@intCast(c)); + }, + else => break, + } + self.next(); + } + + if (self.current == '.') { + try buf.append('.'); + float = true; + self.next(); + } + } else { + try self.expectNext('.'); + try buf.append('.'); + float = true; + } + + if (float) { + while (!self.end) { + switch (self.current) { + '0'...'9' => |c| { + try buf.append(@intCast(c)); + }, + else => break, + } + self.next(); + } + } + + if (self.current == 'e' or self.current == 'E') { + self.next(); + + var expBuf = std.ArrayList(u8).init(self.allocator); + defer expBuf.deinit(); + + while (!self.end) { + switch (self.current) { + '-' => try expBuf.append('-'), + '0'...'9' => |c| try expBuf.append(@intCast(c)), + else => break, + } + self.next(); + } + + if (expBuf.items.len == 0) { + return JSONError.UnexpectedCharacter; + } + + exp = std.fmt.parseInt(isize, expBuf.items, 10) catch return JSONError.UnexpectedCharacter; + } + + if (float or exp != 0) { + var value: f64 = std.fmt.parseFloat(f64, buf.items) catch unreachable; + + if (exp != 0) { + value *= std.math.pow(f64, 10.0, @floatFromInt(exp)); + } + + return try JSONFloatValue.init(self.allocator, value); + } else { + const value: isize = std.fmt.parseInt(isize, buf.items, 10) catch unreachable; + return try JSONIntValue.init(self.allocator, value); + } + } +}; + +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(); + + if (root.get("string")) |value| { + try std.testing.expect(value.type == .String); + } else { + return error.FailedToParse; + } + + if (root.get("int")) |value| { + try std.testing.expect(value.type == .Int); + } else { + return error.FailedToParse; + } + + if (root.get("float")) |value| { + try std.testing.expect(value.type == .Float); + } else { + return error.FailedToParse; + } + + if (root.get("array")) |value| { + try std.testing.expect(value.type == .Array); + } else { + return error.FailedToParse; + } + + if (root.get("object")) |value| { + try std.testing.expect(value.type == .Object); + } else { + return error.FailedToParse; + } + + if (root.get("null")) |value| { + try std.testing.expect(value.type == .Null); + } else { + return error.FailedToParse; + } + + if (root.get("bool")) |value| { + try std.testing.expect(value.type == .Bool); + } else { + return error.FailedToParse; + } +} + +test "ints" { + const root = try parseString(std.testing.allocator, "{\"one\": 1, \"two\": 2, \"three\": 3, \"fifteen\": 15, \"negative\": -15}"); + defer root.deinit(); + + if (root.get("one")) |value| { + try std.testing.expect(value.type == .Int); + try std.testing.expect(try value.getInt() == 1); + } else { + return error.FailedToParse; + } + + if (root.get("two")) |value| { + try std.testing.expect(value.type == .Int); + try std.testing.expect(try value.getInt() == 2); + } else { + return error.FailedToParse; + } + + if (root.get("three")) |value| { + try std.testing.expect(value.type == .Int); + try std.testing.expect(try value.getInt() == 3); + } else { + return error.FailedToParse; + } + + if (root.get("fifteen")) |value| { + try std.testing.expect(value.type == .Int); + try std.testing.expect(try value.getInt() == 15); + } else { + return error.FailedToParse; + } + + if (root.get("negative")) |value| { + try std.testing.expect(value.type == .Int); + try std.testing.expect(try value.getInt() == -15); + } else { + return error.FailedToParse; + } +} + +test "floats" { + const root = try parseString(std.testing.allocator, "{\"one\": 1.2, \"ten\": 1e1, \"three\": -3.4, \"four\": 3.4e12, \"five\": 1e-2}"); + defer root.deinit(); + + if (root.get("one")) |value| { + try std.testing.expect(value.type == .Float); + try std.testing.expect(try value.getFloat() - 1.2 < 0.1); + } else { + return error.FailedToParse; + } + + if (root.get("ten")) |value| { + try std.testing.expect(value.type == .Float); + try std.testing.expect(try value.getFloat() - 10 < 0.1); + } else { + return error.FailedToParse; + } + + if (root.get("three")) |value| { + try std.testing.expect(value.type == .Float); + try std.testing.expect(try value.getFloat() - -3.4 < 0.1); + } else { + return error.FailedToParse; + } + + if (root.get("four")) |value| { + try std.testing.expect(value.type == .Float); + try std.testing.expect(try value.getFloat() - 3400000000000 < 0.1); + } else { + return error.FailedToParse; + } + + if (root.get("five")) |value| { + try std.testing.expect(value.type == .Float); + try std.testing.expect(try value.getFloat() - 0.01 < 0.001); + } else { + return error.FailedToParse; + } +} + +test "bool" { + const root = try parseString(std.testing.allocator, "{\"true\": true, \"false\": false}"); + defer root.deinit(); + + if (root.get("true")) |value| { + try std.testing.expect(value.type == .Bool); + try std.testing.expect(try value.getBool()); + } else { + return error.FailedToParse; + } + + if (root.get("false")) |value| { + try std.testing.expect(value.type == .Bool); + try std.testing.expect(!try value.getBool()); + } else { + return error.FailedToParse; + } +} + +test "string" { + const root = try parseString(std.testing.allocator, "{\"one\": \"abc 123\\r\\t\\n\\u2611 \\\" \\\\ \\/\"}"); + defer root.deinit(); + + if (root.get("one")) |value| { + try std.testing.expect(value.type == .String); + try std.testing.expectEqualStrings("abc 123\r\t\n☑ \" \\ /", try value.getString()); + } else { + return error.FailedToParse; + } +} + +test "null" { + const root = try parseString(std.testing.allocator, "{\"null\": null}"); + defer root.deinit(); + + if (root.get("null")) |value| { + try std.testing.expect(value.type == .Null); + } else { + return error.FailedToParse; + } +} + +test "array" { + const root = try parseString(std.testing.allocator, "{\"empty\": [], \"two\": [1, 2], \"three\": [ 3.1, 3.2, 3.3 ]}"); + defer root.deinit(); + + if (root.get("empty")) |value| { + try std.testing.expect(value.type == .Array); + try std.testing.expect((try value.getArray()).len == 0); + } else { + return error.FailedToParse; + } + + if (root.get("two")) |value| { + try std.testing.expect(value.type == .Array); + const arr = try value.getArray(); + try std.testing.expect(arr.len == 2); + + try std.testing.expect(arr[0].type == .Int); + try std.testing.expect(try arr[0].getInt() == 1); + + try std.testing.expect(arr[1].type == .Int); + try std.testing.expect(try arr[1].getInt() == 2); + } else { + return error.FailedToParse; + } + + if (root.get("three")) |value| { + try std.testing.expect(value.type == .Array); + const arr = try value.getArray(); + try std.testing.expect(arr.len == 3); + + try std.testing.expect(arr[0].type == .Float); + try std.testing.expect(try arr[0].getFloat() - 3.1 < 0.1); + + try std.testing.expect(arr[1].type == .Float); + try std.testing.expect(try arr[1].getFloat() - 3.2 < 0.1); + + try std.testing.expect(arr[2].type == .Float); + try std.testing.expect(try arr[2].getFloat() - 3.3 < 0.1); + } else { + return error.FailedToParse; + } +} + +test "objects" { + const root = try parseString(std.testing.allocator, "{\"empty\": {}, \"two\": {\"abc\": 1}}"); + defer root.deinit(); + + if (root.get("empty")) |value| { + try std.testing.expect(value.type == .Object); + const obj = try value.getObject(); + try std.testing.expect(obj.children.count() == 0); + } else { + return error.FailedToParse; + } + + if (root.get("two")) |value| { + try std.testing.expect(value.type == .Object); + const obj = try value.getObject(); + try std.testing.expect(obj.children.count() == 1); + + if (obj.get("abc")) |value2| { + try std.testing.expect(value2.type == .Int); + try std.testing.expect(try value2.getInt() == 1); + } else { + return error.FailedToParse; + } + } else { + return error.FailedToParse; + } +} + +test "file parsing" { + const root = try parseFile(std.testing.allocator, "test/test.json"); + root.deinit(); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..1bfecf7 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const json = @import("json.zig"); + +pub fn main() void { + if (std.os.argv.len != 2) { + std.debug.print("Usage: {s} [file.json]\n", .{std.os.argv[0]}); + std.process.exit(1); + } + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer { + if (gpa.deinit() == .leak) { + std.debug.print("Memory was leaked D:\n", .{}); + } + } + + const fileName = std.mem.span(std.os.argv[1]); + + const root = json.parseFile(allocator, fileName) catch |err| { + std.debug.print("Failed to parse json: {any}\n", .{err}); + std.process.exit(2); + }; + defer root.deinit(); + + printObject(root, 0); + std.debug.print("\n", .{}); +} + +fn printIndent(indent: usize) void { + for (0..indent) |_| { + std.debug.print(" ", .{}); + } +} + +fn printObject(obj: *json.JSONObject, indent: usize) void { + std.debug.print("{{\n", .{}); + + var first = true; + var iter = obj.children.keyIterator(); + while (iter.next()) |key| { + if (!first) { + std.debug.print(",\n", .{}); + } + printIndent(indent + 1); + std.debug.print("\"{s}\": ", .{key.*}); + printValue(obj.get(key.*).?, indent + 1); + first = false; + } + + std.debug.print("\n", .{}); + printIndent(indent); + std.debug.print("}}", .{}); +} + +fn printArray(arr: []*json.JSONValue, indent: usize) void { + std.debug.print("[ ", .{}); + + var first = true; + for (arr) |value| { + if (!first) { + std.debug.print(", ", .{}); + } + + printValue(value, indent + 1); + first = false; + } + + std.debug.print(" ]", .{}); +} + +fn printString(string: []const u8) void { + std.debug.print("\"{s}\"", .{string}); +} + +fn printBool(b: bool) void { + std.debug.print("{s}", .{if (b) "true" else "false"}); +} + +fn printInt(int: isize) void { + std.debug.print("{d}", .{int}); +} + +fn printFloat(float: f64) void { + std.debug.print("{d}", .{float}); +} + +fn printValue(value: *json.JSONValue, indent: usize) void { + switch (value.type) { + .Object => printObject(value.getObject() catch unreachable, indent), + .Array => printArray(value.getArray() catch unreachable, indent), + .String => printString(value.getString() catch unreachable), + .Bool => printBool(value.getBool() catch unreachable), + .Int => printInt(value.getInt() catch unreachable), + .Float => printFloat(value.getFloat() catch unreachable), + .Null => std.debug.print("null", .{}), + } +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..ecfeade --- /dev/null +++ b/src/root.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const testing = std.testing; + +export fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try testing.expect(add(3, 7) == 10); +} diff --git a/test/test.json b/test/test.json new file mode 100644 index 0000000..e45ba7b --- /dev/null +++ b/test/test.json @@ -0,0 +1,14 @@ +{ + "hello": "world", + "item2": { + "subitem": 3, + "otheritem": 12.7, + "subobject": { + "subsubitem": [ "a", "b", "c", "127" ] + } + }, + "array": [ 4,5, 6 , 8, 20 ], + "online": true, + "active": false, + "null": null +}