Removing usage of usingnamespace

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?

I just thought of an intermediate solution in between those two:

pub const jsonParse = JsonEnum(@This()).jsonParse;
pub const jsonStringify = JsonEnum(@This()).jsonStringify;
1 Like

I believe this is the recommended way for something simple like this. You can do the mapping with manual naming.

One way to do this as well:

const OutcomeJson = JsonEnum(@This());
pub const jsonParse = OutcomeJson.jsonParse;
pub const jsonStringify = OutcomeJson.jsonStringify;
2 Likes

If you’re curious about how to do mixins without usingnamespace, there is an example here https://github.com/ziglang/zig/issues/20663. Scroll down to the ‘Mixins’ section.

However I don’t think this would work here with an enum instead of another container type such as struct which supports @fieldParentPointer().

2 Likes