API Redesign #13

Open
opened 2024-09-11 00:46:28 -06:00 by torque · 0 comments
Owner

Having used this for a while, I think the command building API should be somewhat redesigned. I'm considering two main approaches:

const SomeCommand = noclip.buildCommand(struct {
    const option_a = noclip.namedOption(noclip.String, .{
        .flags = .{"-o", "--option"},
        .default = "hello",
        .help = "an example of a named option"
    });

    const option_b = noclip.orderedOption(u32, .{
        .help = "an ordered option"
    });
});

const 

There's maybe a little bit more magic going on under the hood here, but it saves some redundant information (output struct field names), and there's less boilerplate than the current API. The function signatures outlined would be:

pub fn buildCommand(comptime definition: type) type {
    // do a lot of magic here
}

pub fn namedOption(comptime T: type, comptime options: anytype) Parameter {
    // do a lot of magic here, based on. Introspection on the `options` struct.
}

pub fn orderedOption(comptime T: type, comptime options: anytype) Parameter {
    // this is probably very similar to `namedOption` and can be combined.
}

The nice thing is that there's no dynamic comptime state here.

The second style is as follows:

const Command = cmd: {
    var builder: noclip.Command = .init;
    const Options = struct {
        option_a: builder.namedOption(noclip.String, .{
            .flags = .{"-o", "--option"},
            .default = "hello",
            .help = "this is a named option"
        }),

        fn doSomething(self: Options) usize {
            return self.option_a.len;
        }
    };
    break :cmd builder.finish(Options);
};

This does a very goofy thing by using the builder methods as type specifiers. This allows the options struct to have methods defined on it, which can be quite useful. As far as I have reasoned through it, this does have to have the dynamic state for the builder (and thus the additional boilerplate) because the Options type cannot be generated in order to support defining methods—since the type expressions have to result in the types that the user actually wants to use when accessing the options struct, the only way to store the metadata the parser needs is to do it dynamically in order with each field definition.

Syntactically, I think this ends up being harder to read than the previous style, it has more boilerplate, and I'm really not convinced there's much value in having methods on the Options struct anyway. Besides, #2 provides a solution to that which is compatible with the first style anyway—rather than generating a type, allow the user to pass in a manually defined type with compatible fields.

Having used this for a while, I think the command building API should be somewhat redesigned. I'm considering two main approaches: ```zig const SomeCommand = noclip.buildCommand(struct { const option_a = noclip.namedOption(noclip.String, .{ .flags = .{"-o", "--option"}, .default = "hello", .help = "an example of a named option" }); const option_b = noclip.orderedOption(u32, .{ .help = "an ordered option" }); }); const ``` There's maybe a little bit more magic going on under the hood here, but it saves some redundant information (output struct field names), and there's less boilerplate than the current API. The function signatures outlined would be: ```zig pub fn buildCommand(comptime definition: type) type { // do a lot of magic here } pub fn namedOption(comptime T: type, comptime options: anytype) Parameter { // do a lot of magic here, based on. Introspection on the `options` struct. } pub fn orderedOption(comptime T: type, comptime options: anytype) Parameter { // this is probably very similar to `namedOption` and can be combined. } ``` The nice thing is that there's no dynamic comptime state here. The second style is as follows: ```zig const Command = cmd: { var builder: noclip.Command = .init; const Options = struct { option_a: builder.namedOption(noclip.String, .{ .flags = .{"-o", "--option"}, .default = "hello", .help = "this is a named option" }), fn doSomething(self: Options) usize { return self.option_a.len; } }; break :cmd builder.finish(Options); }; ``` This does a very goofy thing by using the builder methods as type specifiers. This allows the options struct to have methods defined on it, which can be quite useful. As far as I have reasoned through it, this does have to have the dynamic state for the builder (and thus the additional boilerplate) because the `Options` type cannot be generated in order to support defining methods—since the type expressions have to result in the types that the user actually wants to use when accessing the options struct, the only way to store the metadata the parser needs is to do it dynamically in order with each field definition. Syntactically, I think this ends up being harder to read than the previous style, it has more boilerplate, and I'm really not convinced there's much value in having methods on the `Options` struct anyway. Besides, #2 provides a solution to that which is compatible with the first style anyway—rather than generating a type, allow the user to pass in a manually defined type with compatible fields.
torque self-assigned this 2024-10-10 19:02:20 -06:00
Sign in to join this conversation.
No description provided.