From 43c74cfa5360be63faaaf361e5d4385e2796e138 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Fri, 3 Aug 2018 20:58:07 -0700 Subject: [PATCH] Respond to much of the feedback from review Default to requerying swagger for each exec Auto-set the content-type if not yet set for requests based on swagger Fix swagger UI command Make error and warning colors configurable Allow aliases to be specified in structured input based commands Have delete not require a body Show allowed methods for . and .. Show verbs on the announced path after CD Issue a request on set base and warn if there's a socket error --- .../Commands/BaseHttpCommand.cs | 98 ++++++++++--------- .../Commands/ChangeDirectoryCommand.cs | 8 +- .../Commands/ConfigCommand.cs | 8 +- .../Commands/DeleteCommand.cs | 2 +- .../Commands/EchoCommand.cs | 2 +- .../Commands/HelpCommand.cs | 16 ++- .../Commands/ListCommand.cs | 33 +++++-- .../Commands/PrefCommand.cs | 4 +- .../Commands/SetBaseCommand.cs | 13 ++- .../Commands/SetDiagCommand.cs | 6 +- .../Commands/SetSwaggerCommand.cs | 4 +- src/Microsoft.HttpRepl/Commands/UICommand.cs | 4 +- src/Microsoft.HttpRepl/HttpState.cs | 7 ++ .../Preferences/WellKnownPreference.cs | 7 +- src/Microsoft.HttpRepl/Program.cs | 8 +- .../Commanding/CommandInputSpecification.cs | 4 +- .../CommandInputSpecificationBuilder.cs | 13 ++- .../CommandWithStructuredInputBase.cs | 27 ++++- .../Commanding/DefaultCommandInput.cs | 54 +++++++--- 19 files changed, 219 insertions(+), 99 deletions(-) diff --git a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs index ee9b4bcf0e..9a5eba8227 100644 --- a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs @@ -32,7 +32,7 @@ namespace Microsoft.HttpRepl.Commands private const string BodyFileOption = nameof(BodyFileOption); private const string NoBodyOption = nameof(NoBodyOption); private const string NoFormattingOption = nameof(NoFormattingOption); - private const string NoStreamingOption = nameof(NoStreamingOption); + private const string StreamingOption = nameof(StreamingOption); private const string BodyContentOption = nameof(BodyContentOption); private static readonly char[] HeaderSeparatorChars = new[] { '=', ':' }; @@ -58,7 +58,7 @@ namespace Microsoft.HttpRepl.Commands .WithOption(new CommandOptionSpecification(ResponseHeadersFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response:headers", })) .WithOption(new CommandOptionSpecification(ResponseBodyFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response:body", })) .WithOption(new CommandOptionSpecification(NoFormattingOption, maximumOccurrences: 1, forms: new[] { "--no-formatting", "-F" })) - .WithOption(new CommandOptionSpecification(NoStreamingOption, maximumOccurrences: 1, forms: new[] { "--no-streaming", "-S" })); + .WithOption(new CommandOptionSpecification(StreamingOption, maximumOccurrences: 1, forms: new[] { "--streaming", "-s" })); if (RequiresBody) { @@ -76,10 +76,20 @@ namespace Microsoft.HttpRepl.Commands { if (programState.BaseAddress == null && (commandInput.Arguments.Count == 0 || !Uri.TryCreate(commandInput.Arguments[0].Text, UriKind.Absolute, out Uri _))) { - shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests to a relative path".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests to a relative path".SetColor(programState.ErrorColor)); return; } + if (programState.SwaggerEndpoint != null) + { + string swaggerRequeryBehaviorSetting = programState.GetStringPreference(WellKnownPreference.SwaggerRequeryBehavior, "auto"); + + if (swaggerRequeryBehaviorSetting.StartsWith("auto", StringComparison.OrdinalIgnoreCase)) + { + await SetSwaggerCommand.CreateDirectoryStructureForSwaggerEndpointAsync(shellState, programState, programState.SwaggerEndpoint, cancellationToken).ConfigureAwait(false); + } + } + Dictionary thisRequestHeaders = new Dictionary(); foreach (InputElement header in commandInput.Options[HeaderOption]) @@ -88,7 +98,7 @@ namespace Microsoft.HttpRepl.Commands if (equalsIndex < 0) { - shellState.ConsoleManager.Error.WriteLine("Headers must be formatted as {header}={value} or {header}:{value}".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Headers must be formatted as {header}={value} or {header}:{value}".SetColor(programState.ErrorColor)); return; } @@ -114,7 +124,7 @@ namespace Microsoft.HttpRepl.Commands if (!File.Exists(filePath)) { - shellState.ConsoleManager.Error.WriteLine($"Content file {filePath} does not exist".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine($"Content file {filePath} does not exist".SetColor(programState.ErrorColor)); return; } } @@ -127,7 +137,7 @@ namespace Microsoft.HttpRepl.Commands string defaultEditorCommand = programState.GetStringPreference(WellKnownPreference.DefaultEditorCommand); if (defaultEditorCommand == null) { - shellState.ConsoleManager.Error.WriteLine($"The default editor must be configured using the command `pref set {WellKnownPreference.DefaultEditorCommand} \"{{commandline}}\"`".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine($"The default editor must be configured using the command `pref set {WellKnownPreference.DefaultEditorCommand} \"{{commandline}}\"`".SetColor(programState.ErrorColor)); return; } @@ -145,6 +155,7 @@ namespace Microsoft.HttpRepl.Commands } string exampleBody = programState.GetExampleBody(commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty, contentType, Verb); + request.Headers.TryAddWithoutValidation("Content-Type", contentType); if (!string.IsNullOrEmpty(exampleBody)) { @@ -224,7 +235,7 @@ namespace Microsoft.HttpRepl.Commands string method = response.RequestMessage.Method.ToString().ToUpperInvariant().SetColor(requestConfig.MethodColor); string pathAndQuery = response.RequestMessage.RequestUri.PathAndQuery.SetColor(requestConfig.AddressColor); - protocolInfo = $"{"HTTP".SetColor(requestConfig.ProtocolNameColor)}{"/".SetColor(requestConfig.ProtocolSeparatorColor)}{response.RequestMessage.Version.ToString().SetColor(requestConfig.ProtocolVersionColor)}"; + protocolInfo = $"{"HTTP".SetColor(requestConfig.ProtocolNameColor)}{"/".SetColor(requestConfig.ProtocolSeparatorColor)}{response.Version.ToString().SetColor(requestConfig.ProtocolVersionColor)}"; consoleManager.WriteLine($"{method} {pathAndQuery} {protocolInfo}"); IEnumerable>> requestHeaders = response.RequestMessage.Headers; @@ -327,6 +338,42 @@ namespace Microsoft.HttpRepl.Commands private static async Task FormatBodyAsync(DefaultCommandInput commandInput, HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken) { + if (commandInput.Options[StreamingOption].Count > 0) + { + Memory buffer = new Memory(new char[2048]); + Stream s = await content.ReadAsStreamAsync().ConfigureAwait(false); + StreamReader reader = new StreamReader(s); + consoleManager.WriteLine("Streaming the response, press any key to stop...".SetColor(programState.WarningColor)); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + ValueTask readTask = reader.ReadAsync(buffer, cancellationToken); + if (await WaitForCompletionAsync(readTask, cancellationToken).ConfigureAwait(false)) + { + if (readTask.Result == 0) + { + break; + } + + string str = new string(buffer.Span.Slice(0, readTask.Result)); + consoleManager.Write(str); + bodyFileWriter.Write(str); + } + else + { + break; + } + } + catch (OperationCanceledException) + { + } + } + + return; + } + string contentType = null; if (content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues)) { @@ -363,43 +410,6 @@ namespace Microsoft.HttpRepl.Commands } } - //Unless the user has explicitly specified to not stream the response, if we don't have content length, assume streaming - if (commandInput.Options[NoStreamingOption].Count == 0 && !content.Headers.TryGetValues("Content-Length", out IEnumerable _)) - { - Memory buffer = new Memory(new char[2048]); - Stream s = await content.ReadAsStreamAsync().ConfigureAwait(false); - StreamReader reader = new StreamReader(s); - consoleManager.WriteLine("Streaming the response, press any key to stop...".Bold().Yellow()); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - ValueTask readTask = reader.ReadAsync(buffer, cancellationToken); - if (await WaitForCompletionAsync(readTask, cancellationToken).ConfigureAwait(false)) - { - if (readTask.Result == 0) - { - break; - } - - string str = new string(buffer.Span.Slice(0, readTask.Result)); - consoleManager.Write(str); - bodyFileWriter.Write(str); - } - else - { - break; - } - } - catch (OperationCanceledException) - { - } - } - - return; - } - string responseContent = await content.ReadAsStringAsync().ConfigureAwait(false); bodyFileWriter.WriteLine(responseContent); consoleManager.WriteLine(responseContent); diff --git a/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs b/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs index c2baaf0de5..a18aa2df9f 100644 --- a/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs @@ -48,7 +48,13 @@ namespace Microsoft.HttpRepl.Commands } } - shellState.ConsoleManager.WriteLine($"/{string.Join("/", programState.PathSections.Reverse())}"); + IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()); + + string thisDirMethod = s.RequestInfo != null && s.RequestInfo.Methods.Count > 0 + ? "[" + string.Join("|", s.RequestInfo.Methods) + "]" + : "[]"; + + shellState.ConsoleManager.WriteLine($"/{string.Join("/", programState.PathSections.Reverse())} {thisDirMethod}"); } return Task.CompletedTask; diff --git a/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs b/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs index 773bee0f04..938936303f 100644 --- a/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs @@ -21,13 +21,13 @@ namespace Microsoft.HttpRepl.Commands { if (programState.BaseAddress == null) { - shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to query configuration".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to query configuration".SetColor(programState.ErrorColor)); return; } if (string.IsNullOrEmpty(programState.DiagnosticsState.DiagnosticsEndpoint)) { - shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint must be set to query configuration (see set diag)".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint must be set to query configuration (see set diag)".SetColor(programState.ErrorColor)); return; } @@ -35,7 +35,7 @@ namespace Microsoft.HttpRepl.Commands if (configUrl == null) { - shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint does not expose configuration information".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint does not expose configuration information".SetColor(programState.ErrorColor)); return; } @@ -43,7 +43,7 @@ namespace Microsoft.HttpRepl.Commands if (!response.IsSuccessStatusCode) { - shellState.ConsoleManager.Error.WriteLine("Unable to get configuration information from diagnostics endpoint".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Unable to get configuration information from diagnostics endpoint".SetColor(programState.ErrorColor)); return; } diff --git a/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs b/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs index 7c9b789db3..aedef544c0 100644 --- a/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs @@ -7,6 +7,6 @@ namespace Microsoft.HttpRepl.Commands { protected override string Verb => "delete"; - protected override bool RequiresBody => true; + protected override bool RequiresBody => false; } } diff --git a/src/Microsoft.HttpRepl/Commands/EchoCommand.cs b/src/Microsoft.HttpRepl/Commands/EchoCommand.cs index 691eb5679f..e81ff810d4 100644 --- a/src/Microsoft.HttpRepl/Commands/EchoCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/EchoCommand.cs @@ -21,7 +21,7 @@ namespace Microsoft.HttpRepl.Commands { if (commandInput.Arguments.Count == 0 || !_allowedModes.Contains(commandInput.Arguments[0]?.Text)) { - shellState.ConsoleManager.Error.WriteLine("Allowed echo modes are 'on' and 'off'".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Allowed echo modes are 'on' and 'off'".SetColor(programState.ErrorColor)); return false; } diff --git a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs index 646dd4c544..6677bde928 100644 --- a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.HttpRepl.Preferences; using Microsoft.HttpRepl.Suggestions; using Microsoft.Repl; using Microsoft.Repl.Commanding; @@ -24,7 +25,7 @@ namespace Microsoft.HttpRepl.Commands : null; } - public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) { if (shellState.CommandDispatcher is ICommandDispatcher dispatcher) { @@ -55,6 +56,17 @@ namespace Microsoft.HttpRepl.Commands //Maybe the input is an URL if (parseResult.Sections.Count == 2) { + + if (programState.SwaggerEndpoint != null) + { + string swaggerRequeryBehaviorSetting = programState.GetStringPreference(WellKnownPreference.SwaggerRequeryBehavior, "auto"); + + if (swaggerRequeryBehaviorSetting.StartsWith("auto", StringComparison.OrdinalIgnoreCase)) + { + await SetSwaggerCommand.CreateDirectoryStructureForSwaggerEndpointAsync(shellState, programState, programState.SwaggerEndpoint, cancellationToken).ConfigureAwait(false); + } + } + IDirectoryStructure structure = programState.Structure.TraverseTo(parseResult.Sections[1]); if (structure.DirectoryNames.Any()) { @@ -101,8 +113,6 @@ namespace Microsoft.HttpRepl.Commands } } } - - return Task.CompletedTask; } public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) diff --git a/src/Microsoft.HttpRepl/Commands/ListCommand.cs b/src/Microsoft.HttpRepl/Commands/ListCommand.cs index b1aa0718a3..b0641f186b 100644 --- a/src/Microsoft.HttpRepl/Commands/ListCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ListCommand.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.HttpRepl.Preferences; using Microsoft.Repl; using Microsoft.Repl.Commanding; using Microsoft.Repl.Parsing; @@ -17,11 +18,21 @@ namespace Microsoft.HttpRepl.Commands { private const string RecursiveOption = nameof(RecursiveOption); - protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) { + if (programState.SwaggerEndpoint != null) + { + string swaggerRequeryBehaviorSetting = programState.GetStringPreference(WellKnownPreference.SwaggerRequeryBehavior, "auto"); + + if (swaggerRequeryBehaviorSetting.StartsWith("auto", StringComparison.OrdinalIgnoreCase)) + { + await SetSwaggerCommand.CreateDirectoryStructureForSwaggerEndpointAsync(shellState, programState, programState.SwaggerEndpoint, cancellationToken).ConfigureAwait(false); + } + } + if (programState.Structure == null || programState.BaseAddress == null) { - return Task.CompletedTask; + return; } string path = commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty; @@ -29,19 +40,27 @@ namespace Microsoft.HttpRepl.Commands //If it's an absolute URI, nothing to suggest if (Uri.TryCreate(path, UriKind.Absolute, out Uri _)) { - return Task.CompletedTask; + return; } IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path); + string thisDirMethod = s.RequestInfo != null && s.RequestInfo.Methods.Count > 0 + ? "[" + string.Join("|", s.RequestInfo.Methods) + "]" + : "[]"; + List roots = new List(); Formatter formatter = new Formatter(); - roots.Add(new TreeNode(formatter, ".", string.Empty)); + roots.Add(new TreeNode(formatter, ".", thisDirMethod)); if (s.Parent != null) { - roots.Add(new TreeNode(formatter, "..", string.Empty)); + string parentDirMethod = s.Parent.RequestInfo != null && s.Parent.RequestInfo.Methods.Count > 0 + ? "[" + string.Join("|", s.Parent.RequestInfo.Methods) + "]" + : "[]"; + + roots.Add(new TreeNode(formatter, "..", parentDirMethod)); } int recursionDepth = 1; @@ -75,8 +94,6 @@ namespace Microsoft.HttpRepl.Commands { shellState.ConsoleManager.WriteLine(node.ToString()); } - - return Task.CompletedTask; } private static void Recurse(TreeNode parentNode, IDirectoryStructure parent, int remainingDepth) @@ -101,7 +118,7 @@ namespace Microsoft.HttpRepl.Commands - protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("ls") + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("ls").AlternateName("dir") .MaximumArgCount(1) .WithOption(new CommandOptionSpecification(RecursiveOption, maximumOccurrences: 1, acceptsValue: true, forms: new[] {"-r", "--recursive"})) .Finish(); diff --git a/src/Microsoft.HttpRepl/Commands/PrefCommand.cs b/src/Microsoft.HttpRepl/Commands/PrefCommand.cs index 2a64d2f017..d4d5d19b4a 100644 --- a/src/Microsoft.HttpRepl/Commands/PrefCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/PrefCommand.cs @@ -90,7 +90,7 @@ namespace Microsoft.HttpRepl.Commands if (!programState.SavePreferences()) { - shellState.ConsoleManager.Error.WriteLine("Error saving preferences".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Error saving preferences".SetColor(programState.ErrorColor)); } return Task.CompletedTask; @@ -109,7 +109,7 @@ namespace Microsoft.HttpRepl.Commands } else { - shellState.ConsoleManager.Error.WriteLine((commandInput.Arguments[1].Text + " does not have a configured value").Bold().Red()); + shellState.ConsoleManager.Error.WriteLine((commandInput.Arguments[1].Text + " does not have a configured value").SetColor(programState.ErrorColor)); } } else diff --git a/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs b/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs index 77e5f0adde..76166d5c53 100644 --- a/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Net.Http; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Microsoft.Repl; @@ -34,11 +36,20 @@ namespace Microsoft.HttpRepl.Commands } else if (parseResult.Sections.Count != 3 || string.IsNullOrEmpty(parseResult.Sections[2]) || !Uri.TryCreate(EnsureTrailingSlash(parseResult.Sections[2]), UriKind.Absolute, out Uri serverUri)) { - shellState.ConsoleManager.Error.WriteLine("Must specify a server".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Must specify a server".SetColor(state.ErrorColor)); } else { state.BaseAddress = serverUri; + try + { + await state.Client.SendAsync(new HttpRequestMessage(HttpMethod.Head, serverUri)).ConfigureAwait(false); + } + catch (Exception ex) when (ex.InnerException is SocketException se) + { + shellState.ConsoleManager.Error.WriteLine($"Warning: HEAD request to the specified address was unsuccessful ({se.Message})".SetColor(state.WarningColor)); + } + catch { } } if (state.BaseAddress == null || !Uri.TryCreate(state.BaseAddress, "/swagger/v1/swagger.json", out Uri result)) diff --git a/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs b/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs index 535d1a2026..4adb208de3 100644 --- a/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs @@ -41,7 +41,7 @@ namespace Microsoft.HttpRepl.Commands if (parseResult.Sections.Count != 3 || string.IsNullOrEmpty(parseResult.Sections[2]) || !Uri.TryCreate(parseResult.Sections[2], UriKind.Relative, out Uri _)) { - shellState.ConsoleManager.Error.WriteLine("Must specify a relative path".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Must specify a relative path".SetColor(programState.ErrorColor)); } else { @@ -50,7 +50,7 @@ namespace Microsoft.HttpRepl.Commands if (!response.IsSuccessStatusCode) { - shellState.ConsoleManager.Error.WriteLine("Unable to access diagnostics endpoint".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Unable to access diagnostics endpoint".SetColor(programState.ErrorColor)); programState.DiagnosticsState.DiagnosticsEndpoint = null; programState.DiagnosticsState.DiagnosticItems = null; } @@ -66,7 +66,7 @@ namespace Microsoft.HttpRepl.Commands if (!endpointsResponse.IsSuccessStatusCode) { - shellState.ConsoleManager.Error.WriteLine("Unable to get endpoints information from diagnostics endpoint".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Unable to get endpoints information from diagnostics endpoint".SetColor(programState.ErrorColor)); return; } diff --git a/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs b/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs index be99891d5e..41c1564a99 100644 --- a/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs @@ -243,7 +243,7 @@ namespace Microsoft.HttpRepl.Commands if (parseResult.Sections.Count != 3 || string.IsNullOrEmpty(parseResult.Sections[2]) || !Uri.TryCreate(parseResult.Sections[2], UriKind.Absolute, out Uri serverUri)) { - shellState.ConsoleManager.Error.WriteLine("Must specify a swagger document".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Must specify a swagger document".SetColor(programState.ErrorColor)); } else { @@ -253,6 +253,8 @@ namespace Microsoft.HttpRepl.Commands internal static async Task CreateDirectoryStructureForSwaggerEndpointAsync(IShellState shellState, HttpState programState, Uri serverUri, CancellationToken cancellationToken) { + programState.SwaggerEndpoint = serverUri; + try { IEnumerable doc = await GetSwaggerDocAsync(programState.Client, serverUri).ConfigureAwait(false); diff --git a/src/Microsoft.HttpRepl/Commands/UICommand.cs b/src/Microsoft.HttpRepl/Commands/UICommand.cs index 0eb02826f9..ac03d53d29 100644 --- a/src/Microsoft.HttpRepl/Commands/UICommand.cs +++ b/src/Microsoft.HttpRepl/Commands/UICommand.cs @@ -29,13 +29,13 @@ namespace Microsoft.HttpRepl.Commands { if (programState.BaseAddress == null) { - shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to launch Swagger UI".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to launch Swagger UI".SetColor(programState.ErrorColor)); return Task.CompletedTask; } Uri uri = new Uri(programState.BaseAddress, "swagger"); string agent = "cmd"; - string agentParam = $"/c {uri.AbsoluteUri}"; + string agentParam = $"/c start {uri.AbsoluteUri}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { diff --git a/src/Microsoft.HttpRepl/HttpState.cs b/src/Microsoft.HttpRepl/HttpState.cs index d012a8fc45..9104e4e02c 100644 --- a/src/Microsoft.HttpRepl/HttpState.cs +++ b/src/Microsoft.HttpRepl/HttpState.cs @@ -9,6 +9,7 @@ using System.Net.Http; using System.Runtime.InteropServices; using Microsoft.HttpRepl.Diagnostics; using Microsoft.HttpRepl.Preferences; +using Microsoft.Repl.ConsoleHandling; namespace Microsoft.HttpRepl { @@ -19,6 +20,10 @@ namespace Microsoft.HttpRepl public HttpClient Client { get; } + public AllowedColors ErrorColor => this.GetColorPreference(WellKnownPreference.ErrorColor, AllowedColors.BoldRed); + + public AllowedColors WarningColor => this.GetColorPreference(WellKnownPreference.WarningColor, AllowedColors.BoldYellow); + public Stack PathSections { get; } public IDirectoryStructure SwaggerStructure { get; set; } @@ -62,6 +67,8 @@ namespace Microsoft.HttpRepl public DiagnosticsState DiagnosticsState { get; } + public Uri SwaggerEndpoint { get; set; } + public HttpState() { Client = new HttpClient(); diff --git a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs index 424c0bb9a4..a0456bb99f 100644 --- a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs +++ b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs @@ -131,9 +131,12 @@ namespace Microsoft.HttpRepl.Preferences public static string ResponseStatusReaseonPhraseColor { get; } = "colors.response.status.reasonPhrase"; - public static string RequestOrResponseColor { get; } = "colors.requestOrResponse"; + public static string ErrorColor { get; } = "colors.error"; + + public static string WarningColor { get; } = "colors.warning"; + public static string BodyColor { get; } = "colors.body"; public static string SchemeColor { get; } = "colors.scheme"; @@ -167,6 +170,8 @@ namespace Microsoft.HttpRepl.Preferences public static string DefaultEditorArguments { get; } = "editor.command.default.arguments"; + public static string SwaggerRequeryBehavior { get; } = "swagger.requery"; + public static AllowedColors GetColorPreference(this HttpState programState, string preference, AllowedColors defaultvalue = AllowedColors.None) { diff --git a/src/Microsoft.HttpRepl/Program.cs b/src/Microsoft.HttpRepl/Program.cs index 6509e7a38c..e41a36ff21 100644 --- a/src/Microsoft.HttpRepl/Program.cs +++ b/src/Microsoft.HttpRepl/Program.cs @@ -16,15 +16,15 @@ namespace Microsoft.HttpRepl { static async Task Main(string[] args) { - if(Console.IsOutputRedirected) + var state = new HttpState(); + + if (Console.IsOutputRedirected) { - Reporter.Error.WriteLine("Cannot start the REPL when output is being redirected".Bold().Red()); + Reporter.Error.WriteLine("Cannot start the REPL when output is being redirected".SetColor(state.ErrorColor)); return; } - var state = new HttpState(); var dispatcher = DefaultCommandDispatcher.Create(state.GetPrompt, state); - dispatcher.AddCommand(new ChangeDirectoryCommand()); dispatcher.AddCommand(new ClearCommand()); //dispatcher.AddCommand(new ConfigCommand()); diff --git a/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs b/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs index 0e24c3fffa..05a14e8641 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs @@ -7,7 +7,7 @@ namespace Microsoft.Repl.Commanding { public class CommandInputSpecification { - public IReadOnlyList CommandName { get; } + public IReadOnlyList> CommandName { get; } public char OptionPreamble { get; } @@ -17,7 +17,7 @@ namespace Microsoft.Repl.Commanding public IReadOnlyList Options { get; } - public CommandInputSpecification(IReadOnlyList name, char optionPreamble, IReadOnlyList options, int minimumArgs, int maximumArgs) + public CommandInputSpecification(IReadOnlyList> name, char optionPreamble, IReadOnlyList options, int minimumArgs, int maximumArgs) { CommandName = name; OptionPreamble = optionPreamble; diff --git a/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs b/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs index 019782d0b8..7b5b529e3c 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs @@ -1,13 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; namespace Microsoft.Repl.Commanding { public class CommandInputSpecificationBuilder { - private readonly IReadOnlyList _name; + private readonly List> _name; private char _optionPreamble; private int _minimumArgs; private int _maximumArgs; @@ -15,7 +16,7 @@ namespace Microsoft.Repl.Commanding public CommandInputSpecificationBuilder(IReadOnlyList name) { - _name = name; + _name = new List> { name }; _optionPreamble = '-'; } @@ -65,5 +66,13 @@ namespace Microsoft.Repl.Commanding { return new CommandInputSpecification(_name, _optionPreamble, _options, _minimumArgs, _maximumArgs); } + + public CommandInputSpecificationBuilder AlternateName(string baseName, params string[] additionalNameParts) + { + List nameParts = new List { baseName }; + nameParts.AddRange(additionalNameParts); + _name.Add(nameParts); + return this; + } } } diff --git a/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs b/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs index 1957205913..a7e07b3cee 100644 --- a/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs +++ b/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs @@ -40,17 +40,34 @@ namespace Microsoft.Repl.Commanding //If we're completing in a name position, offer completion for the command name if (parseResult.SelectedSection < InputSpec.CommandName.Count) { - for (int i = 0; i < parseResult.SelectedSection; ++i) + IReadOnlyList commandName = null; + for (int j = 0; j < InputSpec.CommandName.Count; ++j) { - if (!string.Equals(InputSpec.CommandName[i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase)) + bool success = true; + for (int i = 0; i < parseResult.SelectedSection; ++i) { - return null; + if (!string.Equals(InputSpec.CommandName[j][i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase)) + { + success = false; + break; + } + } + + if (success) + { + commandName = InputSpec.CommandName[j]; + break; } } - if (InputSpec.CommandName[parseResult.SelectedSection].StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)) + if (commandName is null) { - return new[] {InputSpec.CommandName[parseResult.SelectedSection]}; + return null; + } + + if (commandName[parseResult.SelectedSection].StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)) + { + return new[] {commandName[parseResult.SelectedSection]}; } } diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs b/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs index ead59738b3..8ddaa031dc 100644 --- a/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs +++ b/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs @@ -21,26 +21,19 @@ namespace Microsoft.Repl.Commanding public static bool TryProcess(CommandInputSpecification spec, TParseResult parseResult, out DefaultCommandInput result, out IReadOnlyList processingIssues) { - List issues = new List(); - List commandNameElements = new List(); + List issues = null; + List commandNameElements = null; - if (spec.CommandName.Count > parseResult.Sections.Count) + foreach (IReadOnlyList commandName in spec.CommandName) { - issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, spec.CommandName[parseResult.Sections.Count])); - } - - for (int i = 0; i < spec.CommandName.Count && i < parseResult.Sections.Count; ++i) - { - if (!string.Equals(spec.CommandName[i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase)) + if (TryProcessCommandName(commandName, parseResult, out List nameElements, out issues)) { - issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, parseResult.Sections[i])); + commandNameElements = nameElements; + break; } - - commandNameElements.Add(new InputElement(CommandInputLocation.CommandName, parseResult.Sections[i], spec.CommandName[i], i)); } - //If we have a command name mismatch, no point in continuing - if (issues.Count > 0) + if (commandNameElements is null) { result = null; processingIssues = issues; @@ -185,6 +178,39 @@ namespace Microsoft.Repl.Commanding return issues.Count == 0; } + private static bool TryProcessCommandName(IReadOnlyList commandName, TParseResult parseResult, out List nameElements, out List processingIssues) + { + List issues = new List(); + List commandNameElements = new List(); + + if (commandName.Count > parseResult.Sections.Count) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, commandName[parseResult.Sections.Count])); + } + + for (int i = 0; i < commandName.Count && i < parseResult.Sections.Count; ++i) + { + if (!string.Equals(commandName[i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase)) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, parseResult.Sections[i])); + } + + commandNameElements.Add(new InputElement(CommandInputLocation.CommandName, parseResult.Sections[i], commandName[i], i)); + } + + processingIssues = issues; + + //If we have a command name mismatch, no point in continuing + if (issues.Count > 0) + { + nameElements = null; + return false; + } + + nameElements = commandNameElements; + return true; + } + public InputElement SelectedElement { get; } public IReadOnlyList CommandName { get; }