Hi! I am trying to write a wrapper for Google’s GenAI API.
I want to use Zig’s naming convention, so I need to manually map the enums/struct fields to the JSON ones from the API.
I’ve come up with this way to do it:
const std = @import("std");
const Allocator = std.mem.Allocator;
const json = std.json;
/// A factory function that creates a struct containing JSON parsing and
/// stringifying functions for a given enum type `T`.
///
/// It expects the enum `T` to have a `const json_fields` array of strings
/// whose order perfectly matches the order of the enum members.
fn JsonEnum(comptime T: type) type {
return struct {
/// Generic JSON stringify implementation.
/// It uses the enum's integer value to look up the correct string.
pub fn jsonStringify(self: T, writer: anytype) !void {
try writer.write(T.json_fields[@intFromEnum(self)]);
}
/// Generic JSON parsing implementation.
/// It iterates through the `json_fields` at compile time to find a match.
pub fn jsonParse(allocator: Allocator, source: anytype, options: json.ParseOptions) !T {
const token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?);
const string = switch (token) {
inline .string, .allocated_string => |slice| slice,
else => return error.UnexpectedToken,
};
inline for (T.json_fields, 0..) |field_name, i| {
if (std.mem.eql(u8, string, field_name)) {
return @as(T, @enumFromInt(i));
}
}
return error.InvalidEnumTag;
}
};
}
/// Outcome of the code execution
pub const Outcome = enum {
/// Unspecified status. This value should not be used.
unspecified,
/// Code execution completed successfully.
ok,
/// Code execution finished but with a failure. `stderr` should contain the reason.
failed,
/// Code execution ran for too long, and was cancelled. There may or may not be a partial
/// output present.
deadline_exceeded,
const json_fields = [_][]const u8{
"OUTCOME_UNSPECIFIED",
"OUTCOME_OK",
"OUTCOME_FAILED",
"OUTCOME_DEADLINE_EXCEEDED",
};
pub usingnamespace JsonEnum(@This());
};
But I know that usingnamespace
is deprecated. So, what would be an alternative way to do this?
I thought about this, which is a bit more annoying because those two Json-related functions will be exactly the same for all enums in the API:
fn jsonStringifyGeneric(
comptime T: type,
json_fields: []const []const u8,
self: T,
writer: anytype,
) !void {
try writer.write(json_fields[@intFromEnum(self)]);
}
fn jsonParseGeneric(
comptime T: type,
comptime json_fields: []const []const u8,
allocator: Allocator,
source: anytype,
options: json.ParseOptions,
) !T {
const token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?);
const string = switch (token) {
inline .string, .allocated_string => |slice| slice,
else => return error.UnexpectedToken,
};
inline for (json_fields, 0..) |field_name, i| {
if (std.mem.eql(u8, string, field_name)) {
return @as(T, @enumFromInt(i));
}
}
return error.InvalidEnumTag;
}
/// Outcome of the code execution
pub const Outcome = enum {
/// Unspecified status. This value should not be used.
unspecified,
/// Code execution completed successfully.
ok,
/// Code execution finished but with a failure. `stderr` should contain the reason.
failed,
/// Code execution ran for too long, and was cancelled. There may or may not be a partial
/// output present.
deadline_exceeded,
const json_fields = [_][]const u8{
"OUTCOME_UNSPECIFIED",
"OUTCOME_OK",
"OUTCOME_FAILED",
"OUTCOME_DEADLINE_EXCEEDED",
};
pub fn jsonStringify(self: @This(), writer: anytype) !void {
try jsonStringifyGeneric(@This(), &json_fields, self, writer);
}
pub fn jsonParse(allocator: Allocator, source: anytype, options: json.ParseOptions) !@This() {
return try jsonParseGeneric(@This(), &json_fields, allocator, source, options);
}
};
All of this works:
test Outcome {
const outcome: Outcome = .deadline_exceeded;
var string: std.ArrayList(u8) = .init(std.testing.allocator);
defer string.deinit();
try json.stringify(outcome, .{}, string.writer());
try std.testing.expectEqualSlices(u8, string.items, "\"OUTCOME_DEADLINE_EXCEEDED\"");
var parsed_object = try std.json.parseFromSlice(Outcome, std.testing.allocator, string.items, .{});
defer parsed_object.deinit();
try std.testing.expectEqual(outcome, parsed_object.value);
}
How would you do it without usingnamespace
?