diff --git a/README.md b/README.md index 117bbe4..f275595 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A simple package for building command line apps in Zig. Inspired by [urfave/cli](https://github.com/urfave/cli) Go package. ## Features +* command line arguments are parsed into zig values * long and short options: `--option1`, `-o` * optional `=` sign: `--address=127.0.0.1` equals `--address 127.0.0.1` * concatenated short options: `-a -b -c` equals `-abc` @@ -24,19 +25,25 @@ const cli = @import("zig-cli"); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); +var config = struct { + host: []const u8 = "localhost", + port: u16 = undefined, +}{}; + var host = cli.Option{ .long_name = "host", .help = "host to listen on", - .value = cli.OptionValue{ .string = null }, + .value_ref = cli.mkRef(&config.host), }; var port = cli.Option{ .long_name = "port", .help = "port to bind to", - .value = cli.OptionValue{ .int = null }, + .required = true, + .value_ref = cli.mkRef(&config.port), }; var app = &cli.App{ - .name = "awesome-app", - .options = &.{&host, &port}, + .name = "short", + .options = &.{ &host, &port }, .action = run_server, }; @@ -45,9 +52,7 @@ pub fn main() !void { } fn run_server(_: []const []const u8) !void { - var h = host.value.string.?; - var p = port.value.int.?; - std.log.debug("server is listening on {s}:{any}", .{ h, p }); + std.log.debug("server is listening on {s}:{}", .{ config.host, config.port }); } ``` diff --git a/example/short.zig b/example/short.zig index ce73086..04034a2 100644 --- a/example/short.zig +++ b/example/short.zig @@ -4,16 +4,20 @@ const cli = @import("zig-cli"); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); +var config = struct { + host: []const u8 = "localhost", + port: u16 = undefined, +}{}; var host = cli.Option{ .long_name = "host", .help = "host to listen on", - .value = cli.OptionValue{ .string = "localhost" }, + .value_ref = cli.mkRef(&config.host), }; var port = cli.Option{ .long_name = "port", .help = "port to bind to", .required = true, - .value = cli.OptionValue{ .int = null }, + .value_ref = cli.mkRef(&config.port), }; var app = &cli.App{ .name = "short", @@ -26,7 +30,5 @@ pub fn main() !void { } fn run_server(_: []const []const u8) !void { - var h = host.value.string.?; - var p = port.value.int.?; - std.log.debug("server is listening on {s}:{}", .{ h, p }); + std.log.debug("server is listening on {s}:{}", .{ config.host, config.port }); } diff --git a/example/simple.zig b/example/simple.zig index e2de686..266095b 100644 --- a/example/simple.zig +++ b/example/simple.zig @@ -4,36 +4,38 @@ const cli = @import("zig-cli"); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); +var config = struct { + ip: []const u8 = undefined, + int: i32 = undefined, + bool: bool = false, + float: f64 = 0.34, +}{}; + var ip_option = cli.Option{ .long_name = "ip", .help = "this is the IP address", .short_alias = 'i', - .value = cli.OptionValue{ .string = null }, + .value_ref = cli.mkRef(&config.ip), .required = true, .value_name = "IP", }; var int_option = cli.Option{ .long_name = "int", .help = "this is an int", - .value = cli.OptionValue{ .int = null }, + .value_ref = cli.mkRef(&config.int), }; var bool_option = cli.Option{ .long_name = "bool", .short_alias = 'b', .help = "this is a bool", - .value = cli.OptionValue{ .bool = false }, + .value_ref = cli.mkRef(&config.bool), }; var float_option = cli.Option{ .long_name = "float", .help = "this is a float", - .value = cli.OptionValue{ .float = 0.34 }, + .value_ref = cli.mkRef(&config.float), }; -var name_option = cli.Option{ - .long_name = "long_name", - .help = "long_name help", - .value = cli.OptionValue{ .string = null }, -}; var app = &cli.App{ .name = "simple", .description = "This a simple CLI app\nEnjoy!", @@ -42,7 +44,7 @@ var app = &cli.App{ .subcommands = &.{&cli.Command{ .name = "sub1", .help = "another awesome command", - .description = + .description = \\this is my awesome multiline description. \\This is already line 2. \\And this is line 3. @@ -68,6 +70,6 @@ pub fn main() anyerror!void { } fn run_sub2(args: []const []const u8) anyerror!void { - var ip = ip_option.value.string.?; - std.log.debug("running sub2: ip={s}, bool={any}, float={any} arg_count={any}", .{ ip, bool_option.value.bool, float_option.value.float, args.len }); + const c = &config; + std.log.debug("running sub2: ip={s}, bool={any}, float={any} arg_count={any}", .{ c.ip, c.bool, c.float, args.len }); } diff --git a/src/command.zig b/src/command.zig index e4d9c30..46312e8 100644 --- a/src/command.zig +++ b/src/command.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const vref = @import("./value_ref.zig"); +pub const ValueRef = vref.ValueRef; pub const App = struct { name: []const u8, @@ -39,19 +41,11 @@ pub const Command = struct { pub const Action = *const fn (args: []const []const u8) anyerror!void; -pub const OptionValue = union(enum) { - bool: bool, - string: ?[]const u8, - int: ?i64, - float: ?f64, - string_list: ?[]const []const u8, -}; - pub const Option = struct { long_name: []const u8, short_alias: ?u8 = null, help: []const u8, required: bool = false, - value: OptionValue, + value_ref: ValueRef, value_name: []const u8 = "VALUE", }; diff --git a/src/help.zig b/src/help.zig index 1d5a576..f3a22d6 100644 --- a/src/help.zig +++ b/src/help.zig @@ -99,7 +99,7 @@ const HelpPrinter = struct { self.printer.format("--{s}", .{option.long_name}); self.printer.printColor(color_clear); var width = option.long_name.len; - if (option.value != .bool) { + if (!option.value_ref.value_data.is_bool) { self.printer.printColor(self.help_config.color_option); self.printer.format(" <{s}>", .{option.value_name}); self.printer.printColor(color_clear); diff --git a/src/main.zig b/src/main.zig index 8ef0245..2e413f8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ pub usingnamespace @import("./command.zig"); const parser = @import("./parser.zig"); +pub const mkRef = @import("./value_ref.zig").mkRef; pub const run = parser.run; diff --git a/src/parser.zig b/src/parser.zig index c0f38bc..ced883f 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -5,6 +5,7 @@ const command = @import("command.zig"); const help = @import("./help.zig"); const argp = @import("./arg.zig"); const Printer = @import("./Printer.zig"); +const mkRef = @import("./value_ref.zig").mkRef; pub const ParseResult = struct { action: command.Action, @@ -22,16 +23,15 @@ pub fn run(app: *const command.App, alloc: Allocator) anyerror!void { return result.action(result.args); } +var help_option_set: bool = false; + var help_option = command.Option{ .long_name = "help", .help = "Show this help output.", .short_alias = 'h', - .value = command.OptionValue{ .bool = false }, + .value_ref = mkRef(&help_option_set), }; -const ValueList = std.ArrayList([]const u8); -const ValueListMap = std.AutoHashMap(*command.Option, ValueList); - pub fn Parser(comptime Iterator: type) type { return struct { const Self = @This(); @@ -41,7 +41,6 @@ pub fn Parser(comptime Iterator: type) type { app: *const command.App, command_path: std.ArrayList(*const command.Command), captured_arguments: std.ArrayList([]const u8), - value_lists: ?ValueListMap, pub fn init(app: *const command.App, it: Iterator, alloc: Allocator) !Self { return Self{ @@ -50,7 +49,6 @@ pub fn Parser(comptime Iterator: type) type { .app = app, .command_path = try std.ArrayList(*const command.Command).initCapacity(alloc, 16), .captured_arguments = try std.ArrayList([]const u8).initCapacity(alloc, 16), - .value_lists = null, }; } @@ -95,13 +93,12 @@ pub fn Parser(comptime Iterator: type) type { self.ensure_all_required_set(self.current_command()); var args = try self.captured_arguments.toOwnedSlice(); - if (self.value_lists) |vl| { - var it = vl.iterator(); - while (it.next()) |entry| { - var option: *command.Option = entry.key_ptr.*; - option.value.string_list = try entry.value_ptr.toOwnedSlice(); + for (self.command_path.items) |cmd| { + if (cmd.options) |options| { + for (options) |opt| { + try opt.value_ref.finalize(self.alloc); + } } - self.value_lists.?.deinit(); } if (self.current_command().action) |action| { @@ -136,12 +133,12 @@ pub fn Parser(comptime Iterator: type) type { return self.arg_iterator.next(); } - fn process_option(self: *Self, option: *const argp.OptionInterpretation) !void { - var opt: *command.Option = switch (option.option_type) { - .long => self.find_option_by_name(self.current_command(), option.name), + fn process_option(self: *Self, option_interpretation: *const argp.OptionInterpretation) !void { + var opt: *command.Option = switch (option_interpretation.option_type) { + .long => self.find_option_by_name(self.current_command(), option_interpretation.name), .short => a: { - self.set_boolean_options(self.current_command(), option.name[0 .. option.name.len - 1]); - break :a self.find_option_by_alias(self.current_command(), option.name[option.name.len - 1]); + self.set_concatenated_boolean_options(self.current_command(), option_interpretation.name[0 .. option_interpretation.name.len - 1]); + break :a self.find_option_by_alias(self.current_command(), option_interpretation.name[option_interpretation.name.len - 1]); }, }; @@ -150,49 +147,18 @@ pub fn Parser(comptime Iterator: type) type { std.os.exit(0); } - switch (opt.value) { - .bool => opt.value = command.OptionValue{ .bool = true }, - else => { - const arg = option.value orelse self.next_arg() orelse { - self.fail("missing argument for {s}", .{opt.long_name}); - unreachable; - }; - try self.parse_and_set_option_value(arg, opt); - }, - } - } - - fn parse_and_set_option_value(self: *Self, text: []const u8, option: *command.Option) !void { - switch (option.value) { - .bool => unreachable, - .string => option.value = command.OptionValue{ .string = text }, - .int => { - if (std.fmt.parseInt(i64, text, 10)) |iv| { - option.value = command.OptionValue{ .int = iv }; - } else |_| { - self.fail("option({s}): cannot parse int value", .{option.long_name}); - unreachable; - } - }, - .float => { - if (std.fmt.parseFloat(f64, text)) |fv| { - option.value = command.OptionValue{ .float = fv }; - } else |_| { - self.fail("option({s}): cannot parse float value", .{option.long_name}); - unreachable; - } - }, - .string_list => { - if (self.value_lists == null) { - self.value_lists = ValueListMap.init(self.alloc); - } - - var res = try self.value_lists.?.getOrPut(option); - if (!res.found_existing) { - res.value_ptr.* = try ValueList.initCapacity(self.alloc, 16); - } - try res.value_ptr.append(text); - }, + if (opt.value_ref.value_data.is_bool) { + try opt.value_ref.put("true", self.alloc); + // TODO: bool argument can be explicitly passed as a value + } else { + const arg = option_interpretation.value orelse self.next_arg() orelse { + self.fail("missing argument for {s}", .{opt.long_name}); + unreachable; + }; + opt.value_ref.put(arg, self.alloc) catch |err| { + self.fail("option({s}): cannot parse {s} value: {s}", .{ opt.long_name, opt.value_ref.value_data.type_name, @errorName(err) }); + unreachable; + }; } } @@ -250,11 +216,12 @@ pub fn Parser(comptime Iterator: type) type { } } - fn set_boolean_options(self: *const Self, cmd: *const command.Command, options: []const u8) void { + /// Set boolean options provided like `-acde` + fn set_concatenated_boolean_options(self: *const Self, cmd: *const command.Command, options: []const u8) void { for (options) |alias| { var opt = self.find_option_by_alias(cmd, alias); - if (opt.value == command.OptionValue.bool) { - opt.value.bool = true; + if (opt.value_ref.value_data.is_bool) { + opt.value_ref.put("true", self.alloc) catch unreachable; } else { self.fail("'-{c}' is not a boolean option", .{alias}); } @@ -264,17 +231,8 @@ pub fn Parser(comptime Iterator: type) type { fn ensure_all_required_set(self: *const Self, cmd: *const command.Command) void { if (cmd.options) |list| { for (list) |option| { - if (option.required) { - var not_set = switch (option.value) { - .bool => false, - .string => |x| x == null, - .int => |x| x == null, - .float => |x| x == null, - .string_list => |x| x == null, - }; - if (not_set) { - self.fail("missing required option '{s}'", .{option.long_name}); - } + if (option.required and option.value_ref.element_count == 0) { + self.fail("missing required option '{s}'", .{option.long_name}); } } } diff --git a/src/tests.zig b/src/tests.zig index ed6db82..7d67b92 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const command = @import("./command.zig"); const ppack = @import("./parser.zig"); +const mkRef = @import("./value_ref.zig").mkRef; const Parser = ppack.Parser; const ParseResult = ppack.ParseResult; @@ -38,10 +39,11 @@ fn run(app: *command.App, items: []const []const u8) !ParseResult { fn dummy_action(_: []const []const u8) !void {} test "long option" { + var aa: []const u8 = "test"; var opt = command.Option{ .long_name = "aa", .help = "option aa", - .value = command.OptionValue{ .string = null }, + .value_ref = mkRef(&aa), }; var cmd = command.App{ .name = "abc", @@ -50,18 +52,19 @@ test "long option" { }; _ = try run(&cmd, &.{ "cmd", "--aa", "val" }); - try expect(std.mem.eql(u8, opt.value.string.?, "val")); + try std.testing.expectEqualStrings("val", aa); _ = try run(&cmd, &.{ "cmd", "--aa=bb" }); - try expect(std.mem.eql(u8, opt.value.string.?, "bb")); + try std.testing.expectEqualStrings("bb", aa); } test "short option" { + var aa: []const u8 = undefined; var opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", - .value = command.OptionValue{ .string = null }, + .value_ref = mkRef(&aa), }; var app = command.App{ .name = "abc", @@ -70,92 +73,156 @@ test "short option" { }; _ = try run(&app, &.{ "abc", "-a", "val" }); - try expect(std.mem.eql(u8, opt.value.string.?, "val")); + try std.testing.expectEqualStrings("val", aa); _ = try run(&app, &.{ "abc", "-a=bb" }); - try expect(std.mem.eql(u8, opt.value.string.?, "bb")); + try std.testing.expectEqualStrings("bb", aa); } test "concatenated aliases" { - var bb = command.Option{ + var aa: []const u8 = undefined; + var bb: bool = false; + var bbopt = command.Option{ .long_name = "bb", .short_alias = 'b', .help = "option bb", - .value = command.OptionValue{ .bool = false }, + .value_ref = mkRef(&bb), }; var opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", - .value = command.OptionValue{ .string = null }, + .value_ref = mkRef(&aa), }; var app = command.App{ .name = "abc", - .options = &.{ &bb, &opt }, + .options = &.{ &bbopt, &opt }, .action = dummy_action, }; _ = try run(&app, &.{ "abc", "-ba", "val" }); - try expect(std.mem.eql(u8, opt.value.string.?, "val")); - try expect(bb.value.bool); + try std.testing.expectEqualStrings("val", aa); + try expect(bb); } test "int and float" { - var aa = command.Option{ + var aa: i32 = undefined; + var bb: f64 = undefined; + var aa_opt = command.Option{ .long_name = "aa", .help = "option aa", - .value = command.OptionValue{ .int = null }, + .value_ref = mkRef(&aa), }; - var bb = command.Option{ + var bb_opt = command.Option{ .long_name = "bb", .help = "option bb", - .value = command.OptionValue{ .float = null }, + .value_ref = mkRef(&bb), }; var app = command.App{ .name = "abc", - .options = &.{ &aa, &bb }, + .options = &.{ &aa_opt, &bb_opt }, .action = dummy_action, }; _ = try run(&app, &.{ "abc", "--aa=34", "--bb", "15.25" }); - try expect(aa.value.int.? == 34); - try expect(bb.value.float.? == 15.25); + try expect(34 == aa); + try expect(15.25 == bb); +} + +test "optional values" { + var aa: ?i32 = null; + var bb: ?f32 = 500; + var cc: ?f32 = null; + + var aa_opt = command.Option{ + .long_name = "aa", + .help = "option aa", + .value_ref = mkRef(&aa), + }; + var bb_opt = command.Option{ + .long_name = "bb", + .help = "option bb", + .value_ref = mkRef(&bb), + }; + var cc_opt = command.Option{ + .long_name = "cc", + .help = "option cc", + .value_ref = mkRef(&cc), + }; + var app = command.App{ + .name = "abc", + .options = &.{ &aa_opt, &bb_opt, &cc_opt }, + .action = dummy_action, + }; + + _ = try run(&app, &.{ "abc", "--aa=34", "--bb", "15.25" }); + try expect(34 == aa.?); + try expect(15.25 == bb.?); + try std.testing.expect(cc == null); +} + +test "int list" { + var aa: []u64 = undefined; + var aa_opt = command.Option{ + .long_name = "aa", + .short_alias = 'a', + .help = "option aa", + .value_ref = mkRef(&aa), + }; + var app = command.App{ + .name = "abc", + .options = &.{&aa_opt}, + .action = dummy_action, + }; + + _ = try run(&app, &.{ "abc", "--aa=100", "--aa", "200", "-a", "300", "-a=400" }); + try expect(aa.len == 4); + try expect(aa[0] == 100); + try expect(aa[1] == 200); + try expect(aa[2] == 300); + try expect(aa[3] == 400); + + // FIXME: it tries to deallocated u64 while the memory was allocated using u8 alignment + alloc.free(aa); } test "string list" { - var aa = command.Option{ + var aa: [][]const u8 = undefined; + var aa_opt = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", - .value = command.OptionValue{ .string_list = null }, + .value_ref = mkRef(&aa), }; var app = command.App{ .name = "abc", - .options = &.{&aa}, + .options = &.{&aa_opt}, .action = dummy_action, }; _ = try run(&app, &.{ "abc", "--aa=a1", "--aa", "a2", "-a", "a3", "-a=a4" }); - try expect(aa.value.string_list.?.len == 4); - try expect(std.mem.eql(u8, aa.value.string_list.?[0], "a1")); - try expect(std.mem.eql(u8, aa.value.string_list.?[1], "a2")); - try expect(std.mem.eql(u8, aa.value.string_list.?[2], "a3")); - try expect(std.mem.eql(u8, aa.value.string_list.?[3], "a4")); + try expect(aa.len == 4); + try std.testing.expectEqualStrings("a1", aa[0]); + try std.testing.expectEqualStrings("a2", aa[1]); + try std.testing.expectEqualStrings("a3", aa[2]); + try std.testing.expectEqualStrings("a4", aa[3]); - alloc.free(aa.value.string_list.?); + alloc.free(aa); } test "mix positional arguments and options" { + var aav: []const u8 = undefined; + var bbv: []const u8 = undefined; var aa = command.Option{ .long_name = "aa", .short_alias = 'a', .help = "option aa", - .value = command.OptionValue{ .string = null }, + .value_ref = mkRef(&aav), }; var bb = command.Option{ .long_name = "bb", .help = "option bb", - .value = command.OptionValue{ .string = null }, + .value_ref = mkRef(&bbv), }; var app = command.App{ .name = "abc", @@ -165,11 +232,11 @@ test "mix positional arguments and options" { var result = try run(&app, &.{ "cmd", "--bb", "tt", "arg1", "-a", "val", "arg2", "--", "--arg3", "-arg4" }); defer std.testing.allocator.free(result.args); - try expect(std.mem.eql(u8, aa.value.string.?, "val")); - try expect(std.mem.eql(u8, bb.value.string.?, "tt")); + try std.testing.expectEqualStrings("val", aav); + try std.testing.expectEqualStrings("tt", bbv); try expect(result.args.len == 4); - try expect(std.mem.eql(u8, result.args[0], "arg1")); - try expect(std.mem.eql(u8, result.args[1], "arg2")); - try expect(std.mem.eql(u8, result.args[2], "--arg3")); - try expect(std.mem.eql(u8, result.args[3], "-arg4")); + try std.testing.expectEqualStrings("arg1", result.args[0]); + try std.testing.expectEqualStrings("arg2", result.args[1]); + try std.testing.expectEqualStrings("--arg3", result.args[2]); + try std.testing.expectEqualStrings("-arg4", result.args[3]); } diff --git a/src/value_parser.zig b/src/value_parser.zig new file mode 100644 index 0000000..05a4e17 --- /dev/null +++ b/src/value_parser.zig @@ -0,0 +1,81 @@ +const std = @import("std"); + +pub const ValueParser = *const fn (dest: *anyopaque, value: []const u8) anyerror!void; + +pub const ValueData = struct { + value_size: usize, + value_parser: ValueParser, + is_bool: bool = false, + type_name: []const u8, +}; + +pub fn getValueData(comptime T: type) ValueData { + const ValueType = switch (@typeInfo(T)) { + .Optional => |oinfo| oinfo.child, + else => T, + }; + return switch (@typeInfo(ValueType)) { + .Int => intData(ValueType, T), + .Float => floatData(ValueType, T), + .Bool => boolData(T), + .Pointer => |pinfo| { + if (pinfo.size == .Slice and pinfo.child == u8) { + return stringData(T); + } + }, + else => @compileError("unsupported value type"), + }; +} + +fn intData(comptime ValueType: type, comptime DestinationType: type) ValueData { + return .{ + .value_size = @sizeOf(DestinationType), + .value_parser = struct { + fn parser(dest: *anyopaque, value: []const u8) anyerror!void { + const dt: *DestinationType = @alignCast(@ptrCast(dest)); + dt.* = try std.fmt.parseInt(ValueType, value, 10); + } + }.parser, + .type_name = "integer", + }; +} + +fn floatData(comptime ValueType: type, comptime DestinationType: type) ValueData { + return .{ + .value_size = @sizeOf(DestinationType), + .value_parser = struct { + fn parser(dest: *anyopaque, value: []const u8) anyerror!void { + const dt: *DestinationType = @ptrCast(@alignCast(dest)); + dt.* = try std.fmt.parseFloat(ValueType, value); + } + }.parser, + .type_name = "float", + }; +} + +fn boolData(comptime DestinationType: type) ValueData { + return .{ + .value_size = @sizeOf(DestinationType), + .is_bool = true, + .value_parser = struct { + fn parser(dest: *anyopaque, value: []const u8) anyerror!void { + const dt: *DestinationType = @ptrCast(@alignCast(dest)); + dt.* = std.mem.eql(u8, value, "true"); + } + }.parser, + .type_name = "bool", + }; +} + +fn stringData(comptime DestinationType: type) ValueData { + return .{ + .value_size = @sizeOf(DestinationType), + .value_parser = struct { + fn parser(dest: *anyopaque, value: []const u8) anyerror!void { + const dt: *DestinationType = @ptrCast(@alignCast(dest)); + dt.* = value; + } + }.parser, + .type_name = "string", + }; +} diff --git a/src/value_ref.zig b/src/value_ref.zig new file mode 100644 index 0000000..3ca6237 --- /dev/null +++ b/src/value_ref.zig @@ -0,0 +1,121 @@ +const std = @import("std"); +const command = @import("./command.zig"); +const vp = @import("./value_parser.zig"); +const Allocator = std.mem.Allocator; + +pub const ValueRef = struct { + dest: *anyopaque, + value_data: vp.ValueData, + value_type: ValueType, + element_count: usize = 0, + + const Self = @This(); + + pub fn put(self: *Self, value: []const u8, alloc: Allocator) anyerror!void { + self.element_count += 1; + switch (self.value_type) { + .single => { + return self.value_data.value_parser(self.dest, value); + }, + .multi => |*list| { + if (list.list_ptr == null) { + list.list_ptr = try list.vtable.createList(alloc); + } + var value_ptr = try list.vtable.addOne(list.list_ptr.?, alloc); + try self.value_data.value_parser(value_ptr, value); + }, + } + } + + pub fn finalize(self: *Self, alloc: Allocator) anyerror!void { + switch (self.value_type) { + .single => {}, + .multi => |*list| { + if (list.list_ptr == null) { + list.list_ptr = try list.vtable.createList(alloc); + } + try list.vtable.finalize(list.list_ptr.?, self.dest, alloc); + }, + } + } +}; + +const ValueType = union(enum) { + single, + multi: ValueList, +}; + +const AllocError = Allocator.Error; +pub const Error = AllocError; // | error{NotImplemented}; + +pub fn mkRef(dest: anytype) ValueRef { + const ti = @typeInfo(@TypeOf(dest)); + const t = ti.Pointer.child; + + switch (@typeInfo(t)) { + .Pointer => |pinfo| { + switch (pinfo.size) { + .Slice => { + if (pinfo.child == u8) { + return ValueRef{ + .dest = @ptrCast(dest), + .value_data = vp.getValueData(t), + .value_type = .single, + }; + } else { + return ValueRef{ + .dest = @ptrCast(dest), + .value_data = vp.getValueData(pinfo.child), + .value_type = ValueType{ .multi = ValueList.init(pinfo.child) }, + }; + } + }, + else => @compileError("unsupported value type: only slices are supported"), + } + }, + else => { + return ValueRef{ + .dest = dest, + .value_data = vp.getValueData(t), + .value_type = .single, + }; + }, + } +} + +const ValueList = struct { + list_ptr: ?*anyopaque = null, + vtable: VTable, + + const VTable = struct { + createList: *const fn (Allocator) anyerror!*anyopaque, + addOne: *const fn (list_ptr: *anyopaque, alloc: Allocator) anyerror!*anyopaque, + finalize: *const fn (list_ptr: *anyopaque, dest: *anyopaque, alloc: Allocator) anyerror!void, + }; + + fn init(comptime T: type) ValueList { + const List = std.ArrayListUnmanaged(T); + const gen = struct { + fn createList(alloc: Allocator) anyerror!*anyopaque { + var list = try alloc.create(List); + list.* = List{}; + return list; + } + fn addOne(list_ptr: *anyopaque, alloc: Allocator) anyerror!*anyopaque { + const list: *List = @alignCast(@ptrCast(list_ptr)); + return @ptrCast(try list.addOne(alloc)); + } + fn finalize(list_ptr: *anyopaque, dest: *anyopaque, alloc: Allocator) anyerror!void { + const list: *List = @alignCast(@ptrCast(list_ptr)); + var destSlice: *[]T = @alignCast(@ptrCast(dest)); + destSlice.* = try list.toOwnedSlice(alloc); + alloc.destroy(list); + } + }; + return ValueList{ .vtable = VTable{ + .createList = gen.createList, + .addOne = gen.addOne, + .finalize = gen.finalize, + } }; + } +};