fengb / zCord

Zig ⚡ Discord API with zero allocations in the critical path
MIT License
55 stars 12 forks source link

Comptime JSON path #1

Closed fengb closed 3 years ago

fengb commented 3 years ago

As suggested by @kristoff-it, it'd be really cool if we could have a declarative syntax. We could parse a JSON path and generate the necessary plumbing to read it.

Unfortunately due to streams being stateful, this logic must be generated simultaneously (at least per single array/object). Building a struct seems like the best option:

const result = try element.pathMatch(.{
    .user_id = json.Path.number(u64, "@.user.id"),
    .session_id = json.Path.stringAlloc(allocator, "@.session_id"),
    .app_name = json.Path.stringBuffer(buffer, "@.application.name"),
});

This should generate something similar to:

var result = undefined;

{
    var required_found = .{
        .user_id = false,
        .session_id = false,
        .app_name = false,
    };
    while (try element.objectMatchAny("user", "session_id", "application")) |match| {
        if (std.mem.eql(u8, "user", match.key)) {
            const raw_user_id = try match.value.objectMatch("id");
            result.user_id = try raw_user_id.number(u64);
            continue;
        }
        if (std.mem.eql(u8, "session_id", match.key)) {
            result.session_id = try match.value.stringAlloc(allocator);
            continue;
        }
        if (std.mem.eql(u8, "application", match.key)) {
            const raw_app_name = try match.value.objectMatch("name");
            result.app_name = try raw_app_name.stringAlloc(allocator);
            continue;
        }
        unreachable;
    }
    inline for (std.meta.fields(required_found)) |field| {
        if (!@field(required_found, field.field_name)) {
            return error.RequiredFieldNotFound;
        }
    }
}
fengb commented 3 years ago

Trying to read stuff like this causes major compiler bugs:

.{
    .user_id = json.Path.number(u64, "@.user.id"),
    .session_id = json.Path.stringAlloc(allocator, "@.session_id"),
    .app_name = json.Path.stringBuffer(buffer, "@.application.name"),
}

I think this is the same problem Alex Nask ran into: the compiler gets confused because there's a mixture of comptime and runtime: allocator is runtime known but it's trying to be stored in a comptime context (default_value)

fengb commented 3 years ago

Alternate syntax:

    const match = root.pathMatch(std.testing.allocator, struct {
        "@.foo": bool,
        "@.bar": u32,
        "@.baz": []const u8,
    });

This one cannot use scoped runtime values (e.g. buffers), but it seems to behave a lot better. A FixedBufferAllocator is actually more flexible so maybe this isn't a big loss.

fengb commented 3 years ago

Oh hey this is working now!