mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Merge pull request #502 from lightpanda-io/cdp_node_children
Cdp node children
This commit is contained in:
328
src/cdp/Node.zig
328
src/cdp/Node.zig
@@ -22,79 +22,19 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Id = u32;
|
||||
|
||||
const log = std.log.scoped(.cdp_node);
|
||||
|
||||
const Node = @This();
|
||||
|
||||
id: Id,
|
||||
parent_id: ?Id = null,
|
||||
node_type: u32,
|
||||
backend_node_id: Id,
|
||||
node_name: []const u8,
|
||||
local_name: []const u8,
|
||||
node_value: []const u8,
|
||||
child_node_count: u32,
|
||||
children: []const *Node,
|
||||
document_url: ?[]const u8,
|
||||
base_url: ?[]const u8,
|
||||
xml_version: []const u8,
|
||||
compatibility_mode: CompatibilityMode,
|
||||
is_scrollable: bool,
|
||||
_node: *parser.Node,
|
||||
|
||||
const CompatibilityMode = enum {
|
||||
NoQuirksMode,
|
||||
};
|
||||
|
||||
pub fn jsonStringify(self: *const Node, writer: anytype) !void {
|
||||
try writer.beginObject();
|
||||
try writer.objectField("nodeId");
|
||||
try writer.write(self.id);
|
||||
|
||||
try writer.objectField("parentId");
|
||||
try writer.write(self.parent_id);
|
||||
|
||||
try writer.objectField("backendNodeId");
|
||||
try writer.write(self.backend_node_id);
|
||||
|
||||
try writer.objectField("nodeType");
|
||||
try writer.write(self.node_type);
|
||||
|
||||
try writer.objectField("nodeName");
|
||||
try writer.write(self.node_name);
|
||||
|
||||
try writer.objectField("localName");
|
||||
try writer.write(self.local_name);
|
||||
|
||||
try writer.objectField("nodeValue");
|
||||
try writer.write(self.node_value);
|
||||
|
||||
try writer.objectField("childNodeCount");
|
||||
try writer.write(self.child_node_count);
|
||||
|
||||
try writer.objectField("children");
|
||||
try writer.write(self.children);
|
||||
|
||||
try writer.objectField("documentURL");
|
||||
try writer.write(self.document_url);
|
||||
|
||||
try writer.objectField("baseURL");
|
||||
try writer.write(self.base_url);
|
||||
|
||||
try writer.objectField("xmlVersion");
|
||||
try writer.write(self.xml_version);
|
||||
|
||||
try writer.objectField("compatibilityMode");
|
||||
try writer.write(self.compatibility_mode);
|
||||
|
||||
try writer.objectField("isScrollable");
|
||||
try writer.write(self.is_scrollable);
|
||||
try writer.endObject();
|
||||
}
|
||||
|
||||
// Whenever we send a node to the client, we register it here for future lookup.
|
||||
// We maintain a node -> id and id -> node lookup.
|
||||
pub const Registry = struct {
|
||||
node_id: u32,
|
||||
allocator: Allocator,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
node_pool: std.heap.MemoryPool(Node),
|
||||
lookup_by_id: std.AutoHashMapUnmanaged(Id, *Node),
|
||||
lookup_by_node: std.HashMapUnmanaged(*parser.Node, *Node, NodeContext, std.hash_map.default_max_load_percentage),
|
||||
@@ -102,9 +42,10 @@ pub const Registry = struct {
|
||||
pub fn init(allocator: Allocator) Registry {
|
||||
return .{
|
||||
.node_id = 0,
|
||||
.allocator = allocator,
|
||||
.lookup_by_id = .{},
|
||||
.lookup_by_node = .{},
|
||||
.allocator = allocator,
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.node_pool = std.heap.MemoryPool(Node).init(allocator),
|
||||
};
|
||||
}
|
||||
@@ -114,12 +55,14 @@ pub const Registry = struct {
|
||||
self.lookup_by_id.deinit(allocator);
|
||||
self.lookup_by_node.deinit(allocator);
|
||||
self.node_pool.deinit();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *Registry) void {
|
||||
self.lookup_by_id.clearRetainingCapacity();
|
||||
self.lookup_by_node.clearRetainingCapacity();
|
||||
_ = self.node_pool.reset(.{ .retain_capacity = {} });
|
||||
_ = self.arena.reset(.{ .retain_with_limit = 1024 });
|
||||
_ = self.node_pool.reset(.{ .retain_with_limit = 1024 });
|
||||
}
|
||||
|
||||
pub fn register(self: *Registry, n: *parser.Node) !*Node {
|
||||
@@ -132,38 +75,17 @@ pub const Registry = struct {
|
||||
// but, just in case, let's try to keep things tidy.
|
||||
errdefer _ = self.lookup_by_node.remove(n);
|
||||
|
||||
const children = try parser.nodeGetChildNodes(n);
|
||||
const children_count = try parser.nodeListLength(children);
|
||||
|
||||
const id = self.node_id;
|
||||
defer self.node_id = id + 1;
|
||||
|
||||
const node = try self.node_pool.create();
|
||||
errdefer self.node_pool.destroy(node);
|
||||
|
||||
const id = self.node_id;
|
||||
self.node_id = id + 1;
|
||||
|
||||
node.* = .{
|
||||
._node = n,
|
||||
.id = id,
|
||||
.parent_id = null, // TODO
|
||||
.backend_node_id = id, // ??
|
||||
.node_name = try parser.nodeName(n),
|
||||
.local_name = try parser.nodeLocalName(n),
|
||||
.node_value = try parser.nodeValue(n) orelse "",
|
||||
.node_type = @intFromEnum(try parser.nodeType(n)),
|
||||
.child_node_count = children_count,
|
||||
.children = &.{}, // TODO
|
||||
.document_url = null,
|
||||
.base_url = null,
|
||||
.xml_version = "",
|
||||
.compatibility_mode = .NoQuirksMode,
|
||||
.is_scrollable = false,
|
||||
};
|
||||
|
||||
// if (try parser.nodeParentNode(n)) |pn| {
|
||||
// _ = pn;
|
||||
// // TODO
|
||||
// }
|
||||
|
||||
node_lookup_gop.value_ptr.* = node;
|
||||
try self.lookup_by_id.putNoClobber(self.allocator, id, node);
|
||||
return node;
|
||||
@@ -271,8 +193,107 @@ pub const Search = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// Need a custom writer, because we can't just serialize the node as-is.
|
||||
// Sometimes we want to serializ the node without chidren, sometimes with just
|
||||
// its direct children, and sometimes the entire tree.
|
||||
// (For now, we only support direct children)
|
||||
|
||||
pub const Writer = struct {
|
||||
opts: Opts,
|
||||
node: *const Node,
|
||||
registry: *Registry,
|
||||
|
||||
pub const Opts = struct {};
|
||||
|
||||
pub fn jsonStringify(self: *const Writer, w: anytype) !void {
|
||||
self.toJSON(w) catch |err| {
|
||||
// The only error our jsonStringify method can return is
|
||||
// @TypeOf(w).Error. In other words, our code can't return its own
|
||||
// error, we can only return a writer error. Kinda sucks.
|
||||
log.err("json stringify: {}", .{err});
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
}
|
||||
|
||||
fn toJSON(self: *const Writer, w: anytype) !void {
|
||||
try w.beginObject();
|
||||
try writeCommon(self.node, false, w);
|
||||
|
||||
{
|
||||
var registry = self.registry;
|
||||
const child_nodes = try parser.nodeGetChildNodes(self.node._node);
|
||||
const child_count = try parser.nodeListLength(child_nodes);
|
||||
|
||||
var i: usize = 0;
|
||||
try w.objectField("children");
|
||||
try w.beginArray();
|
||||
for (0..child_count) |_| {
|
||||
const child = (try parser.nodeListItem(child_nodes, @intCast(i))) orelse break;
|
||||
const child_node = try registry.register(child);
|
||||
try w.beginObject();
|
||||
try writeCommon(child_node, true, w);
|
||||
try w.endObject();
|
||||
i += 1;
|
||||
}
|
||||
try w.endArray();
|
||||
|
||||
try w.objectField("childNodeCount");
|
||||
try w.write(i);
|
||||
}
|
||||
|
||||
try w.endObject();
|
||||
}
|
||||
|
||||
fn writeCommon(node: *const Node, include_child_count: bool, w: anytype) !void {
|
||||
try w.objectField("nodeId");
|
||||
try w.write(node.id);
|
||||
|
||||
try w.objectField("backendNodeId");
|
||||
try w.write(node.id);
|
||||
|
||||
const n = node._node;
|
||||
|
||||
// TODO:
|
||||
// try w.objectField("parentId");
|
||||
// try w.write(pid);
|
||||
|
||||
try w.objectField("nodeType");
|
||||
try w.write(@intFromEnum(try parser.nodeType(n)));
|
||||
|
||||
try w.objectField("nodeName");
|
||||
try w.write(try parser.nodeName(n));
|
||||
|
||||
try w.objectField("localName");
|
||||
try w.write(try parser.nodeLocalName(n));
|
||||
|
||||
try w.objectField("nodeValue");
|
||||
try w.write((try parser.nodeValue(n)) orelse "");
|
||||
|
||||
if (include_child_count) {
|
||||
try w.objectField("childNodeCount");
|
||||
const child_nodes = try parser.nodeGetChildNodes(n);
|
||||
try w.write(try parser.nodeListLength(child_nodes));
|
||||
}
|
||||
|
||||
try w.objectField("documentURL");
|
||||
try w.write(null);
|
||||
|
||||
try w.objectField("baseURL");
|
||||
try w.write(null);
|
||||
|
||||
try w.objectField("xmlVersion");
|
||||
try w.write("");
|
||||
|
||||
try w.objectField("compatibilityMode");
|
||||
try w.write("NoQuirksMode");
|
||||
|
||||
try w.objectField("isScrollable");
|
||||
try w.write(false);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "CDP Node: Registry register" {
|
||||
test "cdp Node: Registry register" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
@@ -291,19 +312,6 @@ test "CDP Node: Registry register" {
|
||||
try testing.expectEqual(node, n1c);
|
||||
|
||||
try testing.expectEqual(0, node.id);
|
||||
try testing.expectEqual(null, node.parent_id);
|
||||
try testing.expectEqual(1, node.node_type);
|
||||
try testing.expectEqual(0, node.backend_node_id);
|
||||
try testing.expectEqual("A", node.node_name);
|
||||
try testing.expectEqual("a", node.local_name);
|
||||
try testing.expectEqual("", node.node_value);
|
||||
try testing.expectEqual(1, node.child_node_count);
|
||||
try testing.expectEqual(0, node.children.len);
|
||||
try testing.expectEqual(null, node.document_url);
|
||||
try testing.expectEqual(null, node.base_url);
|
||||
try testing.expectEqual("", node.xml_version);
|
||||
try testing.expectEqual(.NoQuirksMode, node.compatibility_mode);
|
||||
try testing.expectEqual(false, node.is_scrollable);
|
||||
try testing.expectEqual(n, node._node);
|
||||
}
|
||||
|
||||
@@ -316,24 +324,11 @@ test "CDP Node: Registry register" {
|
||||
try testing.expectEqual(node, n1c);
|
||||
|
||||
try testing.expectEqual(1, node.id);
|
||||
try testing.expectEqual(null, node.parent_id);
|
||||
try testing.expectEqual(1, node.node_type);
|
||||
try testing.expectEqual(1, node.backend_node_id);
|
||||
try testing.expectEqual("P", node.node_name);
|
||||
try testing.expectEqual("p", node.local_name);
|
||||
try testing.expectEqual("", node.node_value);
|
||||
try testing.expectEqual(1, node.child_node_count);
|
||||
try testing.expectEqual(0, node.children.len);
|
||||
try testing.expectEqual(null, node.document_url);
|
||||
try testing.expectEqual(null, node.base_url);
|
||||
try testing.expectEqual("", node.xml_version);
|
||||
try testing.expectEqual(.NoQuirksMode, node.compatibility_mode);
|
||||
try testing.expectEqual(false, node.is_scrollable);
|
||||
try testing.expectEqual(n, node._node);
|
||||
}
|
||||
}
|
||||
|
||||
test "CDP Node: search list" {
|
||||
test "cdp Node: search list" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
@@ -383,3 +378,102 @@ test "CDP Node: search list" {
|
||||
try testing.expectEqual(2, registry.lookup_by_node.count());
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp Node: Writer" {
|
||||
var registry = Registry.init(testing.allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
var doc = try testing.Document.init("<a id=a1></a><a id=a2></a>");
|
||||
defer doc.deinit();
|
||||
|
||||
{
|
||||
const node = try registry.register(doc.asNode());
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.node = node,
|
||||
.opts = .{},
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
|
||||
try testing.expectJson(.{
|
||||
.nodeId = 0,
|
||||
.backendNodeId = 0,
|
||||
.nodeType = 9,
|
||||
.nodeName = "#document",
|
||||
.localName = "",
|
||||
.nodeValue = "",
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.isScrollable = false,
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.childNodeCount = 1,
|
||||
.children = &.{.{
|
||||
.nodeId = 1,
|
||||
.backendNodeId = 1,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HTML",
|
||||
.localName = "html",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
}},
|
||||
}, json);
|
||||
}
|
||||
|
||||
{
|
||||
const node = registry.lookup_by_id.get(1).?;
|
||||
const json = try std.json.stringifyAlloc(testing.allocator, Writer{
|
||||
.node = node,
|
||||
.opts = .{},
|
||||
.registry = ®istry,
|
||||
}, .{});
|
||||
defer testing.allocator.free(json);
|
||||
|
||||
try testing.expectJson(.{
|
||||
.nodeId = 1,
|
||||
.backendNodeId = 1,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HTML",
|
||||
.localName = "html",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
.children = &.{ .{
|
||||
.nodeId = 2,
|
||||
.backendNodeId = 2,
|
||||
.nodeType = 1,
|
||||
.nodeName = "HEAD",
|
||||
.localName = "head",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 0,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
}, .{
|
||||
.nodeId = 3,
|
||||
.backendNodeId = 3,
|
||||
.nodeType = 1,
|
||||
.nodeName = "BODY",
|
||||
.localName = "body",
|
||||
.nodeValue = "",
|
||||
.childNodeCount = 2,
|
||||
.documentURL = null,
|
||||
.baseURL = null,
|
||||
.xmlVersion = "",
|
||||
.compatibilityMode = "NoQuirksMode",
|
||||
.isScrollable = false,
|
||||
} },
|
||||
}, json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
self.node_search_list.reset();
|
||||
}
|
||||
|
||||
pub fn nodeWriter(self: *Self, node: *const Node, opts: Node.Writer.Opts) Node.Writer {
|
||||
return .{
|
||||
.node = node,
|
||||
.opts = opts,
|
||||
.registry = &self.node_registry,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
// msg should be {"id":<id>,...
|
||||
|
||||
@@ -51,9 +51,7 @@ fn getDocument(cmd: anytype) !void {
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const node = try bc.node_registry.register(parser.documentToNode(doc));
|
||||
return cmd.sendResult(.{
|
||||
.root = node,
|
||||
}, .{});
|
||||
return cmd.sendResult(.{ .root = bc.nodeWriter(node, .{}) }, .{});
|
||||
}
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
|
||||
@@ -118,6 +116,7 @@ fn getSearchResults(cmd: anytype) !void {
|
||||
}
|
||||
|
||||
const testing = @import("../testing.zig");
|
||||
|
||||
test "cdp.dom: getSearchResults unknown search id" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
@@ -26,11 +26,12 @@ const main = @import("cdp.zig");
|
||||
const parser = @import("netsurf");
|
||||
const App = @import("../app.zig").App;
|
||||
|
||||
pub const allocator = @import("../testing.zig").allocator;
|
||||
|
||||
pub const expectEqual = @import("../testing.zig").expectEqual;
|
||||
pub const expectError = @import("../testing.zig").expectError;
|
||||
pub const expectEqualSlices = @import("../testing.zig").expectEqualSlices;
|
||||
const base = @import("../testing.zig");
|
||||
pub const allocator = base.allocator;
|
||||
pub const expectJson = base.expectJson;
|
||||
pub const expectEqual = base.expectEqual;
|
||||
pub const expectError = base.expectError;
|
||||
pub const expectEqualSlices = base.expectEqualSlices;
|
||||
|
||||
pub const Document = @import("../testing.zig").Document;
|
||||
|
||||
@@ -284,6 +285,7 @@ const TestContext = struct {
|
||||
_ = self.client.?.serialized.orderedRemove(i);
|
||||
return;
|
||||
}
|
||||
|
||||
std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized});
|
||||
for (self.client.?.serialized.items, 0..) |sent, i| {
|
||||
std.debug.print("#{d}\n{s}\n\n", .{ i, sent });
|
||||
@@ -310,47 +312,5 @@ pub fn context() TestContext {
|
||||
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
|
||||
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
|
||||
defer expected_value.deinit();
|
||||
return compareJsonValues(expected_value.value, actual);
|
||||
}
|
||||
|
||||
fn compareJsonValues(a: std.json.Value, b: std.json.Value) bool {
|
||||
if (!std.mem.eql(u8, @tagName(a), @tagName(b))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (a) {
|
||||
.null => return true,
|
||||
.bool => return a.bool == b.bool,
|
||||
.integer => return a.integer == b.integer,
|
||||
.float => return a.float == b.float,
|
||||
.number_string => return std.mem.eql(u8, a.number_string, b.number_string),
|
||||
.string => return std.mem.eql(u8, a.string, b.string),
|
||||
.array => {
|
||||
const a_len = a.array.items.len;
|
||||
const b_len = b.array.items.len;
|
||||
if (a_len != b_len) {
|
||||
return false;
|
||||
}
|
||||
for (a.array.items, b.array.items) |a_item, b_item| {
|
||||
if (compareJsonValues(a_item, b_item) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.object => {
|
||||
var it = a.object.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const key = entry.key_ptr.*;
|
||||
if (b.object.get(key)) |b_item| {
|
||||
if (compareJsonValues(entry.value_ptr.*, b_item) == false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
return base.isEqualJson(expected_value.value, actual);
|
||||
}
|
||||
|
||||
130
src/testing.zig
130
src/testing.zig
@@ -17,14 +17,15 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const parser = @import("netsurf");
|
||||
pub const allocator = std.testing.allocator;
|
||||
pub const expectError = std.testing.expectError;
|
||||
pub const expectString = std.testing.expectEqualStrings;
|
||||
pub const expectEqualSlices = std.testing.expectEqualSlices;
|
||||
|
||||
const App = @import("app.zig").App;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// Merged std.testing.expectEqual and std.testing.expectString
|
||||
// can be useful when testing fields of an anytype an you don't know
|
||||
@@ -217,12 +218,135 @@ pub const Document = struct {
|
||||
|
||||
pub fn querySelectorAll(self: *Document, selector: []const u8) ![]const *parser.Node {
|
||||
const css = @import("dom/css.zig");
|
||||
const node_list = try css.querySelectorAll(self.arena.allocator(), parser.documentToNode(self.doc), selector);
|
||||
const node_list = try css.querySelectorAll(self.arena.allocator(), self.asNode(), selector);
|
||||
return node_list.nodes.items;
|
||||
}
|
||||
|
||||
pub fn querySelector(self: *Document, selector: []const u8) !?*parser.Node {
|
||||
const css = @import("dom/css.zig");
|
||||
return css.querySelector(self.arena.allocator(), parser.documentToNode(self.doc), selector);
|
||||
return css.querySelector(self.arena.allocator(), self.asNode(), selector);
|
||||
}
|
||||
|
||||
pub fn asNode(self: *const Document) *parser.Node {
|
||||
return parser.documentToNode(self.doc);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn expectJson(a: anytype, b: anytype) !void {
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const aa = arena.allocator();
|
||||
|
||||
const a_value = try convertToJson(aa, a);
|
||||
const b_value = try convertToJson(aa, b);
|
||||
|
||||
errdefer {
|
||||
const a_json = std.json.stringifyAlloc(aa, a_value, .{ .whitespace = .indent_2 }) catch unreachable;
|
||||
const b_json = std.json.stringifyAlloc(aa, b_value, .{ .whitespace = .indent_2 }) catch unreachable;
|
||||
std.debug.print("== Expected ==\n{s}\n\n== Actual ==\n{s}", .{ a_json, b_json });
|
||||
}
|
||||
|
||||
try expectJsonValue(a_value, b_value);
|
||||
}
|
||||
|
||||
pub fn isEqualJson(a: anytype, b: anytype) !bool {
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const aa = arena.allocator();
|
||||
const a_value = try convertToJson(aa, a);
|
||||
const b_value = try convertToJson(aa, b);
|
||||
return isJsonValue(a_value, b_value);
|
||||
}
|
||||
|
||||
fn convertToJson(arena: Allocator, value: anytype) !std.json.Value {
|
||||
const T = @TypeOf(value);
|
||||
if (T == std.json.Value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var str: []const u8 = undefined;
|
||||
if (T == []u8 or T == []const u8 or comptime isStringArray(T)) {
|
||||
str = value;
|
||||
} else {
|
||||
str = try std.json.stringifyAlloc(arena, value, .{});
|
||||
}
|
||||
return std.json.parseFromSliceLeaky(std.json.Value, arena, str, .{});
|
||||
}
|
||||
|
||||
fn expectJsonValue(a: std.json.Value, b: std.json.Value) !void {
|
||||
try expectEqual(@tagName(a), @tagName(b));
|
||||
|
||||
// at this point, we know that if a is an int, b must also be an int
|
||||
switch (a) {
|
||||
.null => return,
|
||||
.bool => try expectEqual(a.bool, b.bool),
|
||||
.integer => try expectEqual(a.integer, b.integer),
|
||||
.float => try expectEqual(a.float, b.float),
|
||||
.number_string => try expectEqual(a.number_string, b.number_string),
|
||||
.string => try expectEqual(a.string, b.string),
|
||||
.array => {
|
||||
const a_len = a.array.items.len;
|
||||
const b_len = b.array.items.len;
|
||||
try expectEqual(a_len, b_len);
|
||||
for (a.array.items, b.array.items) |a_item, b_item| {
|
||||
try expectJsonValue(a_item, b_item);
|
||||
}
|
||||
},
|
||||
.object => {
|
||||
var it = a.object.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const key = entry.key_ptr.*;
|
||||
if (b.object.get(key)) |b_item| {
|
||||
try expectJsonValue(entry.value_ptr.*, b_item);
|
||||
} else {
|
||||
return error.MissingKey;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn isJsonValue(a: std.json.Value, b: std.json.Value) bool {
|
||||
if (std.mem.eql(u8, @tagName(a), @tagName(b)) == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// at this point, we know that if a is an int, b must also be an int
|
||||
switch (a) {
|
||||
.null => return true,
|
||||
.bool => return a.bool == b.bool,
|
||||
.integer => return a.integer == b.integer,
|
||||
.float => return a.float == b.float,
|
||||
.number_string => return std.mem.eql(u8, a.number_string, b.number_string),
|
||||
.string => return std.mem.eql(u8, a.string, b.string),
|
||||
.array => {
|
||||
const a_len = a.array.items.len;
|
||||
const b_len = b.array.items.len;
|
||||
if (a_len != b_len) {
|
||||
return false;
|
||||
}
|
||||
for (a.array.items, b.array.items) |a_item, b_item| {
|
||||
if (isJsonValue(a_item, b_item) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.object => {
|
||||
var it = a.object.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const key = entry.key_ptr.*;
|
||||
if (b.object.get(key)) |b_item| {
|
||||
if (isJsonValue(entry.value_ptr.*, b_item) == false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user