-
Notifications
You must be signed in to change notification settings - Fork 188
Better argument parser #1425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Argmaster
wants to merge
39
commits into
PixelGuys:master
Choose a base branch
from
Argmaster:feature/declargs
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+443
−27
Open
Better argument parser #1425
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
4297ad8
Add prototype of declarative argument parser
Argmaster b3b9e3a
Add prototype of introspection based argument parser
Argmaster 75c3a68
Change prototype design
Argmaster 2ea6565
Adjust the design as requested by Quantum
Argmaster 8b052ff
Unpack structs + migrate TP command
Argmaster 691d39c
Update src/server/command/tp.zig
Argmaster c9a563a
Autocompletion sketch, requests, other stuff
Argmaster bc37a3e
Improve error message on failure
Argmaster 0ac44e4
Add checkExists param to BiomeId to enable testing
Argmaster f00de3b
Apply message suggestions
Argmaster 2b0515d
Remove extra empty line between error messages
Argmaster 85aa3d3
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster 38923d4
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster 2fbb740
Use argument names for error messages
Argmaster f25893a
Fix text
Argmaster 350f46f
Foos and bars may break my spams but pr needs some features
Argmaster eaef925
Tests go fix fix
Argmaster 57c59fc
Replace manual loop with std.mem.concat
Argmaster 598982f
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster a4bc1e8
Fixes yet again
Argmaster 668655b
Merge branch 'master' into feature/declargs
Argmaster fe4d8e3
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster 738feb6
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster 3e44684
Use our param for errors
Argmaster eb1c602
Idk how those bugs got here
Argmaster b6eb4aa
Remove continue compiler complains about
Argmaster bebc17e
This is not working for some reason
Argmaster 13ac15d
Use errors for arguments
Argmaster 52da735
Now it works
Argmaster 3e532b1
Return error.ParseError
Argmaster 33443ac
Discard changes from tp.zig
Argmaster 1e1d0f2
Discard changes from biomes.zig
Argmaster 47e7d1b
Merge remote-tracking branch 'origin/master' into feature/declargs
Argmaster d5dfb5b
Discard biome id implementation
Argmaster 0fbf47f
Remove biome id tests
Argmaster 86cf529
Update time command
Argmaster fa71fca
Use enum instead of boolean
Argmaster 841e672
Polish behavior quirks of enums and whitespace
Argmaster 84b291d
Reorder cases in args switch of time
Argmaster File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,382 @@ | ||
| const std = @import("std"); | ||
|
|
||
| const main = @import("main"); | ||
| const NeverFailingAllocator = main.heap.NeverFailingAllocator; | ||
| const ListUnmanaged = main.ListUnmanaged; | ||
| const utils = main.utils; | ||
|
|
||
| pub const Options = struct { | ||
| commandName: []const u8, | ||
| }; | ||
|
|
||
| const ResolveMode = enum { | ||
| Parse, | ||
| Autocomplete, | ||
| }; | ||
|
|
||
| pub fn Parser(comptime T: type, comptime options: Options) type { | ||
| return struct { | ||
| const Self = @This(); | ||
|
|
||
| pub fn parse(allocator: NeverFailingAllocator, args: []const u8, errorMessage: *ListUnmanaged(u8)) error{ParseError}!T { | ||
| return resolve(ResolveMode.Parse, allocator, args, errorMessage); | ||
| } | ||
|
|
||
| pub fn autocomplete(allocator: NeverFailingAllocator, args: []const u8, errorMessage: *ListUnmanaged(u8)) AutocompleteResult { | ||
| return resolve(ResolveMode.Autocomplete, allocator, args, errorMessage); | ||
| } | ||
|
|
||
| pub fn resolve( | ||
| comptime mode: ResolveMode, | ||
| allocator: NeverFailingAllocator, | ||
| args: []const u8, | ||
| errorMessage: *ListUnmanaged(u8), | ||
| ) switch(mode) { | ||
| .Autocomplete => AutocompleteResult, | ||
| .Parse => error{ParseError}!T, | ||
| } { | ||
| switch(@typeInfo(T)) { | ||
| inline .@"struct" => |s| { | ||
| return resolveStruct(mode, s, allocator, args, errorMessage); | ||
| }, | ||
| inline .@"union" => |u| { | ||
| if(u.tag_type == null) @compileError("Union must have a tag type"); | ||
| return switch(mode) { | ||
| .Autocomplete => autocompleteUnion(u, allocator, args, errorMessage), | ||
| .Parse => parseUnion(u, allocator, args, errorMessage), | ||
| }; | ||
| }, | ||
| else => @compileError("Only structs and unions are supported"), | ||
| } | ||
| } | ||
|
|
||
| fn resolveStruct( | ||
| comptime mode: ResolveMode, | ||
| comptime s: std.builtin.Type.Struct, | ||
| allocator: NeverFailingAllocator, | ||
| args: []const u8, | ||
| errorMessage: *ListUnmanaged(u8), | ||
| ) switch(mode) { | ||
| .Autocomplete => AutocompleteResult, | ||
| .Parse => error{ParseError}!T, | ||
| } { | ||
| var result: T = undefined; | ||
| var split = std.mem.splitScalar(u8, args, ' '); | ||
|
|
||
| var tempErrorMessage: ListUnmanaged(u8) = .{}; | ||
| defer tempErrorMessage.deinit(allocator); | ||
|
|
||
| var nextArgument: ?[]const u8 = split.next(); | ||
|
|
||
| inline for(s.fields) |field| { | ||
| const value = resolveArgument(field.type, allocator, field.name[0..], nextArgument, &tempErrorMessage); | ||
|
|
||
| if(value == error.ParseError) { | ||
| if(@typeInfo(field.type) == .optional) { | ||
| @field(result, field.name) = null; | ||
| tempErrorMessage.clearRetainingCapacity(); | ||
| } else { | ||
| errorMessage.appendSlice(allocator, tempErrorMessage.items); | ||
| return error.ParseError; | ||
| } | ||
| } else { | ||
| @field(result, field.name) = value catch unreachable; | ||
| tempErrorMessage.clearRetainingCapacity(); | ||
| nextArgument = split.next(); | ||
| } | ||
| } | ||
|
|
||
| const isLeftoverArgumentWhitespace = nextArgument == null or main.utils.all(std.ascii.isWhitespace, nextArgument.?); | ||
| const areUnusedArgumentsWhitespace = main.utils.all(std.ascii.isWhitespace, split.rest()); | ||
|
|
||
| if(!isLeftoverArgumentWhitespace or !areUnusedArgumentsWhitespace) { | ||
| failWithMessage(allocator, errorMessage, "Too many arguments for command, expected {}", .{s.fields.len}); | ||
| return error.ParseError; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| fn resolveArgument(comptime Field: type, allocator: NeverFailingAllocator, name: []const u8, argument: ?[]const u8, errorMessage: *ListUnmanaged(u8)) error{ParseError}!Field { | ||
| switch(@typeInfo(Field)) { | ||
| inline .optional => |optionalInfo| { | ||
| if(argument == null) return error.ParseError; | ||
| return resolveArgument(optionalInfo.child, allocator, name, argument, errorMessage) catch |err| { | ||
| return err; | ||
| }; | ||
| }, | ||
| inline .@"struct" => { | ||
| const arg = argument orelse { | ||
| failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name}); | ||
| return error.ParseError; | ||
| }; | ||
| if(!@hasDecl(Field, "parse")) @compileError("Struct must have a parse function"); | ||
| return @field(Field, "parse")(allocator, name, arg, errorMessage); | ||
| }, | ||
| inline .@"enum" => { | ||
| const arg = argument orelse { | ||
| failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name}); | ||
| return error.ParseError; | ||
| }; | ||
| return std.meta.stringToEnum(Field, arg) orelse { | ||
| const str = main.meta.concatComptime("/", std.meta.fieldNames(Field)); | ||
| failWithMessage(allocator, errorMessage, "Expected one of {s} for <{s}>, found \"{s}\"", .{str, name, arg}); | ||
| return error.ParseError; | ||
| }; | ||
| }, | ||
| inline .float => |floatInfo| return { | ||
| const arg = argument orelse { | ||
| failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name}); | ||
| return error.ParseError; | ||
| }; | ||
| return std.fmt.parseFloat(std.meta.Float(floatInfo.bits), arg) catch { | ||
| failWithMessage(allocator, errorMessage, "Expected a number for <{s}>, found \"{s}\"", .{name, arg}); | ||
| return error.ParseError; | ||
| }; | ||
| }, | ||
| inline .int => |intInfo| { | ||
| const arg = argument orelse { | ||
| failWithMessage(allocator, errorMessage, missingArgumentMessage, .{name}); | ||
| return error.ParseError; | ||
| }; | ||
| return std.fmt.parseInt(std.meta.Int(intInfo.signedness, intInfo.bits), arg, 0) catch { | ||
| failWithMessage(allocator, errorMessage, "Expected an integer for <{s}>, found \"{s}\"", .{name, arg}); | ||
| return error.ParseError; | ||
| }; | ||
| }, | ||
| inline else => |other| @compileError("Unsupported type " ++ @tagName(other)), | ||
| } | ||
| } | ||
|
|
||
| const missingArgumentMessage = "Missing argument at position <{s}>"; | ||
|
|
||
| fn autocompleteArgument(comptime Field: type, allocator: NeverFailingAllocator, _arg: ?[]const u8) AutocompleteResult { | ||
| const arg = _arg orelse return .{}; | ||
| switch(@typeInfo(Field)) { | ||
| inline .@"struct" => { | ||
| if(!@hasDecl(Field, "autocomplete")) @compileError("Struct must have an autocomplete function"); | ||
| return try @field(Field, "autocomplete")(allocator, arg); | ||
| }, | ||
| inline .@"enum" => { | ||
| var result: AutocompleteResult = .{}; | ||
| inline for(std.meta.fieldNames(Field)) |fieldName| { | ||
| if(!std.mem.startsWith(u8, fieldName, arg)) continue; | ||
| result.suggestions.append(allocator, allocator.dupe(u8, fieldName)); | ||
| } | ||
| return result; | ||
| }, | ||
| inline else => return .{}, | ||
| } | ||
| } | ||
|
|
||
| fn parseUnion(comptime u: std.builtin.Type.Union, allocator: NeverFailingAllocator, args: []const u8, errorMessage: *ListUnmanaged(u8)) error{ParseError}!T { | ||
| var tempErrorMessage: ListUnmanaged(u8) = .{}; | ||
| defer tempErrorMessage.deinit(allocator); | ||
|
|
||
| tempErrorMessage.appendSlice(allocator, "---"); | ||
|
|
||
| inline for(u.fields) |field| { | ||
| tempErrorMessage.append(allocator, '\n'); | ||
| tempErrorMessage.appendSlice(allocator, field.name); | ||
| tempErrorMessage.append(allocator, '\n'); | ||
|
|
||
| const result = Parser(field.type, options).resolve(.Parse, allocator, args, &tempErrorMessage); | ||
| if(result != error.ParseError) { | ||
Argmaster marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return @unionInit(T, field.name, result catch unreachable); | ||
| } | ||
| tempErrorMessage.appendSlice(allocator, "\n---"); | ||
| } | ||
|
|
||
| errorMessage.appendSlice(allocator, tempErrorMessage.items); | ||
| return error.ParseError; | ||
| } | ||
|
|
||
| fn autocompleteUnion(comptime u: std.builtin.Type.Union, allocator: NeverFailingAllocator, args: []const u8) AutocompleteResult { | ||
| var result: AutocompleteResult = .{}; | ||
|
|
||
| inline for(u.fields) |field| { | ||
| var completion = Parser(field.type).resolve(true, allocator, args); | ||
| defer completion.deinit(allocator); | ||
|
|
||
| result.takeSuggestions(allocator, &completion); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| fn failWithMessage(allocator: NeverFailingAllocator, errorMessage: *ListUnmanaged(u8), comptime fmt: []const u8, args: anytype) void { | ||
| const msg = std.fmt.allocPrint(allocator.allocator, fmt, args) catch unreachable; | ||
| defer allocator.free(msg); | ||
| errorMessage.appendSlice(allocator, msg); | ||
| } | ||
|
|
||
| pub const AutocompleteResult = struct { | ||
| suggestions: ListUnmanaged([]const u8) = .{}, | ||
|
|
||
| pub fn takeSuggestions(self: *AutocompleteResult, allocator: NeverFailingAllocator, other: *AutocompleteResult) void { | ||
| for(other.suggestions.items) |message| { | ||
| self.suggestions.append(allocator, message); | ||
| } | ||
| other.suggestions.clearAndFree(allocator); | ||
| } | ||
|
|
||
| pub fn deinit(self: AutocompleteResult, allocator: NeverFailingAllocator) void { | ||
| for(self.suggestions.items) |item| { | ||
| allocator.free(item); | ||
| } | ||
| self.suggestions.deinit(allocator); | ||
| } | ||
| }; | ||
|
|
||
| const Test = struct { | ||
| var testingAllocator = main.heap.ErrorHandlingAllocator.init(std.testing.allocator); | ||
| var allocator = testingAllocator.allocator(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also not needed with #1286 |
||
|
|
||
| const OnlyX = Parser(struct {x: f64}, .{.commandName = ""}); | ||
|
|
||
| const @"Union X or XY" = Parser(union(enum) { | ||
| x: struct {x: f64}, | ||
| xy: struct {x: f64, y: f64}, | ||
| }, .{.commandName = ""}); | ||
|
|
||
| const @"subCommands foo or bar" = Parser(union(enum) { | ||
| foo: struct {cmd: enum(u1) {foo}, x: f64}, | ||
| bar: struct {cmd: enum(u1) {bar}, x: f64, y: f64}, | ||
| }, .{.commandName = ""}); | ||
| }; | ||
|
|
||
| test "float" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try Test.OnlyX.parse(Test.allocator, "33.0", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.x == 33.0); | ||
| } | ||
|
|
||
| test "float negative" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = Test.OnlyX.parse(Test.allocator, "foo", &errors); | ||
|
|
||
| try std.testing.expectError(error.ParseError, result); | ||
| try std.testing.expect(errors.items.len != 0); | ||
| } | ||
|
|
||
| test "enum" { | ||
| const ArgParser = Parser(struct { | ||
| cmd: enum(u1) {foo}, | ||
| }, .{.commandName = "c"}); | ||
|
|
||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try ArgParser.parse(Test.allocator, "foo", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.cmd == .foo); | ||
| } | ||
|
|
||
| test "float int float" { | ||
| const ArgParser = Parser(struct { | ||
| x: f64, | ||
| y: i32, | ||
| z: f32, | ||
| }, .{.commandName = ""}); | ||
|
|
||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try ArgParser.parse(Test.allocator, "33.0 154 -5654.0", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.x == 33.0); | ||
| try std.testing.expect(result.y == 154); | ||
| try std.testing.expect(result.z == -5654.0); | ||
| } | ||
|
|
||
| test "float int optional float missing" { | ||
| const ArgParser = Parser(struct { | ||
| x: f64, | ||
| y: i32, | ||
| z: ?f32, | ||
| }, .{.commandName = ""}); | ||
|
|
||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try ArgParser.parse(Test.allocator, "33.0 154", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.x == 33.0); | ||
| try std.testing.expect(result.y == 154); | ||
| try std.testing.expect(result.z == null); | ||
| } | ||
|
|
||
| test "x or xy case x" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try Test.@"Union X or XY".parse(Test.allocator, "0.9", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.x.x == 0.9); | ||
| } | ||
|
|
||
| test "x or xy case xy" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try Test.@"Union X or XY".parse(Test.allocator, "0.9 1.0", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.xy.x == 0.9); | ||
| try std.testing.expect(result.xy.y == 1.0); | ||
| } | ||
|
|
||
| test "x or xy negative empty" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = Test.@"Union X or XY".parse(Test.allocator, "", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len != 0); | ||
| try std.testing.expectError(error.ParseError, result); | ||
| } | ||
|
|
||
| test "x or xy negative too much" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = Test.@"Union X or XY".parse(Test.allocator, "1.0 3.0 5.0", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len != 0); | ||
| try std.testing.expectError(error.ParseError, result); | ||
| } | ||
|
|
||
| test "subCommands foo" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try Test.@"subCommands foo or bar".parse(Test.allocator, "foo 1.0", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.foo.cmd == .foo); | ||
| try std.testing.expect(result.foo.x == 1.0); | ||
| } | ||
|
|
||
| test "subCommands bar" { | ||
| var errors: ListUnmanaged(u8) = .{}; | ||
| defer errors.deinit(Test.allocator); | ||
|
|
||
| const result = try Test.@"subCommands foo or bar".parse(Test.allocator, "bar 2.0 3.0", &errors); | ||
|
|
||
| try std.testing.expect(errors.items.len == 0); | ||
| try std.testing.expect(result.bar.cmd == .bar); | ||
| try std.testing.expect(result.bar.x == 2.0); | ||
| try std.testing.expect(result.bar.y == 3.0); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All local allocations should use the stackAllocator
@codemob-dev we need the fix for #1286
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not again...