Initial commit
This commit is contained in:
commit
f986fc76ec
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
zig-out/
|
||||
zig-cache/
|
||||
.zig-cache/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
96
build.zig
Normal file
96
build.zig
Normal file
@ -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);
|
||||
}
|
72
build.zig.zon
Normal file
72
build.zig.zon
Normal file
@ -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 <url>`, 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 <url>` 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",
|
||||
},
|
||||
}
|
894
src/json.zig
Normal file
894
src/json.zig
Normal file
@ -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();
|
||||
}
|
98
src/main.zig
Normal file
98
src/main.zig
Normal file
@ -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", .{}),
|
||||
}
|
||||
}
|
10
src/root.zig
Normal file
10
src/root.zig
Normal file
@ -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);
|
||||
}
|
14
test/test.json
Normal file
14
test/test.json
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user