Massive fuckery
This commit is contained in:
parent
41a97a9df8
commit
00a531045b
15 changed files with 812 additions and 0 deletions
9
.woodpecker/build.yml
Normal file
9
.woodpecker/build.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Test
|
||||||
|
image: "git.cutie.zone/lyssieth/baseimages:nix-docker"
|
||||||
|
commands:
|
||||||
|
- nix develop -c zig build test
|
||||||
2
Justfile
Normal file
2
Justfile
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
watch:
|
||||||
|
watchexec -e zig -c -- zig build test
|
||||||
28
build.zig
Normal file
28
build.zig
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const libRoot = b.path("src/root.zig");
|
||||||
|
|
||||||
|
const lib = b.addStaticLibrary(.{
|
||||||
|
.name = "lys",
|
||||||
|
.root_source_file = libRoot,
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
b.installArtifact(lib);
|
||||||
|
|
||||||
|
const libTests = b.addTest(.{
|
||||||
|
.root_source_file = libRoot,
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const libTestsRun = b.addRunArtifact(libTests);
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Run library tests");
|
||||||
|
test_step.dependOn(&libTestsRun.step);
|
||||||
|
}
|
||||||
10
build.zig.zon
Normal file
10
build.zig.zon
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
.{
|
||||||
|
.name = "lys",
|
||||||
|
.version = "0.0.0",
|
||||||
|
.dependencies = .{},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src/root.zig",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
just
|
just
|
||||||
|
watchexec
|
||||||
zig
|
zig
|
||||||
zls
|
zls
|
||||||
];
|
];
|
||||||
|
|
|
||||||
87
src/args/arg.zig
Normal file
87
src/args/arg.zig
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const Arg = union(enum(u2)) {
|
||||||
|
Flag: struct {
|
||||||
|
name: []const u8,
|
||||||
|
value: ?[]const u8,
|
||||||
|
consumed: bool = false,
|
||||||
|
},
|
||||||
|
Positional: struct {
|
||||||
|
value: []const u8,
|
||||||
|
idx: ?usize = undefined,
|
||||||
|
consumed: bool = false,
|
||||||
|
},
|
||||||
|
|
||||||
|
pub inline fn setConsumed(s: *Arg) void {
|
||||||
|
switch (s.*) {
|
||||||
|
.Flag => |*f| f.consumed = true,
|
||||||
|
.Positional => |*p| p.consumed = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn isConsumed(s: Arg) bool {
|
||||||
|
return switch (s) {
|
||||||
|
.Flag => |f| f.consumed,
|
||||||
|
.Positional => |p| p.consumed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_arg(arg: []const u8) !Arg {
|
||||||
|
var flag: ?Arg = null;
|
||||||
|
if (std.mem.startsWith(u8, arg, "--")) {
|
||||||
|
flag = try Arg.parse_flag(arg[2..]);
|
||||||
|
} else if (std.mem.startsWith(u8, arg, "-")) {
|
||||||
|
flag = try Arg.parse_flag(arg[1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag) |f| {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arg{
|
||||||
|
.Positional = .{
|
||||||
|
.value = arg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_flag(arg: []const u8) !Arg {
|
||||||
|
if (arg.len == 0) {
|
||||||
|
return error.EmptyFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = std.mem.indexOf(u8, arg, "=");
|
||||||
|
var flag: Arg = undefined;
|
||||||
|
if (idx) |i| {
|
||||||
|
const remainder = arg[i +| 1..];
|
||||||
|
|
||||||
|
if (remainder.len == 0) {
|
||||||
|
return error.NoValueButExpectedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flag = .{
|
||||||
|
.Flag = .{
|
||||||
|
.name = arg[0..i],
|
||||||
|
.value = arg[i +| 1..],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
flag = .{ .Flag = .{
|
||||||
|
.name = arg,
|
||||||
|
.value = null,
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (flag) {
|
||||||
|
.Flag => |f| {
|
||||||
|
if (f.name.len == 0) {
|
||||||
|
std.debug.print("failed to parse flag: {s}\n", .{arg});
|
||||||
|
return error.InvalidFlag;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Positional => unreachable,
|
||||||
|
}
|
||||||
|
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
};
|
||||||
458
src/args/args.zig
Normal file
458
src/args/args.zig
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const parsers = @import("./parsers/parsers.zig");
|
||||||
|
pub const validators = @import("./validators/validators.zig");
|
||||||
|
|
||||||
|
const Arg = @import("./arg.zig").Arg;
|
||||||
|
|
||||||
|
/// Metadata for a field that is parseable by the argument parser.
|
||||||
|
pub const Extra = union(enum(u2)) {
|
||||||
|
Positional,
|
||||||
|
Remainder,
|
||||||
|
Flag: struct {
|
||||||
|
name: []const u8,
|
||||||
|
short: ?[]const u8 = null,
|
||||||
|
toggle: bool = false,
|
||||||
|
takesValue: bool = false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A marker for any field `T` that you want to be parseable by the argument parser.
|
||||||
|
pub fn Marker(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
value: T,
|
||||||
|
extra: Extra,
|
||||||
|
/// Optional function that parses the value of the field.
|
||||||
|
/// If this is null, no parsing will take place.
|
||||||
|
///
|
||||||
|
/// This function can be any function that takes a `[]const u8` and returns `anyerror!T`.
|
||||||
|
///
|
||||||
|
/// However, it is recommended to use `lys.args.parsers` for common types.
|
||||||
|
parse: ?parsers.ParseSignature(T) = null,
|
||||||
|
/// Optional function that validates the value of the field.
|
||||||
|
/// If this is null, no validation will take place.
|
||||||
|
///
|
||||||
|
/// This function can be any function that takes a `[]const u8` and returns `anyerror!void`.
|
||||||
|
///
|
||||||
|
/// However, it is recommended to use `lys.args.validators` for common types.
|
||||||
|
validate: ?validators.ValidateSignature = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <https://git.cutie.zone/lyssieth/zither/issues/1>
|
||||||
|
pub fn parseArgs(comptime T: type, allocator: Allocator) !T {
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
return parseArgsImpl(T, allocator, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// actual implementation of parse_args, necessary for testing purposes.
|
||||||
|
/// The main implementation is in parse_args, which is a wrapper around this.
|
||||||
|
fn parseArgsImpl(comptime T: type, allocator: Allocator, args: [][]const u8) !T {
|
||||||
|
var flags = std.ArrayList(Arg).init(allocator);
|
||||||
|
defer flags.deinit();
|
||||||
|
|
||||||
|
if (args.len < 2) {
|
||||||
|
return error.NoArguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (args, 0..) |arg, idx| {
|
||||||
|
if (idx == 0) continue; // skip first arg: process name
|
||||||
|
|
||||||
|
const argument = try Arg.parse_arg(arg);
|
||||||
|
|
||||||
|
try flags.append(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
for (0..flags.items.len) |idx| {
|
||||||
|
const flag = &flags.items[idx];
|
||||||
|
switch (flag.*) {
|
||||||
|
.Positional => |*p| {
|
||||||
|
p.idx = i;
|
||||||
|
i += 1;
|
||||||
|
},
|
||||||
|
.Flag => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = initFromParsed(T, allocator, flags.items);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper that finds a flag by name (or short name), if it's not consumed
|
||||||
|
fn find(flags: []Arg, expected_long: []const u8, expected_short: ?[]const u8) ?*Arg {
|
||||||
|
for (flags) |*flag| {
|
||||||
|
if (flag.isConsumed()) continue; // skip consumed flags
|
||||||
|
|
||||||
|
switch (flag.*) {
|
||||||
|
.Flag => |f| {
|
||||||
|
if (std.mem.eql(u8, f.name, expected_long)) {
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expected_short == null) continue;
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, f.name, expected_short.?)) {
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Positional => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// helper that takes the next non-consumed positional argument
|
||||||
|
fn nextPositional(flags: []Arg) ?*Arg {
|
||||||
|
for (flags) |*flag| {
|
||||||
|
if (flag.isConsumed()) continue; // skip consumed flags
|
||||||
|
|
||||||
|
switch (flag.*) {
|
||||||
|
.Flag => continue,
|
||||||
|
.Positional => |_| {
|
||||||
|
return flag;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does the actual work of initializing T from flags.
|
||||||
|
fn initFromParsed(comptime T: type, allocator: Allocator, flags: []Arg) !T {
|
||||||
|
comptime {
|
||||||
|
std.debug.assert(@hasField(T, "allocator"));
|
||||||
|
std.debug.assert(@hasDecl(T, "deinit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = T{
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
errdefer result.deinit();
|
||||||
|
|
||||||
|
const this = @TypeOf(result);
|
||||||
|
const info = @typeInfo(this);
|
||||||
|
|
||||||
|
if ((info.Struct.fields.len -| 1) == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (info.Struct.fields) |field| {
|
||||||
|
if (comptime std.mem.eql(u8, field.name, "allocator")) {
|
||||||
|
continue; // skip allocator
|
||||||
|
}
|
||||||
|
|
||||||
|
const fie = &@field(result, field.name);
|
||||||
|
const extra = fie.*.extra;
|
||||||
|
|
||||||
|
switch (extra) {
|
||||||
|
.Flag => |f| {
|
||||||
|
if (!(f.takesValue or f.toggle)) {
|
||||||
|
std.debug.print("error: invalid flag `{s}`: is not a toggle and doesn't take a value\n", .{f.name});
|
||||||
|
return error.InvalidFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybe_flag = find(flags, f.name, f.short);
|
||||||
|
|
||||||
|
if (maybe_flag) |flag| {
|
||||||
|
flag.setConsumed();
|
||||||
|
if (f.toggle) {
|
||||||
|
if (@TypeOf(fie.*.value) == bool) {
|
||||||
|
fie.*.value = true;
|
||||||
|
comptime continue;
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: invalid switch {s}: expected bool, found {s}\n", .{
|
||||||
|
f.name,
|
||||||
|
@typeName(@TypeOf(fie.*.value)),
|
||||||
|
});
|
||||||
|
return error.InvalidSwitch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.takesValue) {
|
||||||
|
if (flag.*.Flag.value) |value| {
|
||||||
|
if (@TypeOf(fie.*.value) == []const u8) {
|
||||||
|
fie.*.value = try result.allocator.dupe(u8, value);
|
||||||
|
comptime continue;
|
||||||
|
} else {
|
||||||
|
if (fie.*.parse) |parse_fn| {
|
||||||
|
fie.*.value = try parse_fn(value);
|
||||||
|
comptime continue;
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: flag `{s}` expected a value, but no parser was provided\n", .{f.name});
|
||||||
|
return error.NoValueForFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: flag `{s}` expected a value, but none was provided\n", .{f.name});
|
||||||
|
return error.NoValueForFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.print("error: invalid flag `{s}`: is not a toggle and doesn't take a value\n", .{f.name});
|
||||||
|
return error.InvalidFlag;
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: invalid flag `{s}`: could not find flag\n", .{f.name});
|
||||||
|
return error.InvalidFlag;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Remainder => {
|
||||||
|
var not_consumed = std.ArrayList([]const u8).init(result.allocator);
|
||||||
|
errdefer not_consumed.deinit();
|
||||||
|
|
||||||
|
for (flags) |*flag| {
|
||||||
|
if (flag.isConsumed()) continue;
|
||||||
|
|
||||||
|
switch (flag.*) {
|
||||||
|
.Flag => |f| {
|
||||||
|
std.debug.print("TODO: figure out what to do with remainder flag {s} (value: {?s})\n", .{
|
||||||
|
f.name,
|
||||||
|
f.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.Positional => |p| {
|
||||||
|
flag.setConsumed();
|
||||||
|
try not_consumed.append(try result.allocator.dupe(u8, p.value));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@TypeOf(fie.*.value) == std.ArrayList([]const u8)) {
|
||||||
|
fie.*.value = not_consumed;
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: invalid remainder {s}: expected `std.ArrayList([] const u8)`, got `{s}`\n", .{
|
||||||
|
field.name,
|
||||||
|
@typeName(@TypeOf(fie.*.value)),
|
||||||
|
});
|
||||||
|
return error.InvalidRemainder;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Positional => {
|
||||||
|
const next = nextPositional(flags);
|
||||||
|
|
||||||
|
if (next) |flag| {
|
||||||
|
flag.setConsumed();
|
||||||
|
if (@TypeOf(fie.*.value) == []const u8) {
|
||||||
|
fie.*.value = try result.allocator.dupe(u8, flag.*.Positional.value);
|
||||||
|
} else if (fie.*.parse) |parse_fn| {
|
||||||
|
fie.*.value = parse_fn(flag.*.Positional.value) catch |err| {
|
||||||
|
std.debug.print("error: could not parse positional argument for {s}: {s}\n", .{
|
||||||
|
field.name,
|
||||||
|
@errorName(err),
|
||||||
|
});
|
||||||
|
std.debug.print("- tried to parse: {s}\n", .{flag.*.Positional.value});
|
||||||
|
std.debug.print("- expected type: {s}\n", .{@typeName(@TypeOf(fie.*.value))});
|
||||||
|
return error.InvalidPositional;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: could not parse positional argument for {s}\n", .{field.name});
|
||||||
|
return error.InvalidPositional;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std.debug.print("error: could not find positional argument for {s}\n", .{field.name});
|
||||||
|
return error.NoArgumentFound;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
test "parse args" {
|
||||||
|
const args = try t.allocator.alloc([]const u8, 4);
|
||||||
|
defer t.allocator.free(args);
|
||||||
|
|
||||||
|
const Demo = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
flag: Marker([]const u8) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Flag = .{
|
||||||
|
.name = "flag",
|
||||||
|
.takesValue = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toggle: Marker(bool) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Flag = .{
|
||||||
|
.name = "toggle",
|
||||||
|
.toggle = true,
|
||||||
|
.takesValue = false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
positional: Marker([]const u8) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Positional = {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
fn deinit(self: *@This()) void {
|
||||||
|
self.allocator.free(self.flag.value);
|
||||||
|
self.allocator.free(self.positional.value);
|
||||||
|
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
args[0] = "";
|
||||||
|
args[1] = "--flag=value";
|
||||||
|
args[2] = "--toggle";
|
||||||
|
args[3] = "positional";
|
||||||
|
|
||||||
|
var result = try parseArgsImpl(Demo, t.allocator, args);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
try t.expectEqualStrings("value", result.flag.value);
|
||||||
|
try t.expect(result.toggle.value);
|
||||||
|
try t.expectEqualStrings("positional", result.positional.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse fn (positional)" {
|
||||||
|
const args = try t.allocator.alloc([]const u8, 4);
|
||||||
|
defer t.allocator.free(args);
|
||||||
|
|
||||||
|
args[1] = "1234";
|
||||||
|
args[2] = "true";
|
||||||
|
args[3] = "Value";
|
||||||
|
|
||||||
|
const DemoEnum = enum(u8) {
|
||||||
|
NotValue,
|
||||||
|
Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Demo = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
flag: Marker(u16) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Positional = {},
|
||||||
|
},
|
||||||
|
.parse = parsers.num(u16),
|
||||||
|
},
|
||||||
|
|
||||||
|
boolean: Marker(bool) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Positional = {},
|
||||||
|
},
|
||||||
|
.parse = parsers.boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
enumeration: Marker(DemoEnum) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Positional = {},
|
||||||
|
},
|
||||||
|
.parse = parsers.enumLiteral(DemoEnum),
|
||||||
|
},
|
||||||
|
|
||||||
|
fn deinit(self: *@This()) void {
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = try parseArgsImpl(Demo, t.allocator, args);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
try t.expectEqual(1234, result.flag.value);
|
||||||
|
try t.expect(result.boolean.value);
|
||||||
|
try t.expectEqual(DemoEnum.Value, result.enumeration.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse fn (flag)" {
|
||||||
|
const args = try t.allocator.alloc([]const u8, 4);
|
||||||
|
defer t.allocator.free(args);
|
||||||
|
|
||||||
|
args[1] = "--number=1234";
|
||||||
|
args[2] = "--boolean=yes";
|
||||||
|
args[3] = "--enumeration=Value";
|
||||||
|
|
||||||
|
const DemoEnum = enum(u8) {
|
||||||
|
NotValue,
|
||||||
|
Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Demo = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
number: Marker(u16) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Flag = .{
|
||||||
|
.name = "number",
|
||||||
|
.takesValue = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.parse = parsers.num(u16),
|
||||||
|
},
|
||||||
|
|
||||||
|
boolean: Marker(bool) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Flag = .{
|
||||||
|
.name = "boolean",
|
||||||
|
.takesValue = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.parse = parsers.boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
enumeration: Marker(DemoEnum) = .{
|
||||||
|
.value = undefined,
|
||||||
|
.extra = Extra{
|
||||||
|
.Flag = .{
|
||||||
|
.name = "enumeration",
|
||||||
|
.takesValue = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.parse = parsers.enumLiteral(DemoEnum),
|
||||||
|
},
|
||||||
|
|
||||||
|
fn deinit(self: *@This()) void {
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = try parseArgsImpl(Demo, t.allocator, args);
|
||||||
|
defer result.deinit();
|
||||||
|
|
||||||
|
try t.expectEqual(1234, result.number.value);
|
||||||
|
try t.expect(result.boolean.value);
|
||||||
|
try t.expectEqual(DemoEnum.Value, result.enumeration.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse failure because no args" {
|
||||||
|
const args = try t.allocator.alloc([]const u8, 1);
|
||||||
|
defer t.allocator.free(args);
|
||||||
|
|
||||||
|
const Demo = struct {
|
||||||
|
allocator: Allocator,
|
||||||
|
|
||||||
|
fn deinit(self: *@This()) void {
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
args[0] = "zither";
|
||||||
|
|
||||||
|
const result = parseArgsImpl(Demo, t.allocator, args);
|
||||||
|
|
||||||
|
try t.expectError(error.NoArguments, result);
|
||||||
|
}
|
||||||
49
src/args/parsers/boolean.zig
Normal file
49
src/args/parsers/boolean.zig
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const TrueValues = [_][]const u8{ "true", "yes", "y", "on", "1" };
|
||||||
|
const FalseValues = [_][]const u8{ "false", "no", "n", "off", "0" };
|
||||||
|
|
||||||
|
pub fn boolean(value: []const u8) anyerror!bool {
|
||||||
|
if (value.len > 10) {
|
||||||
|
return error.InvalidBool_ValueTooLong;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf: [10]u8 = undefined;
|
||||||
|
const lower = std.ascii.lowerString(&buf, value);
|
||||||
|
|
||||||
|
inline for (TrueValues) |true_value| {
|
||||||
|
if (std.mem.eql(u8, lower, true_value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (FalseValues) |false_value| {
|
||||||
|
if (std.mem.eql(u8, lower, false_value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidBool;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "bool parser" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
const ExpectTrue = [_][]const u8{ "true", "TrUe", "TRUE", "1", "yes", "Y", "on" };
|
||||||
|
const ExpectFalse = [_][]const u8{ "false", "FaLsE", "FALSE", "0", "no", "N", "off" };
|
||||||
|
const ExpectError = [_][]const u8{ "wrong", "maybe", "1.0", "0.0", "truefalse" };
|
||||||
|
|
||||||
|
inline for (ExpectTrue) |expect| {
|
||||||
|
try t.expect(try boolean(expect) == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (ExpectFalse) |expect| {
|
||||||
|
try t.expect(try boolean(expect) == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (ExpectError) |expect| {
|
||||||
|
try t.expectError(error.InvalidBool, boolean(expect));
|
||||||
|
}
|
||||||
|
|
||||||
|
try t.expectError(error.InvalidBool_ValueTooLong, boolean("1234567890123456789012345678901234567890"));
|
||||||
|
}
|
||||||
43
src/args/parsers/enumLiteral.zig
Normal file
43
src/args/parsers/enumLiteral.zig
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn enumLiteral(comptime T: type) *const fn (value: []const u8) anyerror!T {
|
||||||
|
const container = struct {
|
||||||
|
fn func(value: []const u8) anyerror!T {
|
||||||
|
return parseEnumFromStr(T, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return &container.func;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseEnumFromStr(comptime T: type, str: []const u8) !T {
|
||||||
|
const info = @typeInfo(T);
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
if (std.meta.activeTag(info) != .Enum) {
|
||||||
|
@compileError("Expected enum type, got " ++ @typeName(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (info.Enum.fields) |field| {
|
||||||
|
if (std.mem.eql(u8, field.name, str)) {
|
||||||
|
return @enumFromInt(field.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "enum literal" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
const Demo = enum(u8) {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
};
|
||||||
|
|
||||||
|
try t.expectEqual(Demo.A, try parseEnumFromStr(Demo, "A"));
|
||||||
|
try t.expectEqual(Demo.B, try parseEnumFromStr(Demo, "B"));
|
||||||
|
|
||||||
|
try t.expectError(error.InvalidEnum, parseEnumFromStr(Demo, "DefinitelyNot"));
|
||||||
|
}
|
||||||
11
src/args/parsers/num.zig
Normal file
11
src/args/parsers/num.zig
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn num(comptime T: type) *const fn (value: []const u8) anyerror!T {
|
||||||
|
const container = struct {
|
||||||
|
fn func(value: []const u8) anyerror!T {
|
||||||
|
return std.fmt.parseInt(T, value, 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return &container.func;
|
||||||
|
}
|
||||||
32
src/args/parsers/parsers.zig
Normal file
32
src/args/parsers/parsers.zig
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// A signature for a 'parse' function. It takes a string and returns an error or a value.
|
||||||
|
///
|
||||||
|
/// This function should *not* be called directly, but rather through the `parse` field of a `Marker`.
|
||||||
|
pub fn ParseSignature(comptime T: type) type {
|
||||||
|
return *const fn (value: []const u8) anyerror!T;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const num = @import("./num.zig").num;
|
||||||
|
pub const boolean = @import("./boolean.zig").boolean;
|
||||||
|
pub const enumLiteral = @import("./enumLiteral.zig").enumLiteral;
|
||||||
|
|
||||||
|
test "valid parser signatures" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
try t.expectEqual(ParseSignature(u8), @TypeOf(num(u8)));
|
||||||
|
try t.expectEqual(ParseSignature(bool), @TypeOf(&boolean));
|
||||||
|
|
||||||
|
const Enum = enum { Value, Other };
|
||||||
|
try t.expectEqual(ParseSignature(Enum), @TypeOf(enumLiteral(Enum)));
|
||||||
|
}
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
if (builtin.is_test) {
|
||||||
|
std.mem.doNotOptimizeAway(num);
|
||||||
|
std.mem.doNotOptimizeAway(boolean);
|
||||||
|
std.mem.doNotOptimizeAway(enumLiteral);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/args/validators/number.zig
Normal file
0
src/args/validators/number.zig
Normal file
32
src/args/validators/string.zig
Normal file
32
src/args/validators/string.zig
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
pub fn notEmpty(value: []const u8) anyerror!bool {
|
||||||
|
if (value.len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "not empty" {
|
||||||
|
try t.expect(try notEmpty("") == false);
|
||||||
|
try t.expect(try notEmpty("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notWhiteSpace(value: []const u8) anyerror!bool {
|
||||||
|
if (std.mem.trim(u8, value, " \t\n\r").len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "not white space" {
|
||||||
|
try t.expect(try notWhiteSpace("") == false);
|
||||||
|
try t.expect(try notWhiteSpace("\n\r\t ") == false);
|
||||||
|
try t.expect(try notWhiteSpace("a"));
|
||||||
|
try t.expect(try notWhiteSpace(" a"));
|
||||||
|
try t.expect(try notWhiteSpace("\ta"));
|
||||||
|
try t.expect(try notWhiteSpace("\n\r\t a"));
|
||||||
|
}
|
||||||
37
src/args/validators/validators.zig
Normal file
37
src/args/validators/validators.zig
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// A function that validates a string value.
|
||||||
|
/// It should return an error if the value is unparseable.
|
||||||
|
/// Or `true` if the value is valid.
|
||||||
|
/// Or `false` if the value is invalid.
|
||||||
|
pub const ValidateSignature = *const fn (value: []const u8) anyerror!bool;
|
||||||
|
|
||||||
|
pub const string = @import("./string.zig");
|
||||||
|
pub const number = @import("./number.zig");
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
if (builtin.is_test) {
|
||||||
|
std.mem.doNotOptimizeAway(string);
|
||||||
|
std.mem.doNotOptimizeAway(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const this = @This();
|
||||||
|
test "valid validator signatures" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
const decls = @typeInfo(this).Struct.decls;
|
||||||
|
|
||||||
|
inline for (decls) |decl| {
|
||||||
|
const field = @field(this, decl.name);
|
||||||
|
if (comptime std.mem.startsWith(u8, @typeName(field), "*")) {
|
||||||
|
comptime continue; // skip `ValidateSignature`
|
||||||
|
}
|
||||||
|
|
||||||
|
inline for (@typeInfo(field).Struct.decls) |function| {
|
||||||
|
try t.expectEqual(ValidateSignature, @TypeOf(&@field(field, function.name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/root.zig
Normal file
13
src/root.zig
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const args = @import("./args/args.zig");
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
// A hack to prevent the compiler from optimizing tests and "exports" away.
|
||||||
|
// but only in `Debug` mode. Hopefully.
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
if (builtin.is_test) {
|
||||||
|
std.mem.doNotOptimizeAway(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue