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; }