fig 1.0.0

Parse, edit, and convert config files while preserving comments. Supports JSON, YAML, TOML, and more.
//! Editor tests for the JSON family (`Editor(json.Language)`) — both the generic
//! span-splice engine (plain JSON) and the JSON5 dialect.
//!
//! Like `toml/` and `yaml/editor_helper.zig`, each language's editor tests live
//! next to that language's concerns rather than in `editor.zig`. JSON needs no
//! format-specific editing logic (it routes straight through the generic engine),
//! so this module is tests only — they double as the engine's own smoke tests.

const std = @import("std");

const AST = @import("../ast/ast.zig");
const editor = @import("../editor.zig");
const json = @import("json.zig");
const log = std.log.scoped(.editor);

fn testEditor(input: []const u8, path: []const AST.PathSegment, key_or_val: enum { key, val }, text: []const u8, expected: []const u8) !void {
    var ed: editor.Editor(json.Language) = .{ .allocator = std.testing.allocator };
    try ed.init(input);
    defer ed.deinit();
    switch (key_or_val) {
        .key => try ed.replaceKeyAtPath(path, text),
        .val => try ed.replaceValAtPath(path, text),
    }
    const actual = ed.source.items;
    errdefer log.err("actual: {s}", .{actual});
    errdefer log.err("expected: {s}", .{expected});
    try std.testing.expect(std.mem.eql(u8, expected, actual));
}

test "simple value edit" {
    try testEditor(
        "[{\"hello\":\"world\"}]",
        &[_]AST.PathSegment{ .{ .index = 0 }, .{ .key = "hello" } },
        .val,
        "\"person!\"",
        "[{\"hello\":\"person!\"}]",
    );
}

test "simple key edit" {
    try testEditor("[{\"hello\":\"world\"}]", &[_]AST.PathSegment{ .{ .index = 0 }, .{ .key = "hello" } }, .key, "\"greetings\"", "[{\"greetings\":\"world\"}]");
}

// --- JSON5 editing ---
//
// JSON5 is a dialect of the JSON language, so it routes through the same generic
// editor. The editor splices source bytes in place (it never reprints), so every
// JSON5-ism outside the edited span — unquoted keys, trailing commas, single
// quotes, `//` and `/* */` comments — survives byte-for-byte. The owned-comment
// scan on delete is `//`/`/* */`-aware so a deleted key carries its own comment.

fn newJson5Editor(input: []const u8) !editor.Editor(json.Language) {
    var ed: editor.Editor(json.Language) = .{ .allocator = std.testing.allocator, .format = .JSON5 };
    try ed.init(input);
    return ed;
}

fn expectJson5Source(ed: *const editor.Editor(json.Language), expected: []const u8) !void {
    errdefer log.err("actual:   \"{s}\"", .{ed.source.items});
    errdefer log.err("expected: \"{s}\"", .{expected});
    try std.testing.expectEqualStrings(expected, ed.source.items);
}

test "json5 value edit preserves unquoted keys, comments, trailing comma" {
    var ed = try newJson5Editor(
        \\{
        \\  // server config
        \\  host: 'localhost',
        \\  port: 8080, // default
        \\}
    );
    defer ed.deinit();
    try ed.replaceValAtPath(&.{.{ .key = "port" }}, "9090");
    try expectJson5Source(&ed,
        \\{
        \\  // server config
        \\  host: 'localhost',
        \\  port: 9090, // default
        \\}
    );
}

test "json5 key rename keeps it unquoted" {
    var ed = try newJson5Editor("{ host: 'localhost', port: 8080 }");
    defer ed.deinit();
    try ed.replaceKeyAtPath(&.{.{ .key = "port" }}, "listen");
    try expectJson5Source(&ed, "{ host: 'localhost', listen: 8080 }");
}

test "json5 delete key carries its owned // comment" {
    var ed = try newJson5Editor(
        \\{
        \\  host: 'localhost',
        \\  // the listening port
        \\  port: 8080,
        \\}
    );
    defer ed.deinit();
    try ed.deleteKey(&.{.{ .key = "port" }});
    try expectJson5Source(&ed,
        \\{
        \\  host: 'localhost',
        \\}
    );
}

test "json5 delete key carries an owned /* */ block comment" {
    var ed = try newJson5Editor(
        \\{
        \\  host: 'localhost',
        \\  /* the listening
        \\     port number */
        \\  port: 8080,
        \\}
    );
    defer ed.deinit();
    try ed.deleteKey(&.{.{ .key = "port" }});
    try expectJson5Source(&ed,
        \\{
        \\  host: 'localhost',
        \\}
    );
}

test "json5 delete leaves an unrelated earlier comment intact" {
    var ed = try newJson5Editor(
        \\{
        \\  // host comment
        \\  host: 'localhost',
        \\  port: 8080,
        \\}
    );
    defer ed.deinit();
    try ed.deleteKey(&.{.{ .key = "port" }});
    try expectJson5Source(&ed,
        \\{
        \\  // host comment
        \\  host: 'localhost',
        \\}
    );
}