Skip to content

Commit 102b51e

Browse files
Reorders CLI options and subcommands to show alphabetically (#1171)
* Add extension methods to add a range of options, commands * Refactor GetRootCommand to collect and sort all options and commands * Renames GetRootCommand to CreateRootCommand * Formatting --------- Co-authored-by: Waldek Mastykarz <[email protected]>
1 parent 5e660a7 commit 102b51e

File tree

3 files changed

+180
-69
lines changed

3 files changed

+180
-69
lines changed

dev-proxy-abstractions/CommandLineExtensions.cs

+33
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,37 @@ public static class CommandLineExtensions
2121

2222
return parseResult.GetValueForOption(option);
2323
}
24+
25+
private static string ByName<T>(T symbol) where T : Symbol => symbol.Name;
26+
27+
public static IEnumerable<T> OrderByName<T>(this IEnumerable<T> symbols) where T : Symbol
28+
{
29+
ArgumentNullException.ThrowIfNull(symbols);
30+
31+
return symbols.OrderBy(ByName, StringComparer.Ordinal);
32+
}
33+
34+
public static Command AddCommands(this Command command, IEnumerable<Command> subcommands)
35+
{
36+
ArgumentNullException.ThrowIfNull(command);
37+
ArgumentNullException.ThrowIfNull(subcommands);
38+
39+
foreach (var subcommand in subcommands)
40+
{
41+
command.AddCommand(subcommand);
42+
}
43+
return command;
44+
}
45+
46+
public static Command AddOptions(this Command command, IEnumerable<Option> options)
47+
{
48+
ArgumentNullException.ThrowIfNull(command);
49+
ArgumentNullException.ThrowIfNull(options);
50+
51+
foreach (var option in options)
52+
{
53+
command.AddOption(option);
54+
}
55+
return command;
56+
}
2457
}

dev-proxy/Program.cs

+11-12
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@
4141
IProxyContext context = new ProxyContext(ProxyCommandHandler.Configuration, ProxyEngine.Certificate, lmClient);
4242
ProxyHost proxyHost = new();
4343

44-
// this is where the root command is created which contains all commands and subcommands
45-
RootCommand rootCommand = proxyHost.GetRootCommand(logger);
46-
4744
// store the global options that are created automatically for us
4845
// rootCommand doesn't return the global options, so we have to store them manually
4946
string[] globalOptions = ["--version"];
@@ -62,20 +59,22 @@
6259
// load plugins to get their options and commands
6360
var pluginLoader = new PluginLoader(isDiscover, logger, loggerFactory);
6461
PluginLoaderResult loaderResults = await pluginLoader.LoadPluginsAsync(pluginEvents, context);
65-
var options = loaderResults.ProxyPlugins
62+
63+
var pluginOptions = loaderResults.ProxyPlugins
6664
.SelectMany(p => p.GetOptions())
6765
// remove duplicates by comparing the option names
6866
.GroupBy(o => o.Name)
6967
.Select(g => g.First())
70-
.ToList();
71-
options.ForEach(rootCommand.AddOption);
72-
// register all plugin commands
73-
loaderResults.ProxyPlugins
68+
.ToArray();
69+
70+
var pluginCommands = loaderResults.ProxyPlugins
7471
.SelectMany(p => p.GetCommands())
75-
.ToList()
76-
.ForEach(rootCommand.AddCommand);
72+
.ToArray();
73+
74+
// this is where the root command is created which contains all commands and subcommands
75+
RootCommand rootCommand = proxyHost.CreateRootCommand(logger, pluginOptions, pluginCommands);
7776

78-
// get the list of available subcommands
77+
// get the list of available subcommand's names
7978
var subCommands = rootCommand.Children.OfType<Command>().Select(c => c.Name).ToArray();
8079

8180
// check if any of the subcommands are present
@@ -132,7 +131,7 @@
132131
pluginEvents.RaiseInit(new InitArgs());
133132
}
134133

135-
rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, [.. options], loaderResults.UrlsToWatch, logger);
134+
rootCommand.Handler = proxyHost.GetCommandHandler(pluginEvents, [.. pluginOptions], loaderResults.UrlsToWatch, logger);
136135
var exitCode = await rootCommand.InvokeAsync(args);
137136
loggerFactory.Dispose();
138137
Environment.Exit(exitCode);

dev-proxy/ProxyHost.cs

+136-57
Original file line numberDiff line numberDiff line change
@@ -333,9 +333,10 @@ public ProxyHost()
333333
ProxyCommandHandler.Configuration.ConfigFile = ConfigFile;
334334
}
335335

336-
public RootCommand GetRootCommand(ILogger logger)
336+
public RootCommand CreateRootCommand(ILogger logger, Option[] pluginOptions, Command[] pluginCommands)
337337
{
338-
var command = new RootCommand {
338+
var command = new RootCommand("Dev Proxy is a command line tool for testing Microsoft Graph, SharePoint Online and any other HTTP APIs.");
339+
var options = (Option[])[
339340
_portOption,
340341
_ipAddressOption,
341342
_recordOption,
@@ -352,86 +353,89 @@ public RootCommand GetRootCommand(ILogger logger)
352353
_urlsToWatchOption!,
353354
_timeoutOption,
354355
_discoverOption,
355-
_envOption
356-
};
357-
command.Description = "Dev Proxy is a command line tool for testing Microsoft Graph, SharePoint Online and any other HTTP APIs.";
356+
_envOption,
357+
..pluginOptions
358+
];
359+
360+
command.AddOptions(options.OrderByName());
361+
358362
// _logLevelOption is set while initializing the Program
359363
// As such, it's always set here
360364
command.AddGlobalOption(_logLevelOption!);
361365

362-
var msGraphDbCommand = new Command("msgraphdb", "Generate a local SQLite database with Microsoft Graph API metadata")
363-
{
364-
Handler = new MSGraphDbCommandHandler(logger)
365-
};
366-
command.Add(msGraphDbCommand);
366+
var commands = (Command[])[
367+
CreateMsGraphDbCommand(logger),
368+
CreateConfigCommand(logger),
369+
CreateOutdatedCommand(logger),
370+
CreateJwtCommand(),
371+
CreateCertCommand(logger),
372+
..pluginCommands
373+
];
367374

368-
var configCommand = new Command("config", "Manage Dev Proxy configs");
369-
370-
var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery");
371-
var configIdArgument = new Argument<string>("config-id", "The ID of the config to download");
372-
configGetCommand.AddArgument(configIdArgument);
373-
configGetCommand.SetHandler(async configId => await ConfigGetCommandHandler.DownloadConfigAsync(configId, logger), configIdArgument);
374-
configCommand.Add(configGetCommand);
375+
command.AddCommands(commands.OrderByName());
376+
return command;
377+
}
375378

376-
var configNewCommand = new Command("new", "Create new Dev Proxy configuration file");
377-
var nameArgument = new Argument<string>("name", "Name of the configuration file")
378-
{
379-
Arity = ArgumentArity.ZeroOrOne
380-
};
381-
nameArgument.SetDefaultValue("devproxyrc.json");
382-
configNewCommand.AddArgument(nameArgument);
383-
configNewCommand.SetHandler(async name => await ConfigNewCommandHandler.CreateConfigFileAsync(name, logger), nameArgument);
384-
configCommand.Add(configNewCommand);
379+
private static Command CreateCertCommand(ILogger logger)
380+
{
381+
var certCommand = new Command("cert", "Manage the Dev Proxy certificate");
385382

386-
var configOpenCommand = new Command("open", "Open devproxyrc.json");
387-
configOpenCommand.SetHandler(() =>
383+
var sortedCommands = new[]
388384
{
389-
var cfgPsi = new ProcessStartInfo(ConfigFile)
390-
{
391-
UseShellExecute = true
392-
};
393-
Process.Start(cfgPsi);
394-
});
395-
configCommand.Add(configOpenCommand);
385+
CreateCertEnsureCommand(logger)
386+
}.OrderByName();
396387

397-
command.Add(configCommand);
388+
certCommand.AddCommands(sortedCommands);
389+
return certCommand;
390+
}
398391

399-
var outdatedCommand = new Command("outdated", "Check for new version");
400-
var outdatedShortOption = new Option<bool>("--short", "Return version only");
401-
outdatedCommand.AddOption(outdatedShortOption);
402-
outdatedCommand.SetHandler(async versionOnly => await OutdatedCommandHandler.CheckVersionAsync(versionOnly, logger), outdatedShortOption);
403-
command.Add(outdatedCommand);
392+
private static Command CreateCertEnsureCommand(ILogger logger)
393+
{
394+
var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted.");
395+
certEnsureCommand.SetHandler(async () => await CertEnsureCommandHandler.EnsureCertAsync(logger));
396+
return certEnsureCommand;
397+
}
404398

399+
private static Command CreateJwtCommand()
400+
{
405401
var jwtCommand = new Command("jwt", "Manage JSON Web Tokens");
402+
403+
var sortedCommands = new[]
404+
{
405+
CreateJwtCreateCommand()
406+
}.OrderByName();
407+
408+
jwtCommand.AddCommands(sortedCommands);
409+
return jwtCommand;
410+
}
411+
412+
private static Command CreateJwtCreateCommand()
413+
{
406414
var jwtCreateCommand = new Command("create", "Create a new JWT token");
415+
407416
var jwtNameOption = new Option<string>("--name", "The name of the user to create the token for.");
408417
jwtNameOption.AddAlias("-n");
409-
jwtCreateCommand.AddOption(jwtNameOption);
410418

411419
var jwtAudienceOption = new Option<IEnumerable<string>>("--audience", "The audiences to create the token for. Specify once for each audience")
412420
{
413421
AllowMultipleArgumentsPerToken = true
414422
};
415423
jwtAudienceOption.AddAlias("-a");
416-
jwtCreateCommand.AddOption(jwtAudienceOption);
417424

418425
var jwtIssuerOption = new Option<string>("--issuer", "The issuer of the token.");
419426
jwtIssuerOption.AddAlias("-i");
420-
jwtCreateCommand.AddOption(jwtIssuerOption);
421427

422428
var jwtRolesOption = new Option<IEnumerable<string>>("--roles", "A role claim to add to the token. Specify once for each role.")
423429
{
424430
AllowMultipleArgumentsPerToken = true
425431
};
426432
jwtRolesOption.AddAlias("-r");
427-
jwtCreateCommand.AddOption(jwtRolesOption);
428433

429434
var jwtScopesOption = new Option<IEnumerable<string>>("--scopes", "A scope claim to add to the token. Specify once for each scope.")
430435
{
431436
AllowMultipleArgumentsPerToken = true
432437
};
433438
jwtScopesOption.AddAlias("-s");
434-
jwtCreateCommand.AddOption(jwtScopesOption);
435439

436440
var jwtClaimsOption = new Option<Dictionary<string, string>>("--claims",
437441
description: "Claims to add to the token. Specify once for each claim in the format \"name:value\".",
@@ -464,11 +468,9 @@ public RootCommand GetRootCommand(ILogger logger)
464468
{
465469
AllowMultipleArgumentsPerToken = true,
466470
};
467-
jwtCreateCommand.AddOption(jwtClaimsOption);
468471

469472
var jwtValidForOption = new Option<double>("--valid-for", "The duration for which the token is valid. Duration is set in minutes.");
470473
jwtValidForOption.AddAlias("-v");
471-
jwtCreateCommand.AddOption(jwtValidForOption);
472474

473475
var jwtSigningKeyOption = new Option<string>("--signing-key", "The signing key to sign the token. Minimum length is 32 characters.");
474476
jwtSigningKeyOption.AddAlias("-k");
@@ -487,7 +489,6 @@ public RootCommand GetRootCommand(ILogger logger)
487489
input.ErrorMessage = ex.Message;
488490
}
489491
});
490-
jwtCreateCommand.AddOption(jwtSigningKeyOption);
491492

492493
jwtCreateCommand.SetHandler(
493494
JwtCommandHandler.GetToken,
@@ -502,17 +503,95 @@ public RootCommand GetRootCommand(ILogger logger)
502503
jwtSigningKeyOption
503504
)
504505
);
505-
jwtCommand.Add(jwtCreateCommand);
506506

507-
command.Add(jwtCommand);
507+
var sortedOptions = new Option[]
508+
{
509+
jwtNameOption,
510+
jwtAudienceOption,
511+
jwtIssuerOption,
512+
jwtRolesOption,
513+
jwtScopesOption,
514+
jwtClaimsOption,
515+
jwtValidForOption,
516+
jwtSigningKeyOption
517+
}.OrderByName();
518+
519+
jwtCreateCommand.AddOptions(sortedOptions);
520+
return jwtCreateCommand;
521+
}
508522

509-
var certCommand = new Command("cert", "Manage the Dev Proxy certificate");
510-
var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted.");
511-
certEnsureCommand.SetHandler(async () => await CertEnsureCommandHandler.EnsureCertAsync(logger));
512-
certCommand.Add(certEnsureCommand);
513-
command.Add(certCommand);
523+
private static Command CreateOutdatedCommand(ILogger logger)
524+
{
525+
var outdatedCommand = new Command("outdated", "Check for new version");
526+
var outdatedShortOption = new Option<bool>("--short", "Return version only");
527+
outdatedCommand.SetHandler(async versionOnly => await OutdatedCommandHandler.CheckVersionAsync(versionOnly, logger), outdatedShortOption);
514528

515-
return command;
529+
var sortedOptions = new[]
530+
{
531+
outdatedShortOption
532+
}.OrderByName();
533+
534+
outdatedCommand.AddOptions(sortedOptions);
535+
return outdatedCommand;
536+
}
537+
538+
private static Command CreateConfigCommand(ILogger logger)
539+
{
540+
var configCommand = new Command("config", "Manage Dev Proxy configs");
541+
542+
var sortedCommands = new[]
543+
{
544+
CreateConfigGetCommand(logger),
545+
CreateConfigNewCommand(logger),
546+
CreateConfigOpenCommand()
547+
}.OrderByName();
548+
549+
configCommand.AddCommands(sortedCommands);
550+
return configCommand;
551+
}
552+
553+
private static Command CreateConfigGetCommand(ILogger logger)
554+
{
555+
var configGetCommand = new Command("get", "Download the specified config from the Sample Solution Gallery");
556+
var configIdArgument = new Argument<string>("config-id", "The ID of the config to download");
557+
configGetCommand.AddArgument(configIdArgument);
558+
configGetCommand.SetHandler(async configId => await ConfigGetCommandHandler.DownloadConfigAsync(configId, logger), configIdArgument);
559+
return configGetCommand;
560+
}
561+
562+
private static Command CreateConfigNewCommand(ILogger logger)
563+
{
564+
var configNewCommand = new Command("new", "Create new Dev Proxy configuration file");
565+
var nameArgument = new Argument<string>("name", "Name of the configuration file")
566+
{
567+
Arity = ArgumentArity.ZeroOrOne
568+
};
569+
nameArgument.SetDefaultValue("devproxyrc.json");
570+
configNewCommand.AddArgument(nameArgument);
571+
configNewCommand.SetHandler(async name => await ConfigNewCommandHandler.CreateConfigFileAsync(name, logger), nameArgument);
572+
return configNewCommand;
573+
}
574+
575+
private static Command CreateConfigOpenCommand()
576+
{
577+
var configOpenCommand = new Command("open", "Open devproxyrc.json");
578+
configOpenCommand.SetHandler(() =>
579+
{
580+
var cfgPsi = new ProcessStartInfo(ConfigFile)
581+
{
582+
UseShellExecute = true
583+
};
584+
Process.Start(cfgPsi);
585+
});
586+
return configOpenCommand;
587+
}
588+
589+
private static Command CreateMsGraphDbCommand(ILogger logger)
590+
{
591+
return new Command("msgraphdb", "Generate a local SQLite database with Microsoft Graph API metadata")
592+
{
593+
Handler = new MSGraphDbCommandHandler(logger)
594+
};
516595
}
517596

518597
public ProxyCommandHandler GetCommandHandler(PluginEvents pluginEvents, Option[] optionsFromPlugins, ISet<UrlToWatch> urlsToWatch, ILogger logger) => new(

0 commit comments

Comments
 (0)