docopt / docopt.net

Port of docopt to .net
https://docopt.github.io/docopt.net/
Other
353 stars 33 forks source link

Add MatchAsync to `IParser<T>.IResult` #187

Closed giggio closed 1 year ago

giggio commented 1 year ago

It is common that the executing code is async, and it is not easily doable with the current API. We need a Task/ValueTask returning Func for the first argument, so async/await can be used.

This is my current workaround:

ProgramArguments programArguments;
var result = ProgramArguments.CreateParser()
.WithVersion("Naval Fate 2.0")
.Parse(args)
.Match(p => { programArguments = p; return -1; },
    result => { WriteLine(result.Help); return 0; },
    result => { WriteLine(result.Version); return 0; },
    result => { Error.WriteLine(result.Usage); return 1; });
if (result > -1) return result;
// continues with async/await...
return 0;
atifaziz commented 1 year ago

You can do this today without the need for an async version of Match. Just return a Task<int> from each Match function argument instead of int, like this:

return await
    ProgramArguments.CreateParser()
                    .WithVersion("Naval Fate 2.0")
                    .Parse(args)
                    .Match(Main,
                           result => { WriteLine(result.Help); return Task.FromResult(0); },
                           result => { WriteLine(result.Version); return Task.FromResult(0); },
                           result => { Error.WriteLine(result.Usage); return Task.FromResult(1); });

static async Task<int> Main(ProgramArguments args)
{
    // ...

    return 0;
}

If you don't have a Main method, then you can also do this, where you unify ProgramArguments and int via object:

var result =
    ProgramArguments.CreateParser()
                    .WithVersion("Naval Fate 2.0")
                    .Parse(args)
                    .Match(args   => (object)args,
                           result => { WriteLine(result.Help); return 0; },
                           result => { WriteLine(result.Version); return 0; },
                           result => { Error.WriteLine(result.Usage); return 1; });

if (result is not ProgramArguments programArguments)
    return (int)result;
// continues with async/await...
return 0;

If you don't like the cast to object, you can use a generic discriminated union from OneOf as shown next:

var result =
    ProgramArguments.CreateParser()
                    .WithVersion("Naval Fate 2.0")
                    .Parse(args)
                    .Match(OneOf<ProgramArguments, int>.FromT0,
                           result => { WriteLine(result.Help); return OneOf<ProgramArguments, int>.FromT1(0); },
                           result => { WriteLine(result.Version); return OneOf<ProgramArguments, int>.FromT1(0); },
                           result => { Error.WriteLine(result.Usage); return OneOf<ProgramArguments, int>.FromT1(1); });
if (result.TryPickT1(out var exitCode, out var programArguments))
    return exitCode;
// continues with async/await...
return 0;

And if you still don't want to rely on another library, you can introduce your own union:

switch (
    ProgramArguments.CreateParser()
                    .WithVersion("Naval Fate 2.0")
                    .Parse(args)
                    .Match(args => (ProgramAction)new RunAction(args),
                           result => { WriteLine(result.Help); return new ExitAction(0); },
                           result => { WriteLine(result.Version); return new ExitAction(0); },
                           result => { Error.WriteLine(result.Usage); return new ExitAction(1); }))
{
    case ExitAction { ExitCode: var exitCode }:
        return exitCode;
    case RunAction { Arguments: var args }:
        // continues with async/await...
    return 0;
}

abstract record ProgramAction;
sealed record RunAction(ProgramArguments Arguments) : ProgramAction;
sealed record ExitAction(int ExitCode) : ProgramAction;

But then if you're going with a Match and a switch then you might as well go with just a switch as shown in the documentation:

var parser = Docopt.CreateParser(help).WithVersion("Naval Fate 2.0");

return parser.Parse(args) switch
{
    IArgumentsResult<IDictionary<string, ArgValue>> { Arguments: var arguments } => Run(arguments),
    IHelpResult => ShowHelp(help),
    IVersionResult { Version: var version } => ShowVersion(version),
    IInputErrorResult { Usage: var usage } => OnError(usage),
    var result => throw new System.Runtime.CompilerServices.SwitchExpressionException(result)
};

Anyway, as you can see, you have plenty of choices.

atifaziz commented 1 year ago

I'm going to close this as unnecessary, but happy to reconsider if the presented solutions don't address issue.