Address most code review comments

Pagination hasn't been dealt with yet

mac keybindings may be able to be simplified, pending some additional verification

Fix 3rd party signing cert

Bind a few common bash/zsh mappings

Check for output redirection

Make package version variable name consistent

Add --help option for the command line

Remove regions

Remove old pointer resolution code

Make command not found message more friendly

Fix issue where base address was required for requests to absolute URIs

Add options to suppress response formatting and streaming

Turn off packing for the REPL project
This commit is contained in:
Mike Lorbetske 2018-08-03 09:18:39 -07:00
parent 7487da155f
commit af5d8a8244
No known key found for this signature in database
GPG Key ID: 9EA3D5A33FBABE3B
14 changed files with 119 additions and 120 deletions

View File

@ -1,4 +1,4 @@
<Project>
<Project>
<Import
Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
@ -16,6 +16,7 @@
<SignAssembly>true</SignAssembly>
<PackageSigningCertName>MicrosoftNuGet</PackageSigningCertName>
<AssemblySigningCertName>Microsoft</AssemblySigningCertName>
<AssemblySigning3rdPartyCertName>Microsoft3rdPartyAppComponentDual</AssemblySigning3rdPartyCertName>
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

View File

@ -17,7 +17,7 @@
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<SystemDataSqlClientPackageVersion>4.5.1</SystemDataSqlClientPackageVersion>
<SystemSecurityCryptographyCngPackageVersion>4.5.0</SystemSecurityCryptographyCngPackageVersion>
<CommandLine_NewtonsoftJsonPackageVersion>11.0.2</CommandLine_NewtonsoftJsonPackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<VisualStudio_NewtonsoftJsonPackageVersion>9.0.1</VisualStudio_NewtonsoftJsonPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>

View File

@ -31,6 +31,8 @@ namespace Microsoft.HttpRepl.Commands
private const string ResponseFileOption = nameof(ResponseFileOption);
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 BodyContentOption = nameof(BodyContentOption);
private static readonly char[] HeaderSeparatorChars = new[] { '=', ':' };
@ -54,7 +56,9 @@ namespace Microsoft.HttpRepl.Commands
.WithOption(new CommandOptionSpecification(HeaderOption, requiresValue: true, forms: new[] {"--header", "-h"}))
.WithOption(new CommandOptionSpecification(ResponseFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response", }))
.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(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" }));
if (RequiresBody)
{
@ -70,9 +74,9 @@ namespace Microsoft.HttpRepl.Commands
protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
{
if (programState.BaseAddress == null)
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".Bold().Red());
shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests to a relative path".Bold().Red());
return;
}
@ -203,10 +207,10 @@ namespace Microsoft.HttpRepl.Commands
string bodyTarget = commandInput.Options[ResponseBodyFileOption].FirstOrDefault()?.Text ?? commandInput.Options[ResponseFileOption].FirstOrDefault()?.Text;
HttpResponseMessage response = await programState.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await HandleResponseAsync(programState, shellState.ConsoleManager, response, programState.EchoRequest, headersTarget, bodyTarget, cancellationToken).ConfigureAwait(false);
await HandleResponseAsync(programState, commandInput, shellState.ConsoleManager, response, programState.EchoRequest, headersTarget, bodyTarget, cancellationToken).ConfigureAwait(false);
}
private static async Task HandleResponseAsync(HttpState programState, IConsoleManager consoleManager, HttpResponseMessage response, bool echoRequest, string headersTargetFile, string bodyTargetFile, CancellationToken cancellationToken)
private static async Task HandleResponseAsync(HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, IConsoleManager consoleManager, HttpResponseMessage response, bool echoRequest, string headersTargetFile, string bodyTargetFile, CancellationToken cancellationToken)
{
RequestConfig requestConfig = new RequestConfig(programState);
ResponseConfig responseConfig = new ResponseConfig(programState);
@ -244,7 +248,7 @@ namespace Microsoft.HttpRepl.Commands
{
using (StreamWriter writer = new StreamWriter(new MemoryStream()))
{
await FormatBodyAsync(programState, consoleManager, response.RequestMessage.Content, writer, cancellationToken).ConfigureAwait(false);
await FormatBodyAsync(commandInput, programState, consoleManager, response.RequestMessage.Content, writer, cancellationToken).ConfigureAwait(false);
}
}
@ -311,7 +315,7 @@ namespace Microsoft.HttpRepl.Commands
if (response.Content != null)
{
await FormatBodyAsync(programState, consoleManager, response.Content, bodyFileWriter, cancellationToken).ConfigureAwait(false);
await FormatBodyAsync(commandInput, programState, consoleManager, response.Content, bodyFileWriter, cancellationToken).ConfigureAwait(false);
}
bodyFileWriter.Flush();
@ -321,7 +325,7 @@ namespace Microsoft.HttpRepl.Commands
consoleManager.WriteLine();
}
private static async Task FormatBodyAsync(HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken)
private static async Task FormatBodyAsync(DefaultCommandInput<ICoreParseResult> commandInput, HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken)
{
string contentType = null;
if (content.Headers.TryGetValues("Content-Type", out IEnumerable<string> contentTypeValues))
@ -331,33 +335,36 @@ namespace Microsoft.HttpRepl.Commands
contentType = contentType?.ToUpperInvariant() ?? "text/plain";
if (contentType.EndsWith("/JSON", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-JSON", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+JSON", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("/JAVASCRIPT", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-JAVASCRIPT", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+JAVASCRIPT", StringComparison.OrdinalIgnoreCase))
if (commandInput.Options[NoFormattingOption].Count == 0)
{
if (await FormatJsonAsync(programState, consoleManager, content, bodyFileWriter))
if (contentType.EndsWith("/JSON", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-JSON", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+JSON", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("/JAVASCRIPT", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-JAVASCRIPT", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+JAVASCRIPT", StringComparison.OrdinalIgnoreCase))
{
return;
if (await FormatJsonAsync(programState, consoleManager, content, bodyFileWriter))
{
return;
}
}
}
else if (contentType.EndsWith("/HTML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-HTML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+HTML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("/XML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-XML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+XML", StringComparison.OrdinalIgnoreCase))
{
if (await FormatXmlAsync(consoleManager, content, bodyFileWriter))
else if (contentType.EndsWith("/HTML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-HTML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+HTML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("/XML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("-XML", StringComparison.OrdinalIgnoreCase)
|| contentType.EndsWith("+XML", StringComparison.OrdinalIgnoreCase))
{
return;
if (await FormatXmlAsync(consoleManager, content, bodyFileWriter))
{
return;
}
}
}
//If we don't have content length, assume streaming
if (!content.Headers.TryGetValues("Content-Length", out IEnumerable<string> _))
//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<string> _))
{
Memory<char> buffer = new Memory<char>(new char[2048]);
Stream s = await content.ReadAsStreamAsync().ConfigureAwait(false);

View File

@ -30,15 +30,7 @@ namespace Microsoft.HttpRepl.Commands
{
if (parseResult.Sections.Count == 1)
{
foreach (ICommand<HttpState, ICoreParseResult> command in dispatcher.Commands)
{
string help = command.GetHelpSummary(shellState, programState);
if (!string.IsNullOrEmpty(help))
{
shellState.ConsoleManager.WriteLine(help);
}
}
CoreGetHelp(shellState, dispatcher, programState);
}
else
{
@ -171,5 +163,18 @@ namespace Microsoft.HttpRepl.Commands
return null;
}
public void CoreGetHelp(IShellState shellState, ICommandDispatcher<HttpState, ICoreParseResult> dispatcher, HttpState programState)
{
foreach (ICommand<HttpState, ICoreParseResult> command in dispatcher.Commands)
{
string help = command.GetHelpSummary(shellState, programState);
if (!string.IsNullOrEmpty(help))
{
shellState.ConsoleManager.WriteLine(help);
}
}
}
}
}

View File

@ -23,8 +23,8 @@
<ItemGroup>
<SignedPackageFile Include="tools/$(TargetFramework)/any/Microsoft.Repl.dll" Certificate="$(AssemblySigningCertName)" />
<SignedPackageFile Include="tools/$(TargetFramework)/any/Newtonsoft.Json.dll" Certificate="$(Microsoft3rdPartyAppComponentDual)" />
<SignedPackageFile Include="tools/$(TargetFramework)/any/Newtonsoft.Json.Bson.dll" Certificate="$(Microsoft3rdPartyAppComponentDual)" />
<SignedPackageFile Include="tools/$(TargetFramework)/any/Newtonsoft.Json.dll" Certificate="$(AssemblySigning3rdPartyCertName)" />
<SignedPackageFile Include="tools/$(TargetFramework)/any/Newtonsoft.Json.Bson.dll" Certificate="$(AssemblySigning3rdPartyCertName)" />
<SignedPackageFile Include="tools/$(TargetFramework)/any/System.Net.Http.Formatting.dll" Certificate="$(AssemblySigningCertName)" />
</ItemGroup>

View File

@ -145,77 +145,5 @@ namespace Microsoft.HttpRepl.OpenApi
return toResolve;
}
//public static async Task<JToken> ResolvePointerAsync(this JToken root, HttpClient client, string pointer)
//{
// if (!pointer.StartsWith("#/", StringComparison.Ordinal))
// {
// HttpResponseMessage response = await client.GetAsync(pointer).ConfigureAwait(false);
// if (!response.IsSuccessStatusCode)
// {
// //TODO: Failed to resolve pointer message
// return new JValue((object)null);
// }
// string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// try
// {
// root = JToken.Parse(responseString);
// int hashIndex = pointer.IndexOf("#");
// if (hashIndex < 0)
// {
// return root;
// }
// pointer = pointer.Substring(hashIndex);
// }
// catch (Exception ex)
// {
// //TODO: Failed to deserialize pointer message
// return new JValue((object)null);
// }
// }
// string[] pointerParts = pointer.Split('/');
// for (int i = 1; !(root is null) && i < pointerParts.Length; ++i)
// {
// if (root is JArray arr)
// {
// if (!int.TryParse(pointerParts[i], System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int result) || result < 0 || result >= arr.Count)
// {
// //TODO: Failed to resolve pointer part message (non-integer index to array or index out of range)
// return null;
// }
// root = arr[result];
// }
// else if (root is JObject obj)
// {
// root = obj[pointerParts[i]];
// if (root is null)
// {
// //TODO: Failed to resolve pointer part message (no such path in object)
// }
// JToken nestedRef = root["$ref"];
// if (nestedRef is JValue value && value.Type == JTokenType.String)
// {
// root = await ResolvePointerAsync(root, )
// }
// }
// else
// {
// //TODO: Failed to resolve pointer part message (pathing into literal)
// return null;
// }
// }
// return root;
//}
}
}

View File

@ -38,7 +38,6 @@ namespace Microsoft.HttpRepl.Preferences
}
}
#region JSON
public static string JsonArrayBraceColor { get; } = "colors.json.arrayBrace";
public static string JsonObjectBraceColor { get; } = "colors.json.objectBrace";
@ -66,7 +65,6 @@ namespace Microsoft.HttpRepl.Preferences
public static string JsonSyntaxColor { get; } = "colors.json.syntax";
public static string JsonBraceColor { get; } = "colors.json.brace";
#endregion JSON
public static string RequestColor { get; } = "colors.request";

View File

@ -1,10 +1,12 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.Repl;
using Microsoft.Repl.Commanding;
using Microsoft.Repl.ConsoleHandling;
using Microsoft.Repl.Parsing;
using Microsoft.HttpRepl.Commands;
@ -14,6 +16,12 @@ namespace Microsoft.HttpRepl
{
static async Task Main(string[] args)
{
if(Console.IsOutputRedirected)
{
Reporter.Error.WriteLine("Cannot start the REPL when output is being redirected".Bold().Red());
return;
}
var state = new HttpState();
var dispatcher = DefaultCommandDispatcher.Create(state.GetPrompt, state);
@ -44,6 +52,22 @@ namespace Microsoft.HttpRepl
shell.ShellState.ConsoleManager.AddBreakHandler(() => source.Cancel());
if (args.Length > 0)
{
if (string.Equals(args[0], "--help", StringComparison.OrdinalIgnoreCase))
{
shell.ShellState.ConsoleManager.WriteLine("Usage: dotnet httprepl [<BASE_ADDRESS>] [options]");
shell.ShellState.ConsoleManager.WriteLine();
shell.ShellState.ConsoleManager.WriteLine("Arguments:");
shell.ShellState.ConsoleManager.WriteLine(" <BASE_ADDRESS> - The initial base address for the REPL.");
shell.ShellState.ConsoleManager.WriteLine();
shell.ShellState.ConsoleManager.WriteLine("Options:");
shell.ShellState.ConsoleManager.WriteLine(" --help - Show help information.");
shell.ShellState.ConsoleManager.WriteLine();
shell.ShellState.ConsoleManager.WriteLine("REPL Commands:");
new HelpCommand().CoreGetHelp(shell.ShellState, (ICommandDispatcher<HttpState, ICoreParseResult>)shell.ShellState.CommandDispatcher, state);
return;
}
shell.ShellState.CommandDispatcher.OnReady(shell.ShellState);
shell.ShellState.InputManager.SetInput(shell.ShellState, $"set base \"{args[0]}\"");
await shell.ShellState.CommandDispatcher.ExecuteCommandAsync(shell.ShellState, CancellationToken.None).ConfigureAwait(false);

View File

@ -156,6 +156,7 @@ namespace Microsoft.Repl.Commanding
}
shellState.ConsoleManager.Error.WriteLine("No matching command found".Red().Bold());
shellState.ConsoleManager.Error.WriteLine("Execute 'help' to se available commands".Red().Bold());
}
}

View File

@ -13,6 +13,8 @@ namespace Microsoft.Repl.Input
IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler);
IInputManager RegisterKeyHandler(ConsoleKey key, ConsoleModifiers modifiers, AsyncKeyPressHandler handler);
void ResetInput();
Task StartAsync(IShellState state, CancellationToken cancellationToken);

View File

@ -12,7 +12,7 @@ namespace Microsoft.Repl.Input
{
public class InputManager : IInputManager
{
private readonly Dictionary<ConsoleKey, AsyncKeyPressHandler> _handlers = new Dictionary<ConsoleKey, AsyncKeyPressHandler>();
private readonly Dictionary<ConsoleKey, Dictionary<ConsoleModifiers, AsyncKeyPressHandler>> _handlers = new Dictionary<ConsoleKey, Dictionary<ConsoleModifiers, AsyncKeyPressHandler>>();
private readonly List<char> _inputBuffer = new List<char>();
public bool IsOverwriteMode { get; set; }
@ -29,13 +29,37 @@ namespace Microsoft.Repl.Input
public IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler)
{
if (!_handlers.TryGetValue(key, out Dictionary<ConsoleModifiers, AsyncKeyPressHandler> handlers))
{
_handlers[key] = handlers = new Dictionary<ConsoleModifiers, AsyncKeyPressHandler>();
}
if (handler == null)
{
_handlers.Remove(key);
handlers.Remove(default(ConsoleModifiers));
}
else
{
_handlers[key] = handler;
handlers[default(ConsoleModifiers)] = handler;
}
return this;
}
public IInputManager RegisterKeyHandler(ConsoleKey key, ConsoleModifiers modifiers, AsyncKeyPressHandler handler)
{
if (!_handlers.TryGetValue(key, out Dictionary<ConsoleModifiers, AsyncKeyPressHandler> handlers))
{
_handlers[key] = handlers = new Dictionary<ConsoleModifiers, AsyncKeyPressHandler>();
}
if (handler == null)
{
handlers.Remove(modifiers);
}
else
{
handlers[modifiers] = handler;
}
return this;
@ -169,7 +193,7 @@ namespace Microsoft.Repl.Input
{
ConsoleKeyInfo keyPress = state.ConsoleManager.ReadKey(cancellationToken);
if (_handlers.TryGetValue(keyPress.Key, out AsyncKeyPressHandler handler))
if (_handlers.TryGetValue(keyPress.Key, out Dictionary<ConsoleModifiers, AsyncKeyPressHandler> handlerLookup) && handlerLookup.TryGetValue(keyPress.Modifiers, out AsyncKeyPressHandler handler))
{
using (CancellationTokenSource source = new CancellationTokenSource())
using (state.ConsoleManager.AddBreakHandler(() => source.Cancel()))
@ -189,6 +213,7 @@ namespace Microsoft.Repl.Input
FlushInput(state, ref presses);
}
//TODO: Verify on a mac whether these are still needed
if (keyPress.Key == ConsoleKey.A)
{
state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition);
@ -198,6 +223,7 @@ namespace Microsoft.Repl.Input
state.ConsoleManager.MoveCaret(_inputBuffer.Count - state.ConsoleManager.CaretPosition);
}
}
//TODO: Register these like regular commands
else if (!string.IsNullOrEmpty(_ttyState) && keyPress.Modifiers == ConsoleModifiers.Alt)
{
if (presses != null)

View File

@ -14,9 +14,13 @@ namespace Microsoft.Repl.Input
{
//Navigation in line
inputManager.RegisterKeyHandler(ConsoleKey.LeftArrow, LeftArrow);
inputManager.RegisterKeyHandler(ConsoleKey.LeftArrow, ConsoleModifiers.Control, LeftArrow);
inputManager.RegisterKeyHandler(ConsoleKey.RightArrow, RightArrow);
inputManager.RegisterKeyHandler(ConsoleKey.RightArrow, ConsoleModifiers.Control, RightArrow);
inputManager.RegisterKeyHandler(ConsoleKey.Home, Home);
inputManager.RegisterKeyHandler(ConsoleKey.A, ConsoleModifiers.Control, Home);
inputManager.RegisterKeyHandler(ConsoleKey.End, End);
inputManager.RegisterKeyHandler(ConsoleKey.E, ConsoleModifiers.Control, End);
//Command history
inputManager.RegisterKeyHandler(ConsoleKey.UpArrow, UpArrow);
@ -24,9 +28,11 @@ namespace Microsoft.Repl.Input
//Completion
inputManager.RegisterKeyHandler(ConsoleKey.Tab, Tab);
inputManager.RegisterKeyHandler(ConsoleKey.Tab, ConsoleModifiers.Shift, Tab);
//Input manipulation
inputManager.RegisterKeyHandler(ConsoleKey.Escape, Escape);
inputManager.RegisterKeyHandler(ConsoleKey.U, ConsoleModifiers.Control, Escape);
inputManager.RegisterKeyHandler(ConsoleKey.Delete, Delete);
inputManager.RegisterKeyHandler(ConsoleKey.Backspace, Backspace);

View File

@ -4,6 +4,7 @@
<TargetFramework>netcoreapp2.2</TargetFramework>
<Description>A framework for creating REPLs in .NET Core.</Description>
<PackageTags>dotnet;repl</PackageTags>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>

View File

@ -10,7 +10,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(CommandLine_NewtonsoftJsonPackageVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
</ItemGroup>
<ItemGroup>