Is there a way to share enums between different ones?

I started writing a server that has three states and I’m saving them in an enum:

pub fn processInput(
    io: std.Io,
    gpa: std.mem.Allocator,
    conn: std.Io.net.Stream,
    data: []u8,
    ctx: *Context,
    endpoints: *lib.SafeEndpointList,
    current_id: *std.atomic.Value(usize),
    switcher: *lib.SwitcherState,
) !void {
    var msg_buf: [128]u8 = undefined;
    var iter = std.mem.tokenizeAny(u8, data, " \r\n\t");

    switch (ctx.*) {
        .global => try handleGlobal( io, conn, &msg_buf, &iter, ctx, switcher, endpoints, current_id),
        .endpoint => try handleEndpoint( io, gpa, &msg_buf, conn, &iter, ctx, switcher, endpoints, current_id),
        .switcher => try handleSwitcher( io, &msg_buf, conn, &iter, ctx, switcher, endpoints, current_id),
    }
}

And all three handle functions have three command sets available to them:

const GlobalCmd = enum {
    const Self = @This();
    help,
    @"?",
    list,
    status,
    switcher,
    endpoint,
    exit,
    quit,
    q,

    // Helper to turn the string into this enum
    fn from(s: []const u8) ?Self {
        return std.meta.stringToEnum(Self, s);
    }
};
const EndpointCmd = enum {
    const Self = @This();
    help,
    @"?",
    list,
    status,
    add,
    remove,
    set,
    switcher,
    @"return",
    ret,
    exit,
    quit,
    q,

    // Helper to turn the string into this enum
    fn from(s: []const u8) ?Self {
        return std.meta.stringToEnum(Self, s);
    }
};
const SwitcherCmd = enum {
    const Self = @This();
    help,
    @"?",
    status,
    play,
    pause,
    kill,
    timer,
    endpoint,
    @"return",
    ret,
    exit,
    quit,
    q,

    // Helper to turn the string into this enum
    fn from(s: []const u8) ?Self {
        return std.meta.stringToEnum(Self, s);
    }
};

Is there a way to combine them to remove duplication of the same enums or a better way to approach this? Currently each of my functions is processing input like this:

fn handleGlobal(
    io: std.Io,
    conn: std.Io.net.Stream,
    msg_buf: []u8,
    iter: *std.mem.TokenIterator(u8, .any),
    ctx: *Context,
    switcher: *lib.SwitcherState,
    endpoints: *lib.SafeEndpointList,
    current_id: *std.atomic.Value(usize),
) !void {
    const help_msg =
        \\Available Global Commands:
        \\  help (?)    - Show this message
        \\  list        - List all available endpoints
        \\  status      - Show switcher status and current server info
        \\  switcher    - Interactively manage switcher
        \\  endpoint    - Interactively manage endpoint
        \\  exit        - Close the admin connection
        \\
    ;
    const raw_cmd = iter.next() orelse {
        printPrompt(io, conn, ctx.*);
        return;
    };

    // Use your GlobalCmd enum helper
    const cmd = GlobalCmd.from(raw_cmd) orelse {
        reply(io, conn, "Unknown command!\n");
        printPrompt(io, conn, ctx.*);
        return;
    };

    switch (cmd) {
        .exit, .quit, .q => return error.Exit,
        .help, .@"?" => reply(io, conn, help_msg),
        .list => try listEndpoints(io, conn, msg_buf, endpoints),
        .status => try showStatus( io, conn, msg_buf, switcher, endpoints, current_id),
        .switcher => ctx.* = .switcher,
        .endpoint => ctx.* = .endpoint,
    }
    // After every successful command:
    printPrompt(io, conn, ctx.*);
}

You could use tagged unions like this

const EndpointCmd = union(enum) {
    global: GlobalCmd,
    add,
    set,
    @"return",
    ret,
}

Not relevant, but I dislike aliases being different values than the command they alias. I prefer mapping to the same value in parsing, that way in code you only have to deal with the canonical command names.

I mean, I could just have two switches, check if the command is in endpoint, check if it’s in global. Would you prefer that approach?

depends on how you use it, but that would be my first thought.

1 Like

Ok, I’ll use that approach then. In case others are interested to see how union would work, I came up at this:

const EndpointCmd = union(enum) {
    const Self = @This();
    global: GlobalCmd,
    add,
    remove,
    set,
    @"return",
    ret,

    fn from(s: []const u8) ?EndpointCmd {
        const Tag = std.meta.Tag(EndpointCmd);

        if (std.meta.stringToEnum(Tag, s)) |tag| {
            switch (tag) {
                .global => {},
                inline else => |t| {
                    return @unionInit(EndpointCmd, @tagName(t), {});
                },
            }
        }

        if (GlobalCmd.from(s)) |g_cmd| {
            return .{ .global = g_cmd };
        }

        return null;
    }

};

I’m ignoring global in first if since compiler was complaining about the void, it needs GlobalCmd type.
I could add this:

 .global => return @unionInit(EndpointCmd, @tagName(.global), GlobalCmd.from(s).?),

But it is useless in my usecase since I don’t need to check for “global” command. I need to check for a set of commands that are global, what this is doing:

        if (GlobalCmd.from(s)) |g_cmd| {
            return .{ .global = g_cmd };
        }

Then switch can look like this:

    const cmd = EndpointCmd.from(raw_cmd) orelse {
        reply(io, conn, "Unknown command!\n");
        printPrompt(io, conn, ctx.*);
        return;
    };

    switch (cmd) {
        .global => |g| switch (g) {
            .exit, .quit, .q => return error.Exit,
            .help, .@"?" => reply(io, conn, help_msg),
            .list => try listEndpoints(io, conn, msg_buf, endpoints),
            .status => try showStatus(
                io,
                conn,
                msg_buf,
                switcher,
                endpoints,
                current_id,
            ),
            .switcher => ctx.* = .switcher,
            .endpoint => {},
        },
        .add => try addEndpoint(
            io,
            gpa,
            conn,
            msg_buf,
            iter,
            endpoints,
        ),
        .remove => try removeEndpoint(
            io,
            conn,
            msg_buf,
            iter,
            endpoints,
            current_id,
        ),
        .set => try setEndpoint(
            io,
            conn,
            msg_buf,
            iter,
            endpoints,
            current_id,
        ),
        .@"return", .ret => ctx.* = .global,
    }

I have a switch within a switch.

1 Like