fig 1.0.0

Parse, edit, and convert config files while preserving comments. Supports JSON, YAML, TOML, and more.
const std = @import("std");
const testing = std.testing;

const Parser = @import("parser.zig");
const JsonType = @import("json.zig").Type;

const max_fixture_size = 1024 * 1024;

test "json conformance: edgecase JSONTestSuite files" {
    try runConformanceDir(
        "testdata/json/edgecase",
        'i',
        .implementation_defined,
    );
}

test "json conformance: valid JSONTestSuite files" {
    try runConformanceDir(
        "testdata/json/accept",
        'y',
        .should_pass,
    );
}

test "json conformance: invalid JSONTestSuite files" {
    try runConformanceDir(
        "testdata/json/reject",
        'n',
        .should_fail,
    );
}

const Expected = enum {
    should_pass,
    should_fail,
    implementation_defined,
};

const rejected_edgecase_fixtures = [_][]const u8{
    // This parser does NOT accept UTF-16 directly.
    // Users are expected to transcode to UTF-8 first.
    "i_string_UTF-16LE_with_BOM.json",
    "i_string_utf16BE_no_BOM.json",
    "i_string_utf16LE_no_BOM.json",
};

fn shouldRejectEdgecase(name: []const u8) bool {
    for (rejected_edgecase_fixtures) |fixture| {
        if (std.mem.eql(u8, name, fixture)) return true;
    }
    return false;
}

fn runConformanceDir(dir_path: []const u8, prefix: u8, expected: Expected) !void {
    var threaded = std.Io.Threaded.init(testing.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true });
    defer dir.close(io);

    var iterator = dir.iterate();
    while (try iterator.next(io)) |entry| {
        if (entry.kind != .file) continue;
        if (entry.name.len == 0 or entry.name[0] != prefix) continue;
        if (!std.mem.endsWith(u8, entry.name, ".json")) continue;

        const input = try dir.readFileAlloc(
            io,
            entry.name,
            testing.allocator,
            .limited(max_fixture_size),
        );
        defer testing.allocator.free(input);

        switch (expected) {
            .should_pass => {
                var doc = Parser.parse(testing.allocator, input, JsonType.JSON) catch |err| {
                    std.debug.print("Expected valid JSON fixture to parse: {s}, err={any}\n", .{
                        entry.name,
                        err,
                    });
                    return err;
                };
                defer doc.deinit(testing.allocator);
            },
            .should_fail => {
                if (Parser.parse(testing.allocator, input, JsonType.JSON)) |doc| {
                    defer doc.deinit(testing.allocator);
                    std.debug.print("Expected invalid JSON fixture to fail: {s}\n", .{
                        entry.name,
                    });
                    return error.ExpectedParseFailure;
                } else |_| {}
            },
            .implementation_defined => {
                const should_reject = shouldRejectEdgecase(entry.name);
                if (Parser.parse(testing.allocator, input, JsonType.JSON)) |doc| {
                    defer doc.deinit(testing.allocator);
                    if (should_reject) {
                        std.debug.print("Expected edgecase JSON fixture to fail: {s}\n", .{
                            entry.name,
                        });
                        return error.ExpectedParseFailure;
                    }
                } else |err| {
                    if (!should_reject) {
                        std.debug.print("Expected edgecase JSON fixture to parse: {s}, err={any}\n", .{
                            entry.name,
                            err,
                        });
                        return err;
                    }
                }
            },
        }
    }
}