From 89ab0cfde80b1da1e87f563220fcb9bed232d2f3 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Sun, 29 Jul 2018 12:38:02 -0700 Subject: [PATCH 1/9] Initial commit of the HTTP REPL --- DotNetTools.sln | 44 ++ NuGetPackageVerifier.json | 21 + build/dependencies.props | 2 + .../AggregateDirectoryStructure.cs | 38 ++ .../Commands/BaseHttpCommand.cs | 574 ++++++++++++++++++ .../Commands/ChangeDirectoryCommand.cs | 78 +++ .../Commands/ClearCommand.cs | 62 ++ .../Commands/ConfigCommand.cs | 67 ++ .../Commands/DeleteCommand.cs | 9 + .../Commands/EchoCommand.cs | 55 ++ .../Commands/ExitCommand.cs | 29 + src/Microsoft.HttpRepl/Commands/Formatter.cs | 33 + src/Microsoft.HttpRepl/Commands/GetCommand.cs | 9 + .../Commands/HeadCommand.cs | 9 + .../Commands/HelpCommand.cs | 172 ++++++ .../Commands/ListCommand.cs | 160 +++++ .../Commands/OptionsCommand.cs | 9 + .../Commands/PatchCommand.cs | 9 + .../Commands/PostCommand.cs | 9 + .../Commands/PrefCommand.cs | 172 ++++++ src/Microsoft.HttpRepl/Commands/PutCommand.cs | 9 + src/Microsoft.HttpRepl/Commands/RunCommand.cs | 80 +++ .../Commands/SetBaseCommand.cs | 100 +++ .../Commands/SetDiagCommand.cs | 138 +++++ .../Commands/SetHeaderCommand.cs | 92 +++ .../Commands/SetSwaggerCommand.cs | 272 +++++++++ src/Microsoft.HttpRepl/Commands/TreeNode.cs | 47 ++ src/Microsoft.HttpRepl/Commands/UICommand.cs | 78 +++ .../Diagnostics/ConfigItem.cs | 9 + .../Diagnostics/DiagEndpoint.cs | 9 + .../Diagnostics/DiagEndpointMetadata.cs | 9 + .../Diagnostics/DiagItem.cs | 11 + .../Diagnostics/DiagnosticsState.cs | 13 + src/Microsoft.HttpRepl/DirectoryStructure.cs | 105 ++++ .../DirectoryStructureExtensions.cs | 57 ++ .../Formatting/JsonVisitor.cs | 111 ++++ src/Microsoft.HttpRepl/HttpState.cs | 370 +++++++++++ src/Microsoft.HttpRepl/IDirectoryStructure.cs | 24 + .../Microsoft.HttpRepl.csproj | 31 + src/Microsoft.HttpRepl/OpenApi/Either.cs | 33 + .../OpenApi/EitherConverter.cs | 32 + .../OpenApi/EndpointMetadata.cs | 17 + .../OpenApi/EndpointMetadataReader.cs | 38 ++ .../OpenApi/IEndpointMetadataReader.cs | 12 + .../OpenApiV3EndpointMetadataReader.cs | 98 +++ src/Microsoft.HttpRepl/OpenApi/Parameter.cs | 13 + src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs | 218 +++++++ src/Microsoft.HttpRepl/OpenApi/Schema.cs | 125 ++++ .../SwaggerV1EndpointMetadataReader.cs | 106 ++++ .../SwaggerV2EndpointMetadataReader.cs | 87 +++ .../Preferences/IJsonConfig.cs | 29 + .../Preferences/JsonConfig.cs | 42 ++ .../Preferences/RequestConfig.cs | 46 ++ .../Preferences/RequestOrResponseConfig.cs | 44 ++ .../Preferences/ResponseConfig.cs | 42 ++ .../Preferences/WellKnownPreference.cs | 200 ++++++ src/Microsoft.HttpRepl/Program.cs | 52 ++ .../Properties/launchSettings.json | 8 + .../Suggestions/HeaderCompletion.cs | 97 +++ .../Suggestions/ServerPathCompletion.cs | 54 ++ .../Commanding/CommandHistory.cs | 75 +++ .../Commanding/CommandInputLocation.cs | 10 + .../Commanding/CommandInputProcessingIssue.cs | 15 + .../CommandInputProcessingIssueKind.cs | 11 + .../Commanding/CommandInputSpecification.cs | 44 ++ .../CommandInputSpecificationBuilder.cs | 66 ++ .../Commanding/CommandOptionSpecification.cs | 29 + .../CommandWithStructuredInputBase.cs | 193 ++++++ .../Commanding/DefaultCommandDispatcher.cs | 169 ++++++ .../Commanding/DefaultCommandInput.cs | 193 ++++++ src/Microsoft.Repl/Commanding/ICommand.cs | 21 + .../Commanding/ICommandDispatcher.cs | 24 + .../Commanding/ICommandHistory.cs | 15 + src/Microsoft.Repl/Commanding/InputElement.cs | 29 + .../ConsoleHandling/AllowedColors.cs | 27 + .../ConsoleHandling/AnsiColorExtensions.cs | 80 +++ .../ConsoleHandling/AnsiConsole.cs | 151 +++++ .../ConsoleHandling/ConsoleManager.cs | 204 +++++++ .../ConsoleHandling/IConsoleManager.cs | 28 + .../ConsoleHandling/IWritable.cs | 15 + src/Microsoft.Repl/ConsoleHandling/Point.cs | 50 ++ .../ConsoleHandling/Reporter.cs | 115 ++++ .../ConsoleHandling/Writable.cs | 54 ++ src/Microsoft.Repl/Disposable.cs | 42 ++ src/Microsoft.Repl/IShellState.cs | 22 + .../Input/AsyncKeyPressHandler.cs | 8 + src/Microsoft.Repl/Input/IInputManager.cs | 27 + src/Microsoft.Repl/Input/InputManager.cs | 344 +++++++++++ src/Microsoft.Repl/Input/KeyHandlers.cs | 239 ++++++++ src/Microsoft.Repl/Microsoft.Repl.csproj | 9 + src/Microsoft.Repl/Parsing/CoreParseResult.cs | 78 +++ src/Microsoft.Repl/Parsing/CoreParser.cs | 129 ++++ .../Parsing/ICoreParseResult.cs | 23 + src/Microsoft.Repl/Parsing/IParser.cs | 12 + .../Scripting/IScriptExecutor.cs | 11 + .../Scripting/ScriptExecutor.cs | 49 ++ src/Microsoft.Repl/Shell.cs | 30 + src/Microsoft.Repl/ShellState.cs | 31 + .../Suggestions/FileSystemCompletion.cs | 52 ++ .../Suggestions/ISuggestionManager.cs | 9 + .../Suggestions/SuggestionManager.cs | 95 +++ src/Microsoft.Repl/Utils.cs | 12 + .../JsonVisitorTests.cs | 60 ++ .../Microsoft.HttpRepl.Tests.csproj | 21 + .../Microsoft.Repl.Tests.csproj | 19 + test/Microsoft.Repl.Tests/ParserTests.cs | 26 + 106 files changed, 7485 insertions(+) create mode 100644 src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs create mode 100644 src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/ClearCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/ConfigCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/DeleteCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/EchoCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/ExitCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/Formatter.cs create mode 100644 src/Microsoft.HttpRepl/Commands/GetCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/HeadCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/HelpCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/ListCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/OptionsCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/PatchCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/PostCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/PrefCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/PutCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/RunCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs create mode 100644 src/Microsoft.HttpRepl/Commands/TreeNode.cs create mode 100644 src/Microsoft.HttpRepl/Commands/UICommand.cs create mode 100644 src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs create mode 100644 src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs create mode 100644 src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs create mode 100644 src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs create mode 100644 src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs create mode 100644 src/Microsoft.HttpRepl/DirectoryStructure.cs create mode 100644 src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs create mode 100644 src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs create mode 100644 src/Microsoft.HttpRepl/HttpState.cs create mode 100644 src/Microsoft.HttpRepl/IDirectoryStructure.cs create mode 100644 src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj create mode 100644 src/Microsoft.HttpRepl/OpenApi/Either.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/Parameter.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/Schema.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs create mode 100644 src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/JsonConfig.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/RequestConfig.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs create mode 100644 src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs create mode 100644 src/Microsoft.HttpRepl/Program.cs create mode 100644 src/Microsoft.HttpRepl/Properties/launchSettings.json create mode 100644 src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs create mode 100644 src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandHistory.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandInputLocation.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandInputSpecification.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs create mode 100644 src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs create mode 100644 src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs create mode 100644 src/Microsoft.Repl/Commanding/DefaultCommandInput.cs create mode 100644 src/Microsoft.Repl/Commanding/ICommand.cs create mode 100644 src/Microsoft.Repl/Commanding/ICommandDispatcher.cs create mode 100644 src/Microsoft.Repl/Commanding/ICommandHistory.cs create mode 100644 src/Microsoft.Repl/Commanding/InputElement.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/IWritable.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/Point.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/Reporter.cs create mode 100644 src/Microsoft.Repl/ConsoleHandling/Writable.cs create mode 100644 src/Microsoft.Repl/Disposable.cs create mode 100644 src/Microsoft.Repl/IShellState.cs create mode 100644 src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs create mode 100644 src/Microsoft.Repl/Input/IInputManager.cs create mode 100644 src/Microsoft.Repl/Input/InputManager.cs create mode 100644 src/Microsoft.Repl/Input/KeyHandlers.cs create mode 100644 src/Microsoft.Repl/Microsoft.Repl.csproj create mode 100644 src/Microsoft.Repl/Parsing/CoreParseResult.cs create mode 100644 src/Microsoft.Repl/Parsing/CoreParser.cs create mode 100644 src/Microsoft.Repl/Parsing/ICoreParseResult.cs create mode 100644 src/Microsoft.Repl/Parsing/IParser.cs create mode 100644 src/Microsoft.Repl/Scripting/IScriptExecutor.cs create mode 100644 src/Microsoft.Repl/Scripting/ScriptExecutor.cs create mode 100644 src/Microsoft.Repl/Shell.cs create mode 100644 src/Microsoft.Repl/ShellState.cs create mode 100644 src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs create mode 100644 src/Microsoft.Repl/Suggestions/ISuggestionManager.cs create mode 100644 src/Microsoft.Repl/Suggestions/SuggestionManager.cs create mode 100644 src/Microsoft.Repl/Utils.cs create mode 100644 test/Microsoft.HttpRepl.Tests/JsonVisitorTests.cs create mode 100644 test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj create mode 100644 test/Microsoft.Repl.Tests/Microsoft.Repl.Tests.csproj create mode 100644 test/Microsoft.Repl.Tests/ParserTests.cs diff --git a/DotNetTools.sln b/DotNetTools.sln index 82fefe40d0..d996c043b6 100644 --- a/DotNetTools.sln +++ b/DotNetTools.sln @@ -56,6 +56,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.Secr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "src\Microsoft.AspNetCore.DeveloperCertificates.XPlat\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{96E71881-1465-44F5-B4B7-DF9B370FFD02}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.HttpRepl", "src\Microsoft.HttpRepl\Microsoft.HttpRepl.csproj", "{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Repl", "src\Microsoft.Repl\Microsoft.Repl.csproj", "{EE9A6128-3DE2-4206-A5A4-3ED935084590}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Repl.Tests", "test\Microsoft.Repl.Tests\Microsoft.Repl.Tests.csproj", "{59C2B354-3B5E-40EB-A7BC-74583A5707CA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.HttpRepl.Tests", "test\Microsoft.HttpRepl.Tests\Microsoft.HttpRepl.Tests.csproj", "{BE7CC4CD-CD76-4211-B593-CAC84407162A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -140,6 +148,38 @@ Global {96E71881-1465-44F5-B4B7-DF9B370FFD02}.Release|Any CPU.Build.0 = Release|Any CPU {96E71881-1465-44F5-B4B7-DF9B370FFD02}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {96E71881-1465-44F5-B4B7-DF9B370FFD02}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Release|Any CPU.Build.0 = Release|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.Release|Any CPU.Build.0 = Release|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {EE9A6128-3DE2-4206-A5A4-3ED935084590}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Release|Any CPU.Build.0 = Release|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {59C2B354-3B5E-40EB-A7BC-74583A5707CA}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.Release|Any CPU.Build.0 = Release|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {BE7CC4CD-CD76-4211-B593-CAC84407162A}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -155,6 +195,10 @@ Global {5E117F2E-7152-447F-BF47-59F759EEF3A7} = {62826851-7D74-4F1E-B7D1-12553B789CD8} {965F8820-F809-4081-9090-1AEC903F291B} = {62826851-7D74-4F1E-B7D1-12553B789CD8} {96E71881-1465-44F5-B4B7-DF9B370FFD02} = {66517987-2A5A-4330-B130-207039378FD4} + {4725BEAD-34F0-43C1-BF46-7AB16B4DE81D} = {66517987-2A5A-4330-B130-207039378FD4} + {EE9A6128-3DE2-4206-A5A4-3ED935084590} = {66517987-2A5A-4330-B130-207039378FD4} + {59C2B354-3B5E-40EB-A7BC-74583A5707CA} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} + {BE7CC4CD-CD76-4211-B593-CAC84407162A} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {57C07F14-2EAC-44FF-A277-B9221B4B2BF7} diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index fb43448889..4167e36811 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -24,12 +24,33 @@ "DotnetTool" ] }, + "dotnet-httprepl": { + "packageTypes": [ + "DotnetTool" + ], + "Exclusions": { + "ASSEMBLY_DESCRIPTION": { + "tools/netcoreapp2.2/any/System.Net.Http.Formatting.dll": "Referenced assembly, not built as part of this process" + }, + "VERSION_INFORMATIONALVERSION": { + "tools/netcoreapp2.2/any/Newtonsoft.Json.dll": "Referenced assembly, not built as part of this process", + "tools/netcoreapp2.2/any/Newtonsoft.Json.Bson.dll": "Referenced assembly, not built as part of this process" + } + } + }, "Microsoft.AspNetCore.DeveloperCertificates.XPlat": { "Exclusions": { "DOC_MISSING": { "lib/netcoreapp2.2/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package" } } + }, + "Microsoft.Repl": { + "Exclusions": { + "DOC_MISSING": { + "lib/netcoreapp2.2/Microsoft.Repl.dll": "Docs not required to shipoob package" + } + } } } }, diff --git a/build/dependencies.props b/build/dependencies.props index 0f57ae33b0..0c7d2bca9d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -13,9 +13,11 @@ 2.1.2 2.2.0-preview1-26618-02 15.6.1 + 5.2.6 2.0.3 4.5.1 4.5.0 + 10.0.1 9.0.1 2.3.1 2.4.0 diff --git a/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs b/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs new file mode 100644 index 0000000000..ed763a3e8f --- /dev/null +++ b/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.HttpRepl +{ + public class AggregateDirectoryStructure : IDirectoryStructure + { + private readonly IDirectoryStructure _first; + private readonly IDirectoryStructure _second; + + public AggregateDirectoryStructure(IDirectoryStructure first, IDirectoryStructure second) + { + _first = first; + _second = second; + } + + public IEnumerable DirectoryNames + { + get + { + HashSet values = new HashSet(StringComparer.OrdinalIgnoreCase); + values.UnionWith(_first.DirectoryNames); + values.UnionWith(_second.DirectoryNames); + return values.OrderBy(x => x, StringComparer.OrdinalIgnoreCase); + } + } + + public IDirectoryStructure Parent => _first.Parent ?? _second.Parent; + + public IDirectoryStructure GetChildDirectory(string name) + { + return new AggregateDirectoryStructure(_first.GetChildDirectory(name), _second.GetChildDirectory(name)); + } + + public IRequestInfo RequestInfo => _first.RequestInfo ?? _second.RequestInfo; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs new file mode 100644 index 0000000000..3db2f39b7b --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs @@ -0,0 +1,574 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.HttpRepl.Formatting; +using Microsoft.HttpRepl.Preferences; +using Microsoft.HttpRepl.Suggestions; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; +using Microsoft.Repl.Suggestions; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.Commands +{ + public abstract class BaseHttpCommand : CommandWithStructuredInputBase + { + private const string HeaderOption = nameof(HeaderOption); + private const string ResponseHeadersFileOption = nameof(ResponseHeadersFileOption); + private const string ResponseBodyFileOption = nameof(ResponseBodyFileOption); + private const string ResponseFileOption = nameof(ResponseFileOption); + private const string BodyFileOption = nameof(BodyFileOption); + private const string NoBodyOption = nameof(NoBodyOption); + private const string BodyContentOption = nameof(BodyContentOption); + private static readonly char[] HeaderSeparatorChars = new[] { '=', ':' }; + + private CommandInputSpecification _inputSpec; + + protected abstract string Verb { get; } + + protected abstract bool RequiresBody { get; } + + protected override CommandInputSpecification InputSpec + { + get + { + if (_inputSpec != null) + { + return _inputSpec; + } + + CommandInputSpecificationBuilder builder = CommandInputSpecification.Create(Verb) + .MaximumArgCount(1) + .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", })); + + if (RequiresBody) + { + builder = builder.WithOption(new CommandOptionSpecification(NoBodyOption, maximumOccurrences: 1, forms: "--no-body")) + .WithOption(new CommandOptionSpecification(BodyFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] {"--file", "-f"})) + .WithOption(new CommandOptionSpecification(BodyContentOption, requiresValue: true, maximumOccurrences: 1, forms: new[] {"--content", "-c"})); + } + + _inputSpec = builder.Finish(); + return _inputSpec; + } + } + + protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (programState.BaseAddress == null) + { + shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests".Bold().Red()); + return; + } + + Dictionary thisRequestHeaders = new Dictionary(); + + foreach (InputElement header in commandInput.Options[HeaderOption]) + { + int equalsIndex = header.Text.IndexOfAny(HeaderSeparatorChars); + + if (equalsIndex < 0) + { + shellState.ConsoleManager.Error.WriteLine("Headers must be formatted as {header}={value} or {header}:{value}".Bold().Red()); + return; + } + + thisRequestHeaders[header.Text.Substring(0, equalsIndex)] = header.Text.Substring(equalsIndex + 1); + } + + Uri effectivePath = programState.GetEffectivePath(commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty); + HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(Verb.ToUpperInvariant()), effectivePath); + bool noBody = false; + + if (RequiresBody) + { + string filePath = null; + string bodyContent = null; + bool deleteFile = false; + noBody = commandInput.Options[NoBodyOption].Count > 0; + + if (!noBody) + { + if (commandInput.Options[BodyFileOption].Count > 0) + { + filePath = commandInput.Options[BodyFileOption][0].Text; + + if (!File.Exists(filePath)) + { + shellState.ConsoleManager.Error.WriteLine($"Content file {filePath} does not exist".Bold().Red()); + return; + } + } + else if (commandInput.Options[BodyContentOption].Count > 0) + { + bodyContent = commandInput.Options[BodyContentOption][0].Text; + } + else + { + 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()); + return; + } + + deleteFile = true; + filePath = Path.GetTempFileName(); + + if (!thisRequestHeaders.TryGetValue("content-type", out string contentType) && programState.Headers.TryGetValue("content-type", out IEnumerable contentTypes)) + { + contentType = contentTypes.FirstOrDefault(); + } + + if (contentType == null) + { + contentType = "application/json"; + } + + string exampleBody = programState.GetExampleBody(commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty, contentType, Verb); + + if (!string.IsNullOrEmpty(exampleBody)) + { + File.WriteAllText(filePath, exampleBody); + } + + string defaultEditorArguments = programState.GetStringPreference(WellKnownPreference.DefaultEditorArguments) ?? ""; + string original = defaultEditorArguments; + string pathString = $"\"{filePath}\""; + + defaultEditorArguments = defaultEditorArguments.Replace("{filename}", pathString); + + if (string.Equals(defaultEditorArguments, original, StringComparison.Ordinal)) + { + defaultEditorArguments = (defaultEditorArguments + " " + pathString).Trim(); + } + + ProcessStartInfo info = new ProcessStartInfo(defaultEditorCommand, defaultEditorArguments); + + Process.Start(info)?.WaitForExit(); + } + } + + byte[] data = noBody + ? new byte[0] + : string.IsNullOrEmpty(bodyContent) + ? File.ReadAllBytes(filePath) + : Encoding.UTF8.GetBytes(bodyContent); + + HttpContent content = new ByteArrayContent(data); + request.Content = content; + + if (deleteFile) + { + File.Delete(filePath); + } + + foreach (KeyValuePair> header in programState.Headers) + { + content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + foreach (KeyValuePair header in thisRequestHeaders) + { + content.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + foreach (KeyValuePair> header in programState.Headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + foreach (KeyValuePair header in thisRequestHeaders) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + string headersTarget = commandInput.Options[ResponseHeadersFileOption].FirstOrDefault()?.Text ?? commandInput.Options[ResponseFileOption].FirstOrDefault()?.Text; + 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); + } + + private static async Task HandleResponseAsync(HttpState programState, IConsoleManager consoleManager, HttpResponseMessage response, bool echoRequest, string headersTargetFile, string bodyTargetFile, CancellationToken cancellationToken) + { + RequestConfig requestConfig = new RequestConfig(programState); + ResponseConfig responseConfig = new ResponseConfig(programState); + string protocolInfo; + + if (echoRequest) + { + string hostString = response.RequestMessage.RequestUri.Scheme + "://" + response.RequestMessage.RequestUri.Host + (!response.RequestMessage.RequestUri.IsDefaultPort ? ":" + response.RequestMessage.RequestUri.Port : ""); + consoleManager.WriteLine($"Request to {hostString}...".SetColor(requestConfig.AddressColor)); + consoleManager.WriteLine(); + + 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)}"; + + consoleManager.WriteLine($"{method} {pathAndQuery} {protocolInfo}"); + IEnumerable>> requestHeaders = response.RequestMessage.Headers; + + if (response.RequestMessage.Content != null) + { + requestHeaders = requestHeaders.Union(response.RequestMessage.Content.Headers); + } + + foreach (KeyValuePair> header in requestHeaders.OrderBy(x => x.Key)) + { + string headerKey = header.Key.SetColor(requestConfig.HeaderKeyColor); + string headerSep = ":".SetColor(requestConfig.HeaderSeparatorColor); + string headerValue = string.Join(";".SetColor(requestConfig.HeaderValueSeparatorColor), header.Value.Select(x => x.Trim().SetColor(requestConfig.HeaderValueColor))); + consoleManager.WriteLine($"{headerKey}{headerSep} {headerValue}"); + } + + consoleManager.WriteLine(); + + if (response.RequestMessage.Content != null) + { + using (StreamWriter writer = new StreamWriter(new MemoryStream())) + { + await FormatBodyAsync(programState, consoleManager, response.RequestMessage.Content, writer, cancellationToken).ConfigureAwait(false); + } + } + + consoleManager.WriteLine(); + consoleManager.WriteLine($"Response from {hostString}...".SetColor(requestConfig.AddressColor)); + consoleManager.WriteLine(); + } + + protocolInfo = $"{"HTTP".SetColor(responseConfig.ProtocolNameColor)}{"/".SetColor(responseConfig.ProtocolSeparatorColor)}{response.Version.ToString().SetColor(responseConfig.ProtocolVersionColor)}"; + string status = ((int)response.StatusCode).ToString().SetColor(responseConfig.StatusCodeColor) + " " + response.ReasonPhrase.SetColor(responseConfig.StatusReasonPhraseColor); + + consoleManager.WriteLine($"{protocolInfo} {status}"); + + IEnumerable>> responseHeaders = response.Headers; + + if (response.Content != null) + { + responseHeaders = responseHeaders.Union(response.Content.Headers); + } + + StreamWriter headerFileWriter; + + if (headersTargetFile != null) + { + headerFileWriter = new StreamWriter(File.Create(headersTargetFile)); + } + else + { + headerFileWriter = new StreamWriter(new MemoryStream()); + } + + foreach (KeyValuePair> header in responseHeaders.OrderBy(x => x.Key)) + { + string headerKey = header.Key.SetColor(responseConfig.HeaderKeyColor); + string headerSep = ":".SetColor(responseConfig.HeaderSeparatorColor); + string headerValue = string.Join(";".SetColor(responseConfig.HeaderValueSeparatorColor), header.Value.Select(x => x.Trim().SetColor(responseConfig.HeaderValueColor))); + consoleManager.WriteLine($"{headerKey}{headerSep} {headerValue}"); + headerFileWriter.WriteLine($"{header.Key}: {string.Join(";", header.Value.Select(x => x.Trim()))}"); + } + + StreamWriter bodyFileWriter; + if (!string.Equals(headersTargetFile, bodyTargetFile, StringComparison.Ordinal)) + { + headerFileWriter.Flush(); + headerFileWriter.Close(); + headerFileWriter.Dispose(); + + if (bodyTargetFile != null) + { + bodyFileWriter = new StreamWriter(File.Create(bodyTargetFile)); + } + else + { + bodyFileWriter = new StreamWriter(new MemoryStream()); + } + } + else + { + headerFileWriter.WriteLine(); + bodyFileWriter = headerFileWriter; + } + + consoleManager.WriteLine(); + + if (response.Content != null) + { + await FormatBodyAsync(programState, consoleManager, response.Content, bodyFileWriter, cancellationToken).ConfigureAwait(false); + } + + bodyFileWriter.Flush(); + bodyFileWriter.Close(); + bodyFileWriter.Dispose(); + + consoleManager.WriteLine(); + } + + private static async Task FormatBodyAsync(HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken) + { + string contentType = null; + if (content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues)) + { + contentType = contentTypeValues.FirstOrDefault()?.Split(';').FirstOrDefault(); + } + + 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 (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)) + { + return; + } + } + + //If we don't have content length, assume streaming + if (!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); + } + + private static async Task WaitForCompletionAsync(ValueTask readTask, CancellationToken cancellationToken) + { + while (!readTask.IsCompleted && !cancellationToken.IsCancellationRequested && !Console.KeyAvailable) + { + await Task.Delay(1, cancellationToken).ConfigureAwait(false); + } + + if (Console.KeyAvailable) + { + Console.ReadKey(false); + return false; + } + + return readTask.IsCompleted; + } + + private static async Task FormatXmlAsync(IWritable consoleManager, HttpContent content, StreamWriter bodyFileWriter) + { + string responseContent = await content.ReadAsStringAsync().ConfigureAwait(false); + try + { + XDocument body = XDocument.Parse(responseContent); + consoleManager.WriteLine(body.ToString()); + bodyFileWriter.WriteLine(body.ToString()); + return true; + } + catch + { + } + + return false; + } + + private static async Task FormatJsonAsync(HttpState programState, IWritable outputSink, HttpContent content, StreamWriter bodyFileWriter) + { + string responseContent = await content.ReadAsStringAsync().ConfigureAwait(false); + + try + { + JsonConfig config = new JsonConfig(programState); + string formatted = JsonVisitor.FormatAndColorize(config, responseContent); + outputSink.WriteLine(formatted); + bodyFileWriter.WriteLine(JToken.Parse(responseContent).ToString()); + return true; + } + catch + { + } + + return false; + } + + protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + return $"Issues a {Verb.ToUpperInvariant()} request"; + } + + public override string GetHelpSummary(IShellState shellState, HttpState programState) + { + return $"{Verb.ToLowerInvariant()} - Issues a {Verb.ToUpperInvariant()} request"; + } + + protected override IEnumerable GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput commandInput, string normalCompletionString) + { + List results = new List(); + + if (programState.Structure != null && programState.BaseAddress != null) + { + //If it's an absolute URI, nothing to suggest + if (Uri.TryCreate(parseResult.Sections[1], UriKind.Absolute, out Uri _)) + { + return null; + } + + string path = normalCompletionString.Replace('\\', '/'); + int searchFrom = normalCompletionString.Length - 1; + int lastSlash = path.LastIndexOf('/', searchFrom); + string prefix; + + if (lastSlash < 0) + { + path = string.Empty; + prefix = normalCompletionString; + } + else + { + path = path.Substring(0, lastSlash + 1); + prefix = normalCompletionString.Substring(lastSlash + 1); + } + + IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path); + + foreach (string child in s.DirectoryNames) + { + if (child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + results.Add(path + child); + } + } + } + + return results; + } + + protected override IEnumerable GetOptionValueCompletions(IShellState shellState, HttpState programState, string optionId, DefaultCommandInput commandInput, ICoreParseResult parseResult, string normalizedCompletionText) + { + if (string.Equals(optionId, BodyFileOption, StringComparison.Ordinal) || string.Equals(optionId, ResponseFileOption, StringComparison.OrdinalIgnoreCase) || string.Equals(optionId, ResponseBodyFileOption, StringComparison.OrdinalIgnoreCase) || string.Equals(optionId, ResponseHeadersFileOption, StringComparison.OrdinalIgnoreCase)) + { + return FileSystemCompletion.GetCompletions(normalizedCompletionText); + } + + if (string.Equals(optionId, HeaderOption, StringComparison.Ordinal)) + { + HashSet alreadySpecifiedHeaders = new HashSet(StringComparer.Ordinal); + IReadOnlyList options = commandInput.Options[HeaderOption]; + for (int i = 0; i < options.Count; ++i) + { + if (options[i] == commandInput.SelectedElement) + { + continue; + } + + string elementText = options[i].Text; + string existingHeaderName = elementText.Split(HeaderSeparatorChars)[0]; + alreadySpecifiedHeaders.Add(existingHeaderName); + } + + //Check to see if the selected element is in a header name or value + int equalsIndex = normalizedCompletionText.IndexOfAny(HeaderSeparatorChars); + string path = commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty; + + if (equalsIndex < 0) + { + IEnumerable headerNameOptions = HeaderCompletion.GetCompletions(alreadySpecifiedHeaders, normalizedCompletionText); + + if (headerNameOptions == null) + { + return null; + } + + List allSuggestions = new List(); + foreach (string suggestion in headerNameOptions.Select(x => x)) + { + allSuggestions.Add(suggestion + ":"); + + IEnumerable suggestions = HeaderCompletion.GetValueCompletions(Verb, path, suggestion, string.Empty, programState); + + if (suggestions != null) + { + foreach (string valueSuggestion in suggestions) + { + allSuggestions.Add(suggestion + ":" + valueSuggestion); + } + } + } + + return allSuggestions; + } + else + { + //Didn't exit from the header name check, so must be a value + string headerName = normalizedCompletionText.Substring(0, equalsIndex); + IEnumerable suggestions = HeaderCompletion.GetValueCompletions(Verb, path, headerName, normalizedCompletionText.Substring(equalsIndex + 1), programState); + + if (suggestions == null) + { + return null; + } + + return suggestions.Select(x => normalizedCompletionText.Substring(0, equalsIndex + 1) + x); + } + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs b/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs new file mode 100644 index 0000000000..3a983698cf --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.Suggestions; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class ChangeDirectoryCommand : CommandWithStructuredInputBase + { + protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (commandInput.Arguments.Count == 0 || string.IsNullOrEmpty(commandInput.Arguments[0]?.Text)) + { + shellState.ConsoleManager.WriteLine($"/{string.Join("/", programState.PathSections.Reverse())}"); + } + else + { + string[] parts = commandInput.Arguments[0].Text.Replace('\\', '/').Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + if (commandInput.Arguments[0].Text.StartsWith("/", StringComparison.Ordinal)) + { + programState.PathSections.Clear(); + } + + foreach (string part in parts) + { + switch (part) + { + case ".": + break; + case "..": + if (programState.PathSections.Count > 0) + { + programState.PathSections.Pop(); + } + break; + default: + programState.PathSections.Push(part); + break; + } + } + + shellState.ConsoleManager.WriteLine($"/{string.Join("/", programState.PathSections.Reverse())}"); + } + + return Task.CompletedTask; + } + + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("cd") + .MaximumArgCount(1) + .Finish(); + + protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + if (commandInput.Arguments.Count == 1 && !string.IsNullOrEmpty(commandInput.Arguments[0]?.Text)) + { + return "Prints the current directory if no argument is specified, otherwise changes to the specified directory"; + } + + return "Changes to the directory " + commandInput.Arguments[0].Text; + } + + public override string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "cd [directory name] - Prints the current directory if no argument is specified, otherwise changes to the specified directory"; + } + + protected override IEnumerable GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput commandInput, string normalCompletionString) + { + return ServerPathCompletion.GetCompletions(programState, normalCompletionString); + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/ClearCommand.cs b/src/Microsoft.HttpRepl/Commands/ClearCommand.cs new file mode 100644 index 0000000000..a695dfc544 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/ClearCommand.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class ClearCommand : ICommand + { + private static readonly string Name = "clear"; + private static readonly string AlternateName = "cls"; + + public bool? CanHandle(IShellState shellState, object programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count == 1 && (string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) || string.Equals(parseResult.Sections[0], AlternateName, StringComparison.OrdinalIgnoreCase)) + ? (bool?) true + : null; + } + + public Task ExecuteAsync(IShellState shellState, object programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + shellState.ConsoleManager.Clear(); + shellState.CommandDispatcher.OnReady(shellState); + return Task.CompletedTask; + } + + public string GetHelpDetails(IShellState shellState, object programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count == 1 && (string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) || string.Equals(parseResult.Sections[0], AlternateName, StringComparison.OrdinalIgnoreCase))) + { + return "Clears the shell"; + } + + return null; + } + + public string GetHelpSummary(IShellState shellState, object programState) + { + return "clear - Clears the shell"; + } + + public IEnumerable Suggest(IShellState shellState, object programState, ICoreParseResult parseResult) + { + if (parseResult.SelectedSection == 0 && + (string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { Name }; + } + + if (parseResult.SelectedSection == 0 && + (string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || AlternateName.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { Name }; + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs b/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs new file mode 100644 index 0000000000..56003280ab --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.Diagnostics; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class ConfigCommand : CommandWithStructuredInputBase + { + protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (programState.BaseAddress == null) + { + shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to query configuration".Bold().Red()); + return; + } + + if (string.IsNullOrEmpty(programState.DiagnosticsState.DiagnosticsEndpoint)) + { + shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint must be set to query configuration (see set diag)".Bold().Red()); + return; + } + + string configUrl = programState.DiagnosticsState.DiagnosticItems.FirstOrDefault(x => x.DisplayName == "Configuration")?.Url; + + if (configUrl == null) + { + shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint does not expose configuration information".Bold().Red()); + return; + } + + HttpResponseMessage response = await programState.Client.GetAsync(new Uri(programState.BaseAddress, configUrl), cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + shellState.ConsoleManager.Error.WriteLine("Unable to get configuration information from diagnostics endpoint".Bold().Red()); + return; + } + + List configItems = await response.Content.ReadAsAsync>(cancellationToken).ConfigureAwait(false); + + foreach (ConfigItem item in configItems) + { + shellState.ConsoleManager.WriteLine($"{item.Key.Cyan()}: {item.Value}"); + } + } + + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("config").Finish(); + + protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + return "config - Gets configuration information for the site if connected to a diagnostics endpoint"; + } + + public override string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "config - Gets configuration information for the site if connected to a diagnostics endpoint"; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs b/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs new file mode 100644 index 0000000000..a861b35142 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class DeleteCommand : BaseHttpCommand + { + protected override string Verb => "delete"; + + protected override bool RequiresBody => true; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/EchoCommand.cs b/src/Microsoft.HttpRepl/Commands/EchoCommand.cs new file mode 100644 index 0000000000..19eb33788b --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/EchoCommand.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class EchoCommand : CommandWithStructuredInputBase + { + private readonly HashSet _allowedModes = new HashSet(StringComparer.OrdinalIgnoreCase) {"on", "off"}; + + protected override bool CanHandle(IShellState shellState, HttpState programState, DefaultCommandInput commandInput) + { + if (commandInput.Arguments.Count == 0 || !_allowedModes.Contains(commandInput.Arguments[0]?.Text)) + { + shellState.ConsoleManager.Error.WriteLine("Allowed echo modes are 'on' and 'off'".Bold().Red()); + return false; + } + + return true; + } + + protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + bool turnOn = string.Equals(commandInput.Arguments[0].Text, "on", StringComparison.OrdinalIgnoreCase); + programState.EchoRequest = turnOn; + + shellState.ConsoleManager.WriteLine("Request echoing is " + (turnOn ? "on" : "off")); + return Task.CompletedTask; + } + + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("echo").ExactArgCount(1).Finish(); + + protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + return "Turns request echoing on or off"; + } + + public override string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "echo [on/off] - Turns request echoing on or off"; + } + + protected override IEnumerable GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput commandInput, string normalCompletionString) + { + List result = _allowedModes.Where(x => x.StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)).ToList(); + return result.Count > 0 ? result : null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/ExitCommand.cs b/src/Microsoft.HttpRepl/Commands/ExitCommand.cs new file mode 100644 index 0000000000..ebc0635876 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/ExitCommand.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class ExitCommand : CommandWithStructuredInputBase + { + protected override Task ExecuteAsync(IShellState shellState, object programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + shellState.IsExiting = true; + return Task.CompletedTask; + } + + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("exit").ExactArgCount(0).Finish(); + + protected override string GetHelpDetails(IShellState shellState, object programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + return "Exits the shell"; + } + + public override string GetHelpSummary(IShellState shellState, object programState) + { + return "exit - Exits the shell"; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/Formatter.cs b/src/Microsoft.HttpRepl/Commands/Formatter.cs new file mode 100644 index 0000000000..5a450e3406 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/Formatter.cs @@ -0,0 +1,33 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class Formatter + { + //private readonly List _prefix = new List(); + private int _prefix; + private int _maxDepth; + + public void RegisterEntry(int prefixLength, int depth) + { + //while (_prefix.Count < depth + 1) + //{ + // _prefix.Add(0); + //} + + if (depth > _maxDepth) + { + _maxDepth = depth; + } + + if (prefixLength > _prefix) + { + _prefix = prefixLength; + } + } + + public string Format(string prefix, string entry, int level) + { + string indent = "".PadRight(level * 4); + return (indent + prefix).PadRight(_prefix + 3 + _maxDepth * 4) + entry; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/Commands/GetCommand.cs b/src/Microsoft.HttpRepl/Commands/GetCommand.cs new file mode 100644 index 0000000000..76765ce374 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/GetCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class GetCommand : BaseHttpCommand + { + protected override string Verb => "get"; + + protected override bool RequiresBody => false; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/HeadCommand.cs b/src/Microsoft.HttpRepl/Commands/HeadCommand.cs new file mode 100644 index 0000000000..97023f5860 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/HeadCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class HeadCommand : BaseHttpCommand + { + protected override string Verb => "head"; + + protected override bool RequiresBody => false; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs new file mode 100644 index 0000000000..de909a4906 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.Suggestions; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class HelpCommand : ICommand + { + private static readonly string Name = "help"; + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count > 0 && string.Equals(parseResult.Sections[0], Name) + ? (bool?)true + : null; + } + + public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (shellState.CommandDispatcher is ICommandDispatcher dispatcher) + { + if (parseResult.Sections.Count == 1) + { + foreach (ICommand command in dispatcher.Commands) + { + string help = command.GetHelpSummary(shellState, programState); + + if (!string.IsNullOrEmpty(help)) + { + shellState.ConsoleManager.WriteLine(help); + } + } + } + else + { + bool anyHelp = false; + + if (parseResult.Slice(1) is ICoreParseResult continuationParseResult) + { + foreach (ICommand command in dispatcher.Commands) + { + string help = command.GetHelpDetails(shellState, programState, continuationParseResult); + + if (!string.IsNullOrEmpty(help)) + { + anyHelp = true; + shellState.ConsoleManager.WriteLine(help); + } + } + } + + if (!anyHelp) + { + //Maybe the input is an URL + if (parseResult.Sections.Count == 2) + { + IDirectoryStructure structure = programState.Structure.TraverseTo(parseResult.Sections[1]); + if (structure.DirectoryNames.Any()) + { + shellState.ConsoleManager.WriteLine("Child directories:"); + + foreach (string name in structure.DirectoryNames) + { + shellState.ConsoleManager.WriteLine(" " + name + "/"); + } + + anyHelp = true; + } + + if (structure.RequestInfo != null) + { + if (structure.RequestInfo.Methods.Count > 0) + { + if (anyHelp) + { + shellState.ConsoleManager.WriteLine(); + } + + anyHelp = true; + shellState.ConsoleManager.WriteLine("Available methods:"); + + foreach (string method in structure.RequestInfo.Methods) + { + shellState.ConsoleManager.WriteLine(" " + method.ToUpperInvariant()); + IReadOnlyList accepts = structure.RequestInfo.ContentTypesByMethod[method]; + string acceptsString = string.Join(", ", accepts.Where(x => !string.IsNullOrEmpty(x))); + if (!string.IsNullOrEmpty(acceptsString)) + { + shellState.ConsoleManager.WriteLine(" Accepts: " + acceptsString); + } + } + } + } + } + + if (!anyHelp) + { + shellState.ConsoleManager.WriteLine("Unable to locate any help information for the specified command"); + } + } + } + } + + return Task.CompletedTask; + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count > 0 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase)) + { + if (parseResult.Sections.Count > 1) + { + return "Gets help about " + parseResult.Slice(1).CommandText; + } + else + { + return "Gets help"; + } + } + + return null; + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "help - Gets help"; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.SelectedSection == 0 && + (string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { Name }; + } + else if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase)) + { + if (shellState.CommandDispatcher is ICommandDispatcher dispatcher + && parseResult.Slice(1) is ICoreParseResult continuationParseResult) + { + HashSet suggestions = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (ICommand command in dispatcher.Commands) + { + IEnumerable commandSuggestions = command.Suggest(shellState, programState, continuationParseResult); + + if (commandSuggestions != null) + { + suggestions.UnionWith(commandSuggestions); + } + } + + if (continuationParseResult.SelectedSection == 0) + { + string normalizedCompletionText = continuationParseResult.Sections[0].Substring(0, continuationParseResult.CaretPositionWithinSelectedSection); + suggestions.UnionWith(ServerPathCompletion.GetCompletions(programState, normalizedCompletionText)); + } + + return suggestions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + } + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/ListCommand.cs b/src/Microsoft.HttpRepl/Commands/ListCommand.cs new file mode 100644 index 0000000000..078a20d398 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/ListCommand.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class ListCommand : CommandWithStructuredInputBase + { + private const string RecursiveOption = nameof(RecursiveOption); + + protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (programState.Structure == null || programState.BaseAddress == null) + { + return Task.CompletedTask; + } + + string path = commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty; + + //If it's an absolute URI, nothing to suggest + if (Uri.TryCreate(path, UriKind.Absolute, out Uri _)) + { + return Task.CompletedTask; + } + + IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path); + + List roots = new List(); + Formatter formatter = new Formatter(); + + roots.Add(new TreeNode(formatter, ".", string.Empty)); + + if (s.Parent != null) + { + roots.Add(new TreeNode(formatter, "..", string.Empty)); + } + + int recursionDepth = 1; + + if (commandInput.Options[RecursiveOption].Count > 0) + { + if (string.IsNullOrEmpty(commandInput.Options[RecursiveOption][0]?.Text)) + { + recursionDepth = int.MaxValue; + } + else if (int.TryParse(commandInput.Options[RecursiveOption][0].Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int rd) && rd > 1) + { + recursionDepth = rd; + } + } + + foreach (string child in s.DirectoryNames) + { + IDirectoryStructure dir = s.GetChildDirectory(child); + + string methods = dir.RequestInfo != null && dir.RequestInfo.Methods.Count > 0 + ? "[" + string.Join("|", dir.RequestInfo.Methods) + "]" + : "[]"; + + TreeNode dirNode = new TreeNode(formatter, child, methods); + roots.Add(dirNode); + Recurse(dirNode, dir, recursionDepth - 1); + } + + foreach (TreeNode node in roots) + { + shellState.ConsoleManager.WriteLine(node.ToString()); + } + + return Task.CompletedTask; + } + + private static void Recurse(TreeNode parentNode, IDirectoryStructure parent, int remainingDepth) + { + if (remainingDepth <= 0) + { + return; + } + + foreach (string child in parent.DirectoryNames) + { + IDirectoryStructure dir = parent.GetChildDirectory(child); + + string methods = dir.RequestInfo != null && dir.RequestInfo.Methods.Count > 0 + ? "[" + string.Join("|", dir.RequestInfo.Methods) + "]" + : "[]"; + + TreeNode node = parentNode.AddChild(child, methods); + Recurse(node, dir, remainingDepth - 1); + } + } + + + + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("ls") + .MaximumArgCount(1) + .WithOption(new CommandOptionSpecification(RecursiveOption, maximumOccurrences: 1, acceptsValue: true, forms: new[] {"-r", "--recursive"})) + .Finish(); + + protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + return "Lists the contents of a directory"; + } + + public override string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "ls - Performs a directory listing"; + } + + protected override IEnumerable GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput commandInput, string normalCompletionString) + { + if (programState.Structure == null || programState.BaseAddress == null) + { + return null; + } + + //If it's an absolute URI, nothing to suggest + if (Uri.TryCreate(normalCompletionString, UriKind.Absolute, out Uri _)) + { + return null; + } + + string path = normalCompletionString.Replace('\\', '/'); + int searchFrom = normalCompletionString.Length - 1; + int lastSlash = path.LastIndexOf('/', searchFrom); + string prefix; + + if (lastSlash < 0) + { + path = string.Empty; + prefix = normalCompletionString; + } + else + { + path = path.Substring(0, lastSlash + 1); + prefix = normalCompletionString.Substring(lastSlash + 1); + } + + IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path); + + List results = new List(); + + foreach (string child in s.DirectoryNames) + { + if (child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + results.Add(path + child); + } + } + + return results; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs b/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs new file mode 100644 index 0000000000..f6a3c8903c --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class OptionsCommand : BaseHttpCommand + { + protected override string Verb => "options"; + + protected override bool RequiresBody => false; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/PatchCommand.cs b/src/Microsoft.HttpRepl/Commands/PatchCommand.cs new file mode 100644 index 0000000000..c5d9c875be --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/PatchCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class PatchCommand : BaseHttpCommand + { + protected override string Verb => "patch"; + + protected override bool RequiresBody => true; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/PostCommand.cs b/src/Microsoft.HttpRepl/Commands/PostCommand.cs new file mode 100644 index 0000000000..216fe8ef1d --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/PostCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class PostCommand : BaseHttpCommand + { + protected override string Verb => "post"; + + protected override bool RequiresBody => true; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/PrefCommand.cs b/src/Microsoft.HttpRepl/Commands/PrefCommand.cs new file mode 100644 index 0000000000..7f16af09b3 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/PrefCommand.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.Preferences; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class PrefCommand : CommandWithStructuredInputBase + { + private readonly HashSet _allowedSubcommands = new HashSet(StringComparer.OrdinalIgnoreCase) {"get", "set"}; + + public override string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "pref [get/set] {setting} [{value}] - Allows viewing or changing preferences"; + } + + protected override bool CanHandle(IShellState shellState, HttpState programState, DefaultCommandInput commandInput) + { + if (commandInput.Arguments.Count == 0 || !_allowedSubcommands.Contains(commandInput.Arguments[0]?.Text)) + { + shellState.ConsoleManager.Error.WriteLine("Whether get or set settings must be specified"); + return false; + } + + if (!string.Equals("get", commandInput.Arguments[0].Text) && (commandInput.Arguments.Count < 2 || string.IsNullOrEmpty(commandInput.Arguments[1]?.Text))) + { + shellState.ConsoleManager.Error.WriteLine("The preference to set must be specified"); + return false; + } + + return true; + } + + protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult) + { + if (commandInput.Arguments.Count == 0 || !_allowedSubcommands.Contains(commandInput.Arguments[0]?.Text)) + { + return "pref [get/set] {setting} [{value}] - Get or sets a preference to a particular value"; + } + + if (string.Equals(commandInput.Arguments[0].Text, "get", StringComparison.OrdinalIgnoreCase)) + { + return "pref get [{setting}] - Gets the value of the specified preference or lists all preferences if no preference is specified"; + } + else + { + return "pref set {setting} [{value}] - Sets (or clears if value is not specified) the value of the specified preference"; + } + } + + protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (string.Equals(commandInput.Arguments[0].Text, "get", StringComparison.OrdinalIgnoreCase)) + { + return GetSetting(shellState, programState, commandInput); + } + + return SetSetting(shellState, programState, commandInput); + } + + private static Task SetSetting(IShellState shellState, HttpState programState, DefaultCommandInput commandInput) + { + string prefName = commandInput.Arguments[1].Text; + string prefValue = commandInput.Arguments.Count > 2 ? commandInput.Arguments[2]?.Text : null; + + if (string.IsNullOrEmpty(prefValue)) + { + if (!programState.DefaultPreferences.TryGetValue(prefName, out string defaultValue)) + { + programState.Preferences.Remove(prefName); + } + else + { + programState.Preferences[prefName] = defaultValue; + } + } + else + { + programState.Preferences[prefName] = prefValue; + } + + if (!programState.SavePreferences()) + { + shellState.ConsoleManager.Error.WriteLine("Error saving preferences".Bold().Red()); + } + + return Task.CompletedTask; + } + + private static Task GetSetting(IShellState shellState, HttpState programState, DefaultCommandInput commandInput) + { + string preferenceName = commandInput.Arguments.Count > 1 ? commandInput.Arguments[1]?.Text : null; + + //If there's a particular setting to get the value of + if (!string.IsNullOrEmpty(preferenceName)) + { + if (programState.Preferences.TryGetValue(preferenceName, out string value)) + { + shellState.ConsoleManager.WriteLine("Configured value: " + value); + } + else + { + shellState.ConsoleManager.Error.WriteLine((commandInput.Arguments[1].Text + " does not have a configured value").Bold().Red()); + } + } + else + { + foreach (KeyValuePair entry in programState.Preferences.OrderBy(x => x.Key)) + { + shellState.ConsoleManager.WriteLine($"{entry.Key}={entry.Value}"); + } + } + + return Task.CompletedTask; + } + + protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("pref") + .MinimumArgCount(1) + .MaximumArgCount(3) + .Finish(); + + + protected override IEnumerable GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput commandInput, string normalCompletionString) + { + if (parseResult.SelectedSection == 1) + { + return _allowedSubcommands.Where(x => x.StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)); + } + + if (parseResult.SelectedSection == 2) + { + string prefix = parseResult.Sections.Count > 2 ? normalCompletionString : string.Empty; + List matchingProperties = new List(); + + foreach (string val in WellKnownPreference.Catalog.Names) + { + if (val.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + matchingProperties.Add(val); + } + } + + return matchingProperties; + } + + if (parseResult.SelectedSection == 3 + && parseResult.Sections[2].StartsWith("colors.", StringComparison.OrdinalIgnoreCase)) + { + string prefix = parseResult.Sections.Count > 3 ? normalCompletionString : string.Empty; + List matchingProperties = new List(); + + foreach (string val in Enum.GetNames(typeof(AllowedColors))) + { + if (val.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + matchingProperties.Add(val); + } + } + + return matchingProperties; + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/PutCommand.cs b/src/Microsoft.HttpRepl/Commands/PutCommand.cs new file mode 100644 index 0000000000..a65439dfb7 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/PutCommand.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Commands +{ + public class PutCommand : BaseHttpCommand + { + protected override string Verb => "put"; + + protected override bool RequiresBody => true; + } +} diff --git a/src/Microsoft.HttpRepl/Commands/RunCommand.cs b/src/Microsoft.HttpRepl/Commands/RunCommand.cs new file mode 100644 index 0000000000..6dd2f4a378 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/RunCommand.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; +using Microsoft.Repl.Scripting; +using Microsoft.Repl.Suggestions; + +namespace Microsoft.HttpRepl.Commands +{ + public class RunCommand : ICommand + { + private static readonly string Name = "run"; + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count > 1 && parseResult.Sections.Count < 4 && string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) + ? (bool?)true + : null; + } + + public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (!File.Exists(parseResult.Sections[1])) + { + shellState.ConsoleManager.Error.WriteLine($"Could not file script file {parseResult.Sections[1]}"); + return; + } + + bool suppressScriptLinesInHistory = true; + if (parseResult.Sections.Count == 3) + { + suppressScriptLinesInHistory = !string.Equals(parseResult.Sections[2], "+history"); + } + + string[] lines = File.ReadAllLines(parseResult.Sections[1]); + IScriptExecutor scriptExecutor = new ScriptExecutor(suppressScriptLinesInHistory); + await scriptExecutor.ExecuteScriptAsync(shellState, lines, cancellationToken).ConfigureAwait(false); + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count > 0 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase)) + { + if (parseResult.Sections.Count == 1) + { + return "Runs the specified script"; + } + + return "Runs the script " + parseResult.Sections[1]; + } + + return null; + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "run {path to script} - Runs a script"; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.SelectedSection == 0 && + (string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { Name }; + } + + if (parseResult.SelectedSection == 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase)) + { + return FileSystemCompletion.GetCompletions(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection)); + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs b/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs new file mode 100644 index 0000000000..c346f541d4 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class SetBaseCommand : ICommand + { + private const string Name = "set"; + private const string SubCommand = "base"; + + public string Description => "Sets the base address to direct requests to."; + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase) + ? (bool?)true + : null; + } + + public async Task ExecuteAsync(IShellState shellState, HttpState state, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (parseResult.Sections.Count == 2) + { + state.BaseAddress = null; + } + 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()); + } + else + { + state.BaseAddress = serverUri; + } + + if (state.BaseAddress == null || !Uri.TryCreate(state.BaseAddress, "/swagger/v1/swagger.json", out Uri result)) + { + state.SwaggerStructure = null; + } + else + { + await SetSwaggerCommand.CreateDirectoryStructureForSwaggerEndpointAsync(shellState, state, result, cancellationToken).ConfigureAwait(false); + if (state.SwaggerStructure != null) + { + shellState.ConsoleManager.WriteLine("Using swagger metadata from " + result); + } + } + } + + private string EnsureTrailingSlash(string v) + { + if (!v.EndsWith("/", StringComparison.Ordinal)) + { + v += "/"; + } + + return v; + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)) + { + return Description; + } + + return null; + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return Description; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count == 0) + { + return new[] { Name }; + } + + if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)) + { + return new[] { Name }; + } + + if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { SubCommand }; + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs b/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs new file mode 100644 index 0000000000..4e86452b7c --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.Diagnostics; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class SetDiagCommand : ICommand + { + private static readonly string Name = "set"; + private static readonly string SubCommand = "diag"; + + public string Description => "Sets the diagnostics path to direct requests to."; + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase) + ? (bool?)true + : null; + } + + public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (parseResult.Sections.Count == 2) + { + programState.DiagnosticsState.DiagnosticsEndpoint = null; + programState.DiagnosticsState.DiagnosticItems = null; + programState.DiagnosticsState.DiagEndpointsStructure = null; + return; + } + + 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()); + } + else + { + programState.DiagnosticsState.DiagnosticsEndpoint = parseResult.Sections[2]; + HttpResponseMessage response = await programState.Client.GetAsync(new Uri(programState.BaseAddress, programState.DiagnosticsState.DiagnosticsEndpoint), cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + shellState.ConsoleManager.Error.WriteLine("Unable to access diagnostics endpoint".Bold().Red()); + programState.DiagnosticsState.DiagnosticsEndpoint = null; + programState.DiagnosticsState.DiagnosticItems = null; + } + else + { + programState.DiagnosticsState.DiagnosticItems = (await response.Content.ReadAsAsync>(cancellationToken).ConfigureAwait(false))?.Select(x => x.Value).ToList(); + + DiagItem endpointsItem = programState.DiagnosticsState.DiagnosticItems?.FirstOrDefault(x => string.Equals(x.DisplayName, "Endpoints", StringComparison.OrdinalIgnoreCase)); + + if (endpointsItem != null) + { + HttpResponseMessage endpointsResponse = await programState.Client.GetAsync(new Uri(programState.BaseAddress, endpointsItem.Url), cancellationToken).ConfigureAwait(false); + + if (!endpointsResponse.IsSuccessStatusCode) + { + shellState.ConsoleManager.Error.WriteLine("Unable to get endpoints information from diagnostics endpoint".Bold().Red()); + return; + } + + List endpoints = await endpointsResponse.Content.ReadAsAsync>(cancellationToken).ConfigureAwait(false); + DirectoryStructure structure = new DirectoryStructure(null); + + foreach (DiagEndpoint endpoint in endpoints) + { + if (endpoint.Url.StartsWith(endpointsItem.Url, StringComparison.OrdinalIgnoreCase) + || endpoint.Url.StartsWith("/graphql", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + FillDirectoryInfo(structure, endpoint.Url); + } + + programState.DiagnosticsState.DiagEndpointsStructure = structure; + } + } + } + } + + private static void FillDirectoryInfo(DirectoryStructure parent, string endpoint) + { + string[] parts = endpoint.Split('/'); + + foreach (string part in parts) + { + if (!string.IsNullOrEmpty(part)) + { + parent = parent.DeclareDirectory(part); + } + } + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return Description; + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)) + { + return Description; + } + + return null; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count == 0) + { + return new[] { Name }; + } + + if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)) + { + return new[] { Name }; + } + + if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { SubCommand }; + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs b/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs new file mode 100644 index 0000000000..c95d22a6be --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.Suggestions; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class SetHeaderCommand : ICommand + { + private static readonly string Name = "set"; + private static readonly string SubCommand = "header"; + + public string Description => "set header {name} [{{value}}] - Sets or clears a header"; + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count > 2 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase) + ? (bool?)true + : null; + } + + public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (parseResult.Sections.Count == 3) + { + programState.Headers.Remove(parseResult.Sections[2]); + } + else + { + programState.Headers[parseResult.Sections[2]] = parseResult.Sections.Skip(3).ToList(); + } + + return Task.CompletedTask; + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)) + { + return Description; + } + + return null; + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return Description; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count == 0) + { + return new[] { Name }; + } + + if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)) + { + return new[] { Name }; + } + + if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { SubCommand }; + } + + if (parseResult.Sections.Count > 2 + && string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) + && string.Equals(SubCommand, parseResult.Sections[1], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 2) + { + string prefix = parseResult.Sections[2].Substring(0, parseResult.CaretPositionWithinSelectedSection); + return HeaderCompletion.GetCompletions(null, prefix); + } + + if (parseResult.Sections.Count > 3 + && string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) + && string.Equals(SubCommand, parseResult.Sections[1], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 3) + { + string prefix = parseResult.Sections[3].Substring(0, parseResult.CaretPositionWithinSelectedSection); + return HeaderCompletion.GetValueCompletions(null, string.Empty, parseResult.Sections[2], prefix, programState); + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs b/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs new file mode 100644 index 0000000000..9c9a1f36cc --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.HttpRepl.OpenApi; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.Commands +{ + public class SetSwaggerCommand : ICommand + { + private static readonly string Name = "set"; + private static readonly string SubCommand = "swagger"; + + public string Description => "Sets the swagger document to use for information about the current server"; + + private static void FillDirectoryInfo(DirectoryStructure parent, EndpointMetadata entry) + { + string[] parts = entry.Path.Split('/'); + + foreach (string part in parts) + { + if (!string.IsNullOrEmpty(part)) + { + parent = parent.DeclareDirectory(part); + } + } + + RequestInfo dirRequestInfo = new RequestInfo(); + + foreach (KeyValuePair>> requestInfo in entry.AvailableRequests) + { + string method = requestInfo.Key; + + foreach (KeyValuePair> parameterSetsByContentType in requestInfo.Value) + { + if (string.IsNullOrEmpty(parameterSetsByContentType.Key)) + { + dirRequestInfo.SetFallbackRequestBody(method, GetBodyString(null, parameterSetsByContentType.Value)); + } + + dirRequestInfo.SetRequestBody(method, parameterSetsByContentType.Key, GetBodyString(parameterSetsByContentType.Key, parameterSetsByContentType.Value)); + } + + dirRequestInfo.AddMethod(method); + } + + if (dirRequestInfo.Methods.Count > 0) + { + parent.RequestInfo = dirRequestInfo; + } + } + + private static string GetBodyString(string contentType, IEnumerable operation) + { + Parameter body = operation.FirstOrDefault(x => string.Equals(x.Location, "body", StringComparison.OrdinalIgnoreCase)); + + if (body != null) + { + JToken result = GenerateData(body.Schema); + return result?.ToString() ?? "{\n}"; + } + + return null; + } + + private static JToken GenerateData(Schema schema) + { + if (schema == null) + { + return null; + } + + if (schema.Example != null) + { + return JToken.FromObject(schema.Example); + } + + if (schema.Default != null) + { + return JToken.FromObject(schema.Default); + } + + if (schema.Type is null) + { + if (schema.Properties != null || schema.AdditionalProperties != null || schema.MinProperties.HasValue || schema.MaxProperties.HasValue) + { + schema.Type = "OBJECT"; + } + else if (schema.Items != null || schema.MinItems.HasValue || schema.MaxItems.HasValue) + { + schema.Type = "ARRAY"; + } + else if (schema.Minimum.HasValue || schema.Maximum.HasValue || schema.MultipleOf.HasValue) + { + schema.Type = "INTEGER"; + } + } + + switch (schema.Type?.ToUpperInvariant()) + { + case null: + case "STRING": + return ""; + case "NUMBER": + if (schema.Minimum.HasValue) + { + if (schema.Maximum.HasValue) + { + return (schema.Maximum.Value + schema.Minimum.Value) / 2; + } + + if (schema.ExclusiveMinimum) + { + return schema.Minimum.Value + 1; + } + + return schema.Minimum.Value; + } + return 1.1; + case "INTEGER": + if (schema.Minimum.HasValue) + { + if (schema.Maximum.HasValue) + { + return (int)((schema.Maximum.Value + schema.Minimum.Value) / 2); + } + + if (schema.ExclusiveMinimum) + { + return schema.Minimum.Value + 1; + } + + return schema.Minimum.Value; + } + return 0; + case "BOOLEAN": + return true; + case "ARRAY": + JArray container = new JArray(); + JToken item = GenerateData(schema.Items) ?? ""; + + int count = schema.MinItems.GetValueOrDefault(); + count = Math.Max(1, count); + + for (int i = 0; i < count; ++i) + { + container.Add(item.DeepClone()); + } + + return container; + case "OBJECT": + JObject obj = new JObject(); + foreach (KeyValuePair property in schema.Properties) + { + JToken data = GenerateData(property.Value) ?? ""; + obj[property.Key] = data; + } + return obj; + } + + return null; + } + + private static async Task> GetSwaggerDocAsync(HttpClient client, Uri uri) + { + var resp = await client.GetAsync(uri).ConfigureAwait(false); + resp.EnsureSuccessStatusCode(); + string responseString = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + JsonSerializer serializer = new JsonSerializer{ PreserveReferencesHandling = PreserveReferencesHandling.All }; + JObject responseObject = (JObject)serializer.Deserialize(new StringReader(responseString), typeof(JObject)); + EndpointMetadataReader reader = new EndpointMetadataReader(); + responseObject = await PointerUtil.ResolvePointersAsync(uri, responseObject, client).ConfigureAwait(false) as JObject; + + if (responseObject is null) + { + return new EndpointMetadata[0]; + } + + return reader.Read(responseObject); + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return Description; + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)) + { + return Description; + } + + return null; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count == 0) + { + return new[] { Name }; + } + + if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)) + { + return new[] { Name }; + } + + if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { SubCommand }; + } + + return null; + } + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase) + ? (bool?)true + : null; + } + + public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (parseResult.Sections.Count == 2) + { + programState.SwaggerStructure = null; + return; + } + + 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()); + } + else + { + await CreateDirectoryStructureForSwaggerEndpointAsync(shellState, programState, serverUri, cancellationToken).ConfigureAwait(false); + } + } + + internal static async Task CreateDirectoryStructureForSwaggerEndpointAsync(IShellState shellState, HttpState programState, Uri serverUri, CancellationToken cancellationToken) + { + try + { + IEnumerable doc = await GetSwaggerDocAsync(programState.Client, serverUri).ConfigureAwait(false); + + DirectoryStructure d = new DirectoryStructure(null); + + foreach (EndpointMetadata entry in doc) + { + FillDirectoryInfo(d, entry); + } + + programState.SwaggerStructure = !cancellationToken.IsCancellationRequested ? d : null; + } + catch + { + programState.SwaggerStructure = null; + } + } + } +} diff --git a/src/Microsoft.HttpRepl/Commands/TreeNode.cs b/src/Microsoft.HttpRepl/Commands/TreeNode.cs new file mode 100644 index 0000000000..256d5d9dae --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/TreeNode.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.HttpRepl.Commands +{ + public class TreeNode + { + private readonly int _depth; + private readonly Formatter _formatter; + private readonly string _prefix; + private readonly string _entry; + private readonly List _children = new List(); + + public TreeNode(Formatter formatter, string prefix, string entry) + : this(formatter, prefix, entry, 0) + { + } + + private TreeNode(Formatter formatter, string prefix, string entry, int depth) + { + _formatter = formatter; + formatter.RegisterEntry(prefix.Length, depth); + _prefix = prefix; + _entry = entry; + _depth = depth; + } + + public TreeNode AddChild(string prefix, string entry) + { + TreeNode child = new TreeNode(_formatter, prefix, entry, _depth + 1); + _children.Add(child); + return child; + } + + public override string ToString() + { + string self = _formatter.Format(_prefix, _entry, _depth); + + if (_children.Count == 0) + { + return self; + } + + return self + Environment.NewLine + string.Join(Environment.NewLine, _children); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/Commands/UICommand.cs b/src/Microsoft.HttpRepl/Commands/UICommand.cs new file mode 100644 index 0000000000..ddf4ca4097 --- /dev/null +++ b/src/Microsoft.HttpRepl/Commands/UICommand.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.HttpRepl.Commands +{ + public class UICommand : ICommand + { + private static readonly string Name = "ui"; + + public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + return parseResult.Sections.Count == 1 && string.Equals(parseResult.Sections[0], Name) + ? (bool?)true + : null; + } + + public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken) + { + if (programState.BaseAddress == null) + { + shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to launch Swagger UI".Bold().Red()); + return Task.CompletedTask; + } + + Uri uri = new Uri(programState.BaseAddress, "swagger"); + string agent = "cmd"; + string agentParam = $"/c {uri.AbsoluteUri}"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + agent = "open"; + agentParam = uri.AbsoluteUri; + } + else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + agent = "xdg-open"; + agentParam = uri.AbsoluteUri; + } + + Process.Start(new ProcessStartInfo(agent, agentParam) { CreateNoWindow = true }); + return Task.CompletedTask; + } + + public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.Sections.Count == 1 && string.Equals(parseResult.Sections[0], Name)) + { + return "ui - Launches the Swagger UI page (if available) in the default browser"; + } + + return null; + } + + public string GetHelpSummary(IShellState shellState, HttpState programState) + { + return "ui - Launches the Swagger UI page (if available) in the default browser"; + } + + public IEnumerable Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult) + { + if (parseResult.SelectedSection == 0 && + (string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))) + { + return new[] { Name }; + } + + return null; + } + } +} diff --git a/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs b/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs new file mode 100644 index 0000000000..ba16701d58 --- /dev/null +++ b/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Diagnostics +{ + public class ConfigItem + { + public string Key { get; set; } + + public string Value { get; set; } + } +} diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs new file mode 100644 index 0000000000..80e619dcc3 --- /dev/null +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Diagnostics +{ + public class DiagEndpoint + { + public string DisplayName { get; set; } + public string Url { get; set; } + public DiagEndpointMetadata[] Metadata { get; set; } + } +} diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs new file mode 100644 index 0000000000..53e5aa3918 --- /dev/null +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs @@ -0,0 +1,9 @@ +namespace Microsoft.HttpRepl.Diagnostics +{ + public class DiagEndpointMetadata + { + public object Item { get; set; } + public string Type { get; set; } + public string[] Interfaces { get; set; } + } +} diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs new file mode 100644 index 0000000000..c0fbf2df9c --- /dev/null +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs @@ -0,0 +1,11 @@ +namespace Microsoft.HttpRepl.Diagnostics +{ + public class DiagItem + { + public string DisplayName { get; set; } + + public string Description { get; set; } + + public string Url { get; set; } + } +} diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs new file mode 100644 index 0000000000..3fe27cce19 --- /dev/null +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Microsoft.HttpRepl.Diagnostics +{ + public class DiagnosticsState + { + public string DiagnosticsEndpoint { get; set; } + + public IReadOnlyList DiagnosticItems { get; internal set; } + + public IDirectoryStructure DiagEndpointsStructure { get; set; } + } +} diff --git a/src/Microsoft.HttpRepl/DirectoryStructure.cs b/src/Microsoft.HttpRepl/DirectoryStructure.cs new file mode 100644 index 0000000000..f682488b9b --- /dev/null +++ b/src/Microsoft.HttpRepl/DirectoryStructure.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.HttpRepl +{ + public class DirectoryStructure : IDirectoryStructure + { + private readonly Dictionary _childDirectories = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public DirectoryStructure(IDirectoryStructure parent) + { + Parent = parent; + } + + public IEnumerable DirectoryNames => _childDirectories.Keys; + + public IDirectoryStructure Parent { get; } + + public DirectoryStructure DeclareDirectory(string name) + { + if (_childDirectories.TryGetValue(name, out DirectoryStructure existing)) + { + return existing; + } + + return _childDirectories[name] = new DirectoryStructure(this); + } + + public IDirectoryStructure GetChildDirectory(string name) + { + if (_childDirectories.TryGetValue(name, out DirectoryStructure result)) + { + return result; + } + + IDirectoryStructure parameterizedTarget = _childDirectories.FirstOrDefault(x => x.Key.StartsWith('{') && x.Key.EndsWith('}')).Value; + + if (!(parameterizedTarget is null)) + { + return parameterizedTarget; + } + + return new DirectoryStructure(this); + } + + public IRequestInfo RequestInfo { get; set; } + } + + public class RequestInfo : IRequestInfo + { + private readonly HashSet _methods = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _requestBodiesByMethodByContentType = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _fallbackBodyStringsByMethod = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _contentTypesByMethod = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public IReadOnlyList Methods => _methods.ToList(); + + public IReadOnlyDictionary> ContentTypesByMethod => _contentTypesByMethod; + + public string GetRequestBodyForContentType(string contentType, string method) + { + if (_requestBodiesByMethodByContentType.TryGetValue(method, out Dictionary bodiesByContentType) + && bodiesByContentType.TryGetValue(contentType, out string body)) + { + return body; + } + + if (_fallbackBodyStringsByMethod.TryGetValue(method, out body)) + { + return body; + } + + return null; + } + + public void SetRequestBody(string method, string contentType, string body) + { + if (!_requestBodiesByMethodByContentType.TryGetValue(method, out Dictionary bodiesByContentType)) + { + _requestBodiesByMethodByContentType[method] = bodiesByContentType = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (!_contentTypesByMethod.TryGetValue(method, out IReadOnlyList contentTypesRaw)) + { + _contentTypesByMethod[method] = contentTypesRaw = new List(); + } + + List contentTypes = (List)contentTypesRaw; + contentTypes.Add(contentType); + + bodiesByContentType[contentType] = body; + } + + public void AddMethod(string method) + { + _methods.Add(method); + } + + public void SetFallbackRequestBody(string method, string fallbackBodyString) + { + _fallbackBodyStringsByMethod[method] = fallbackBodyString; + } + } +} diff --git a/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs b/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs new file mode 100644 index 0000000000..2b7ad5341a --- /dev/null +++ b/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.HttpRepl +{ + public static class DirectoryStructureExtensions + { + public static IEnumerable GetDirectoryListingAtPath(this IDirectoryStructure structure, string path) + { + return structure.TraverseTo(path).DirectoryNames; + } + + public static IDirectoryStructure TraverseTo(this IDirectoryStructure structure, string path) + { + string[] parts = path.Replace('\\', '/').Split('/'); + return structure.TraverseTo(parts); + } + + public static IDirectoryStructure TraverseTo(this IDirectoryStructure structure, IEnumerable pathParts) + { + IDirectoryStructure s = structure; + IReadOnlyList parts = pathParts.ToList(); + + if (parts.Count == 0) + { + return s; + } + + if (parts[0] == string.Empty && parts.Count > 1) + { + while (s.Parent != null) + { + s = s.Parent; + } + } + + foreach (string part in parts) + { + if (part == ".") + { + continue; + } + + if (part == "..") + { + s = s.Parent ?? s; + } + else if (!string.IsNullOrEmpty(part)) + { + s = s.GetChildDirectory(part); + } + } + + return s; + } + } +} diff --git a/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs b/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs new file mode 100644 index 0000000000..fa40ee62ab --- /dev/null +++ b/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs @@ -0,0 +1,111 @@ +using System.IO; +using System.Text; +using Microsoft.HttpRepl.Preferences; +using Microsoft.Repl.ConsoleHandling; +using Newtonsoft.Json; + +namespace Microsoft.HttpRepl.Formatting +{ + public static class JsonVisitor + { + public static string FormatAndColorize(IJsonConfig config, string jsonData) + { + if (jsonData == null) + { + return string.Empty; + } + + StringBuilder result = new StringBuilder(); + JsonTextReader reader = new JsonTextReader(new StringReader(jsonData)); + bool isValuePosition = false; + bool isTerminalValue = false; + bool isFirstToken = true; + + while (reader.Read()) + { + if (!isValuePosition) + { + //If we're about to write an end object/array, we shouldn't have a comma + if (reader.TokenType != JsonToken.EndArray && reader.TokenType != JsonToken.EndObject + && isTerminalValue) + { + result.Append(",".SetColor(config.CommaColor)); + } + + if (!isFirstToken) + { + result.AppendLine(); + } + } + + isFirstToken = false; + + if (!isValuePosition) + { + result.Append("".PadLeft(reader.Depth * config.IndentSize)); + } + + isTerminalValue = false; + isValuePosition = false; + JsonToken type = reader.TokenType; + + switch (type) + { + case JsonToken.StartObject: + result.Append("{".SetColor(config.ObjectBraceColor)); + break; + case JsonToken.EndObject: + result.Append("}".SetColor(config.ObjectBraceColor)); + isTerminalValue = true; + break; + case JsonToken.StartArray: + result.Append("[".SetColor(config.ArrayBraceColor)); + break; + case JsonToken.EndArray: + result.Append("]".SetColor(config.ArrayBraceColor)); + isTerminalValue = true; + break; + case JsonToken.PropertyName: + result.Append((reader.QuoteChar.ToString() + reader.Value + reader.QuoteChar).SetColor(config.NameColor) + ": ".SetColor(config.NameSeparatorColor)); + isValuePosition = true; + break; + case JsonToken.Boolean: + result.Append(reader.Value.ToString().ToLowerInvariant().SetColor(config.BoolColor)); + isTerminalValue = true; + break; + case JsonToken.Integer: + case JsonToken.Float: + result.Append(reader.Value.ToString().ToLowerInvariant().SetColor(config.NumericColor)); + isTerminalValue = true; + break; + case JsonToken.Null: + result.Append("null".SetColor(config.NullColor)); + isTerminalValue = true; + break; + case JsonToken.Comment: + result.Append(("//" + reader.Value).SetColor(config.NumericColor)); + break; + case JsonToken.String: + result.Append((reader.QuoteChar.ToString() + reader.Value + reader.QuoteChar.ToString()).SetColor(config.StringColor)); + isTerminalValue = true; + break; + case JsonToken.Raw: + case JsonToken.Date: + case JsonToken.Bytes: + case JsonToken.Undefined: + case JsonToken.None: + result.Append(reader.Value.ToString().SetColor(config.DefaultColor)); + isTerminalValue = true; + break; + case JsonToken.EndConstructor: + case JsonToken.StartConstructor: + default: + result.Append(reader.Value.ToString().SetColor(config.DefaultColor)); + break; + } + } + + return result.ToString(); + } + } +} diff --git a/src/Microsoft.HttpRepl/HttpState.cs b/src/Microsoft.HttpRepl/HttpState.cs new file mode 100644 index 0000000000..55cfdd6188 --- /dev/null +++ b/src/Microsoft.HttpRepl/HttpState.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; +using Microsoft.HttpRepl.Diagnostics; +using Microsoft.HttpRepl.Preferences; + +namespace Microsoft.HttpRepl +{ + public class HttpState + { + private string _userProfileDir; + private string _prefsFilePath; + + public HttpClient Client { get; } + + public Stack PathSections { get; } + + public IDirectoryStructure SwaggerStructure { get; set; } + + public IDirectoryStructure Structure => DiagnosticsState.DiagEndpointsStructure == null + ? SwaggerStructure + : SwaggerStructure == null + ? DiagnosticsState.DiagEndpointsStructure + : new AggregateDirectoryStructure(SwaggerStructure, DiagnosticsState.DiagEndpointsStructure); + + public Uri BaseAddress { get; set; } + + public bool EchoRequest { get; set; } + + public Dictionary Preferences { get; } + + public IReadOnlyDictionary DefaultPreferences { get; } + + public string UserProfileDir + { + get + { + if (_userProfileDir == null) + { + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + string profileDir = Environment.GetEnvironmentVariable(isWindows + ? "USERPROFILE" + : "HOME"); + + _userProfileDir = profileDir; + } + + return _userProfileDir; + } + } + + public string PrefsFilePath => _prefsFilePath ?? (_prefsFilePath = Path.Combine(UserProfileDir, ".httpreplprefs")); + + public Dictionary> Headers { get; } + + public DiagnosticsState DiagnosticsState { get; } + + public HttpState() + { + Client = new HttpClient(); + PathSections = new Stack(); + Preferences = new Dictionary(); + DefaultPreferences = CreateDefaultPreferencs(); + Headers = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "User-Agent", new[] { "HTTP-REPL" } } + }; + Preferences = new Dictionary(DefaultPreferences); + LoadPreferences(); + DiagnosticsState = new DiagnosticsState(); + } + + public string GetPrompt() + { + return $"{GetEffectivePath(new string[0], false, out int _)?.ToString() ?? "(Disconnected)"}~ "; + } + + private void LoadPreferences() + { + if (File.Exists(PrefsFilePath)) + { + string[] prefsFile = File.ReadAllLines(PrefsFilePath); + + foreach (string line in prefsFile) + { + int equalsIndex = line.IndexOf('='); + + if (equalsIndex < 0) + { + continue; + } + + Preferences[line.Substring(0, equalsIndex)] = line.Substring(equalsIndex + 1); + } + } + } + + private IReadOnlyDictionary CreateDefaultPreferencs() + { + return new Dictionary + { + { WellKnownPreference.ProtocolColor, "BoldGreen" }, + { WellKnownPreference.StatusColor, "BoldYellow" }, + + { WellKnownPreference.JsonArrayBraceColor, "BoldCyan" }, + { WellKnownPreference.JsonCommaColor, "BoldYellow" }, + { WellKnownPreference.JsonNameColor, "BoldMagenta" }, + { WellKnownPreference.JsonNameSeparatorColor, "BoldWhite" }, + { WellKnownPreference.JsonObjectBraceColor, "Cyan" }, + { WellKnownPreference.JsonColor, "Green" } + }; + } + + public bool SavePreferences() + { + List lines = new List(); + foreach (KeyValuePair entry in Preferences.OrderBy(x => x.Key)) + { + //If the value didn't exist in the defaults or the value's different, include it in the user's preferences file + if (!DefaultPreferences.TryGetValue(entry.Key, out string value) || !string.Equals(value, entry.Value, StringComparison.Ordinal)) + { + lines.Add($"{entry.Key}={entry.Value}"); + } + } + + try + { + File.WriteAllLines(PrefsFilePath, lines); + return true; + } + catch + { + return false; + } + } + + public string GetExampleBody(string path, string contentType, string method) + { + Uri effectivePath = GetEffectivePath(path); + string rootRelativePath = effectivePath.LocalPath.Substring(BaseAddress.LocalPath.Length).TrimStart('/'); + IDirectoryStructure structure = SwaggerStructure?.TraverseTo(rootRelativePath); + return structure?.RequestInfo?.GetRequestBodyForContentType(contentType, method); + } + + public IEnumerable GetApplicableContentTypes(string method, string path) + { + Uri effectivePath = GetEffectivePath(path); + string rootRelativePath = effectivePath.LocalPath.Substring(BaseAddress.LocalPath.Length).TrimStart('/'); + IDirectoryStructure structure = SwaggerStructure?.TraverseTo(rootRelativePath); + IReadOnlyDictionary> contentTypesByMethod = structure?.RequestInfo?.ContentTypesByMethod; + + if (contentTypesByMethod != null) + { + if (method is null) + { + return contentTypesByMethod.Values.SelectMany(x => x).Distinct(StringComparer.OrdinalIgnoreCase); + } + + if (contentTypesByMethod.TryGetValue(method, out IReadOnlyList contentTypes)) + { + return contentTypes; + } + } + + return null; + } + + public Uri GetEffectivePath(string commandSpecifiedPath) + { + if (Uri.TryCreate(commandSpecifiedPath, UriKind.Absolute, out Uri absoluteUri)) + { + return absoluteUri; + } + + UriBuilder builder = new UriBuilder(BaseAddress); + string path = string.Join('/', PathSections.Reverse()); + string[] parts = path.Split('?'); + string query = null; + string query2 = null; + + if (parts.Length > 1) + { + path = parts[0]; + query = string.Join('?', parts.Skip(1)); + } + + builder.Path += path; + + if (commandSpecifiedPath.Length > 0) + { + if (commandSpecifiedPath[0] != '/') + { + string argPath = commandSpecifiedPath; + if (builder.Path.Length > 0 && builder.Path[builder.Path.Length - 1] != '/') + { + argPath = "/" + argPath; + } + + int queryIndex = argPath.IndexOf('?'); + path = argPath; + + if (queryIndex > -1) + { + query2 = argPath.Substring(queryIndex + 1); + path = argPath.Substring(0, queryIndex); + } + + builder.Path += path; + } + else + { + int queryIndex = commandSpecifiedPath.IndexOf('?'); + path = commandSpecifiedPath; + + if (queryIndex > -1) + { + query2 = commandSpecifiedPath.Substring(queryIndex + 1); + path = commandSpecifiedPath.Substring(0, queryIndex); + } + + builder.Path = path; + } + } + else + { + + int queryIndex = commandSpecifiedPath.IndexOf('?'); + path = commandSpecifiedPath; + + if (queryIndex > -1) + { + query2 = commandSpecifiedPath.Substring(queryIndex + 1); + path = commandSpecifiedPath.Substring(0, queryIndex); + } + + builder.Path += path; + } + + if (query != null) + { + if (!string.IsNullOrEmpty(builder.Query)) + { + query = "&" + query; + } + + builder.Query += query; + } + + if (query2 != null) + { + if (!string.IsNullOrEmpty(builder.Query)) + { + query2 = "&" + query2; + } + + builder.Query += query2; + } + + return builder.Uri; + } + + public Uri GetEffectivePath(IReadOnlyList sections, bool requiresBody, out int filePathIndex) + { + filePathIndex = 1; + + if (BaseAddress == null) + { + return null; + } + + UriBuilder builder = new UriBuilder(BaseAddress); + string path = string.Join('/', PathSections.Reverse()); + string[] parts = path.Split('?'); + string query = null; + string query2 = null; + + if (parts.Length > 1) + { + path = parts[0]; + query = string.Join('?', parts.Skip(1)); + } + + builder.Path += path; + + if (sections.Count > 1) + { + if (!requiresBody || !File.Exists(sections[1])) + { + if (sections[1].Length > 0) + { + if (sections[1][0] != '/') + { + string argPath = sections[1]; + if (builder.Path.Length > 0 && builder.Path[builder.Path.Length - 1] != '/') + { + argPath = "/" + argPath; + } + + int queryIndex = argPath.IndexOf('?'); + path = argPath; + + if (queryIndex > -1) + { + query2 = argPath.Substring(queryIndex + 1); + path = argPath.Substring(0, queryIndex); + } + + builder.Path += path; + } + else + { + int queryIndex = sections[1].IndexOf('?'); + path = sections[1]; + + if (queryIndex > -1) + { + query2 = sections[1].Substring(queryIndex + 1); + path = sections[1].Substring(0, queryIndex); + } + + builder.Path = path; + } + } + else + { + + int queryIndex = sections[1].IndexOf('?'); + path = sections[1]; + + if (queryIndex > -1) + { + query2 = sections[1].Substring(queryIndex + 1); + path = sections[1].Substring(0, queryIndex); + } + + builder.Path += path; + } + + filePathIndex = 2; + } + } + + if (query != null) + { + if (!string.IsNullOrEmpty(builder.Query)) + { + query = "&" + query; + } + + builder.Query += query; + } + + if (query2 != null) + { + if (!string.IsNullOrEmpty(builder.Query)) + { + query2 = "&" + query2; + } + + builder.Query += query2; + } + + return builder.Uri; + } + } +} diff --git a/src/Microsoft.HttpRepl/IDirectoryStructure.cs b/src/Microsoft.HttpRepl/IDirectoryStructure.cs new file mode 100644 index 0000000000..5c02e8e476 --- /dev/null +++ b/src/Microsoft.HttpRepl/IDirectoryStructure.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Microsoft.HttpRepl +{ + public interface IDirectoryStructure + { + IEnumerable DirectoryNames { get; } + + IDirectoryStructure Parent { get; } + + IDirectoryStructure GetChildDirectory(string name); + + IRequestInfo RequestInfo { get; } + } + + public interface IRequestInfo + { + IReadOnlyDictionary> ContentTypesByMethod { get; } + + IReadOnlyList Methods { get; } + + string GetRequestBodyForContentType(string contentType, string method); + } +} diff --git a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj new file mode 100644 index 0000000000..cec7ac17d5 --- /dev/null +++ b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj @@ -0,0 +1,31 @@ + + + + Exe + netcoreapp2.2 + true + dotnet-httprepl + latest + Command line tool to for making HTTP calls and viewing their results. + dotnet;http;httprepl + + + true + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.HttpRepl/OpenApi/Either.cs b/src/Microsoft.HttpRepl/OpenApi/Either.cs new file mode 100644 index 0000000000..f1e7dfacb3 --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/Either.cs @@ -0,0 +1,33 @@ +namespace Microsoft.HttpRepl.OpenApi +{ + public class Either + { + public Either(TOption1 option1) + { + Option1 = option1; + IsOption1 = true; + } + + public Either(TOption2 option2) + { + Option2 = option2; + IsOption1 = false; + } + + public bool IsOption1 { get; } + + public TOption1 Option1 { get; } + + public TOption2 Option2 { get; } + + public static implicit operator Either(TOption1 value) + { + return new Either(value); + } + + public static implicit operator Either(TOption2 value) + { + return new Either(value); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs b/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs new file mode 100644 index 0000000000..b89a85c9ef --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs @@ -0,0 +1,32 @@ +using System; +using Newtonsoft.Json; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class EitherConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(TOption1).IsAssignableFrom(objectType) || typeof(TOption2).IsAssignableFrom(objectType) || typeof(EitherConverter) == objectType; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + try + { + TOption1 option1 = serializer.Deserialize(reader); + return new Either(option1); + } + catch + { + TOption2 option2 = serializer.Deserialize(reader); + return new Either(option2); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs new file mode 100644 index 0000000000..e46387b3e0 --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class EndpointMetadata + { + public EndpointMetadata(string path, IReadOnlyDictionary>> requestsByMethodAndContentType) + { + Path = path; + AvailableRequests = requestsByMethodAndContentType ?? new Dictionary>>(); + } + + public string Path { get; } + + public IReadOnlyDictionary>> AvailableRequests { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs new file mode 100644 index 0000000000..8635d6fbee --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class EndpointMetadataReader + { + private readonly List _readers = new List + { + new OpenApiV3EndpointMetadataReader(), + new SwaggerV2EndpointMetadataReader(), + new SwaggerV1EndpointMetadataReader() + }; + + public void RegisterReader(IEndpointMetadataReader reader) + { + _readers.Add(reader); + } + + public IEnumerable Read(JObject document) + { + foreach (IEndpointMetadataReader reader in _readers) + { + if (reader.CanHandle(document)) + { + IEnumerable result = reader.ReadMetadata(document); + + if (result != null) + { + return result; + } + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs new file mode 100644 index 0000000000..287740c7af --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public interface IEndpointMetadataReader + { + bool CanHandle(JObject document); + + IEnumerable ReadMetadata(JObject document); + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs new file mode 100644 index 0000000000..5fa968efc6 --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class OpenApiV3EndpointMetadataReader : IEndpointMetadataReader + { + public bool CanHandle(JObject document) + { + return (document["openapi"]?.ToString() ?? "").StartsWith("3.", StringComparison.Ordinal); + } + + public IEnumerable ReadMetadata(JObject document) + { + List metadata = new List(); + + if (document["paths"] is JObject paths) + { + foreach (JProperty path in paths.Properties()) + { + if (!(path.Value is JObject pathBody)) + { + continue; + } + + Dictionary>> requestMethods = new Dictionary>>(StringComparer.OrdinalIgnoreCase); + + foreach (JProperty method in pathBody.Properties()) + { + List parameters = new List(); + + if (method.Value is JObject methodBody) + { + if (methodBody["parameters"] is JArray parametersArray) + { + foreach (JObject parameterObj in parametersArray.OfType()) + { + Parameter p = parameterObj.ToObject(); + p.Location = parameterObj["in"]?.ToString(); + + if (!(parameterObj["schema"] is JObject schemaObject)) + { + schemaObject = null; + } + + p.Schema = schemaObject?.ToObject() ?? parameterObj.ToObject(); + parameters.Add(p); + } + } + + if (methodBody["requestBody"] is JObject bodyObject) + { + if (!(bodyObject["content"] is JObject contentTypeLookup)) + { + continue; + } + + foreach (JProperty contentTypeEntry in contentTypeLookup.Properties()) + { + List parametersByContentType = new List(parameters); + Parameter p = bodyObject.ToObject(); + p.Location = "body"; + p.IsRequired = bodyObject["required"]?.ToObject() ?? false; + + if (!(bodyObject["schema"] is JObject schemaObject)) + { + schemaObject = null; + } + + p.Schema = schemaObject?.ToObject() ?? bodyObject.ToObject(); + parametersByContentType.Add(p); + + Dictionary> bucketByMethod; + if (!requestMethods.TryGetValue(method.Name, out IReadOnlyDictionary> bucketByMethodRaw)) + { + requestMethods[method.Name] = bucketByMethodRaw = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "", parametersByContentType } + }; + } + + bucketByMethod = (Dictionary>)bucketByMethodRaw; + bucketByMethod[contentTypeEntry.Name] = parametersByContentType; + } + } + } + } + + metadata.Add(new EndpointMetadata(path.Name, requestMethods)); + } + } + + return metadata; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/Parameter.cs b/src/Microsoft.HttpRepl/OpenApi/Parameter.cs new file mode 100644 index 0000000000..faf21e5f9a --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/Parameter.cs @@ -0,0 +1,13 @@ +namespace Microsoft.HttpRepl.OpenApi +{ + public class Parameter + { + public string Name { get; set; } + + public string Location { get; set; } + + public bool IsRequired { get; set; } + + public Schema Schema { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs new file mode 100644 index 0000000000..b01730b116 --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs @@ -0,0 +1,218 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public static class PointerUtil + { + public static Task ResolvePointersAsync(Uri loadLocation, JToken root, HttpClient client) + { + return ResolvePointersAsync(loadLocation, root, root, client); + } + + private static async Task ResolvePointersAsync(Uri loadLocation, JToken root, JToken toResolve, HttpClient client) + { + if (toResolve is JArray arr) + { + for (int i = 0; i < arr.Count; ++i) + { + arr[i] = await ResolvePointersAsync(loadLocation, root, arr[i], client).ConfigureAwait(false); + } + } + else if (toResolve is JObject obj) + { + if (obj["$ref"] is JValue refVal && refVal.Type == JTokenType.String) + { + if (!Uri.TryCreate((string)refVal.Value, UriKind.RelativeOrAbsolute, out Uri loadTarget)) + { + //TODO: Error resolving pointer (pointer must be a valid URI) + return new JValue((object)null); + } + + if (!loadTarget.IsAbsoluteUri) + { + if (!Uri.TryCreate(loadLocation, loadTarget, out loadTarget)) + { + //TODO: Error resolving pointer (could not combine with base path) + return new JValue((object)null); + } + } + + //Check to see if we're changing source documents, if we are, get it + if (!string.Equals(loadLocation.Host, loadTarget.Host, StringComparison.OrdinalIgnoreCase) || !string.Equals(loadLocation.AbsolutePath, loadTarget.AbsolutePath, StringComparison.OrdinalIgnoreCase)) + { + HttpResponseMessage responseMessage = await client.GetAsync(loadTarget).ConfigureAwait(false); + + if (!responseMessage.IsSuccessStatusCode) + { + //TODO: Error resolving pointer (could not get referenced document) + return new JValue((object)null); + } + + string responseString = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); + JToken newRoot; + + try + { + newRoot = JToken.Parse(responseString); + } + catch + { + //TODO: Error resolving pointer (referenced document is not valid JSON) + return new JValue((object)null); + } + + return await ResolvePointersAsync(loadTarget, newRoot, newRoot, client).ConfigureAwait(false); + } + + //We're in the right document, grab the bookmark (fragment) of the URI and get the element at that path + string fragment = loadTarget.Fragment; + + if (fragment.StartsWith('#')) + { + fragment = fragment.Substring(1); + } + + string[] parts = fragment.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + JToken cursor = root; + + for (int i = 0; i < parts.Length; ++i) + { + if (cursor is JArray ca) + { + if (!int.TryParse(parts[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out int index)) + { + //TODO: Error resolving pointer, array index is non-integral + return new JValue((object)null); + } + + if (index < 0 || index >= ca.Count) + { + //TODO: Error resolving pointer, array index is out of bounds + return new JValue((object)null); + } + + JToken val = ca[index]; + if (val is JObject vo && vo.TryGetValue("$ref", out JToken vor) && vor is JValue vorv && vorv.Type == JTokenType.String) + { + cursor = await ResolvePointersAsync(loadLocation, root, val, client).ConfigureAwait(false); + } + else + { + cursor = val; + } + } + else if (cursor is JObject co) + { + if (!co.TryGetValue(parts[i], out JToken val)) + { + //TODO: Error resolving pointer, no such property on object + return new JValue((object)null); + } + + if (val is JObject vo && vo.TryGetValue("$ref", out JToken vor) && vor is JValue vorv && vorv.Type == JTokenType.String) + { + cursor = await ResolvePointersAsync(loadLocation, root, val, client).ConfigureAwait(false); + } + else + { + cursor = val; + } + } + else + { + //TODO: Error resolving pointer, cannot index into literal + return new JValue((object)null); + } + } + + cursor = await ResolvePointersAsync(loadLocation, root, cursor, client); + return cursor.DeepClone(); + } + + foreach (JProperty property in obj.Properties().ToList()) + { + obj[property.Name] = await ResolvePointersAsync(loadLocation, root, property.Value, client).ConfigureAwait(false); + } + } + + return toResolve; + } + + //public static async Task 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; + //} + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/Schema.cs b/src/Microsoft.HttpRepl/OpenApi/Schema.cs new file mode 100644 index 0000000000..028c0b13ca --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/Schema.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class Schema + { + public void PrepareForUsage(JToken document) + { + AdditionalProperties?.Option1?.PrepareForUsage(document); + + if (AllOf != null) + { + for (int i = 0; i < AllOf.Length; ++i) + { + AllOf[i].PrepareForUsage(document); + } + } + + if (AnyOf != null) + { + for (int i = 0; i < AnyOf.Length; ++i) + { + AnyOf[i].PrepareForUsage(document); + } + } + + if (OneOf != null) + { + for (int i = 0; i < OneOf.Length; ++i) + { + OneOf[i].PrepareForUsage(document); + } + } + + if (Properties != null) + { + IReadOnlyList keys = Properties.Keys.ToList(); + for (int i = 0; i < keys.Count; ++i) + { + Properties[keys[i]]?.PrepareForUsage(document); + } + } + + Items?.PrepareForUsage(document); + Not?.PrepareForUsage(document); + + if (Required?.Option1 != null) + { + if (Properties != null) + { + foreach (string propertyName in Required.Option1) + { + if (Properties.TryGetValue(propertyName, out Schema value)) + { + value.Required = true; + } + } + } + + Required = false; + } + } + + [JsonConverter(typeof(EitherConverter))] + public Either AdditionalProperties { get; set; } + + public Schema[] AllOf { get; set; } + + public Schema[] AnyOf { get; set; } + + public object Default { get; set; } + + public string Description { get; set; } + + public object[] Enum { get; set; } + + public object Example { get; set; } + + public bool ExclusiveMaximum { get; set; } + + public bool ExclusiveMinimum { get; set; } + + public string Format { get; set; } + + public Schema Items { get; set; } + + public double? Maximum { get; set; } + + public double? Minimum { get; set; } + + public int? MaxItems { get; set; } + + public int? MinItems { get; set; } + + public int? MaxLength { get; set; } + + public int? MinLength { get; set; } + + public int? MaxProperties { get; set; } + + public int? MinProperties { get; set; } + + public double? MultipleOf { get; set; } + + public Schema Not { get; set; } + + public Schema[] OneOf { get; set; } + + public string Pattern { get; set; } + + public Dictionary Properties { get; set; } + + [JsonConverter(typeof(EitherConverter))] + public Either Required { get; set; } + + public string Title { get; set; } + + public string Type { get; set; } + + public bool UniqueItems { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs new file mode 100644 index 0000000000..7f4fef4fb1 --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class SwaggerV1EndpointMetadataReader : IEndpointMetadataReader + { + public bool CanHandle(JObject document) + { + return (document["swaggerVersion"]?.ToString() ?? "").StartsWith("1.", StringComparison.Ordinal); + } + + public IEnumerable ReadMetadata(JObject document) + { + List metadata = new List(); + + if (!(document["consumes"] is JArray globalConsumes)) + { + globalConsumes = new JArray(); + } + + if (document["apis"] is JObject obj) + { + foreach (JProperty property in obj.Properties()) + { + string path = obj["path"]?.ToString(); + + if (path is null) + { + continue; + } + + Dictionary>> requestMethods = new Dictionary>>(StringComparer.Ordinal); + + if (obj["operations"] is JArray operations) + { + foreach (JObject operationObject in operations.OfType()) + { + string method = operationObject["method"]?.ToString(); + List parameters = new List(); + + if (operationObject["parameters"] is JArray parametersArray) + { + foreach (JObject parameterObj in parametersArray.OfType()) + { + Parameter p = parameterObj.ToObject(); + p.Location = parameterObj["paramType"]?.ToString(); + p.IsRequired = parameterObj["required"]?.ToObject() ?? false; + + string type = parameterObj["type"]?.ToString(); + + if (type is null) + { + continue; + } + + switch (type.ToUpperInvariant()) + { + case "INTEGER": + case "NUMBER": + case "STRING": + case "BOOLEAN": + p.Schema = new Schema { Type = type }; + break; + case "FILE": + break; + default: + if (document["models"]?[type] is JObject schemaObject) + { + //TODO: Handle subtypes (https://github.com/OAI/OpenAPI-Specification/blob/master/versions/1.2.md#527-model-object) + p.Schema = schemaObject.ToObject(); + } + break; + } + + parameters.Add(p); + } + } + + if (!(operationObject["consumes"] is JArray consumes)) + { + consumes = globalConsumes; + } + + Dictionary> parametersByContentType = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "", parameters } + }; + + foreach (JValue value in consumes.OfType().Where(x => x.Type == JTokenType.String)) + { + parametersByContentType[value.ToString()] = parameters; + } + } + } + + metadata.Add(new EndpointMetadata(path, requestMethods)); + } + } + + return metadata; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs new file mode 100644 index 0000000000..5e7ecfe6aa --- /dev/null +++ b/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace Microsoft.HttpRepl.OpenApi +{ + public class SwaggerV2EndpointMetadataReader : IEndpointMetadataReader + { + public bool CanHandle(JObject document) + { + return (document["swagger"]?.ToString() ?? "").StartsWith("2.", StringComparison.Ordinal); + } + + public IEnumerable ReadMetadata(JObject document) + { + List metadata = new List(); + + if (!(document["consumes"] is JArray globalConsumes)) + { + globalConsumes = new JArray(); + } + + if (document["paths"] is JObject obj) + { + foreach (JProperty property in obj.Properties()) + { + if (!(property.Value is JObject requestMethodInfos)) + { + continue; + } + + Dictionary>> requestMethods = new Dictionary>>(StringComparer.Ordinal); + + foreach (JProperty methodInfo in requestMethodInfos.Properties()) + { + List parameters = new List(); + + if (methodInfo.Value is JObject methodInfoDescription) + { + if (methodInfoDescription["parameters"] is JArray parametersArray) + { + foreach (JObject parameterObj in parametersArray.OfType()) + { + //TODO: Resolve refs here + + Parameter p = parameterObj.ToObject(); + p.Location = parameterObj["in"]?.ToString(); + p.IsRequired = parameterObj["required"]?.ToObject() ?? false; + + if (!(parameterObj["schema"] is JObject schemaObject)) + { + schemaObject = null; + } + + p.Schema = schemaObject?.ToObject() ?? parameterObj.ToObject(); + parameters.Add(p); + } + } + + if (!(methodInfoDescription["consumes"] is JArray consumes)) + { + consumes = globalConsumes; + } + + Dictionary> parametersByContentType = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "", parameters } + }; + + foreach (JValue value in consumes.OfType().Where(x => x.Type == JTokenType.String)) + { + parametersByContentType[value.ToString()] = parameters; + } + + requestMethods[methodInfo.Name] = parametersByContentType; + } + } + + metadata.Add(new EndpointMetadata(property.Name, requestMethods)); + } + } + + return metadata; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs b/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs new file mode 100644 index 0000000000..21918e44ef --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs @@ -0,0 +1,29 @@ +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Preferences +{ + public interface IJsonConfig + { + int IndentSize { get; } + + AllowedColors DefaultColor { get; } + + AllowedColors ArrayBraceColor { get; } + + AllowedColors ObjectBraceColor { get; } + + AllowedColors CommaColor { get; } + + AllowedColors NameColor { get; } + + AllowedColors NameSeparatorColor { get; } + + AllowedColors BoolColor { get; } + + AllowedColors NumericColor { get; } + + AllowedColors StringColor { get; } + + AllowedColors NullColor { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs b/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs new file mode 100644 index 0000000000..a1902e245e --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs @@ -0,0 +1,42 @@ +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Preferences +{ + public class JsonConfig : IJsonConfig + { + private readonly HttpState _state; + + public int IndentSize => _state.GetIntPreference(WellKnownPreference.JsonIndentSize, 2); + + public AllowedColors DefaultColor => _state.GetColorPreference(WellKnownPreference.JsonColor); + + private AllowedColors DefaultBraceColor => _state.GetColorPreference(WellKnownPreference.JsonBraceColor, DefaultSyntaxColor); + + private AllowedColors DefaultSyntaxColor => _state.GetColorPreference(WellKnownPreference.JsonSyntaxColor, DefaultColor); + + private AllowedColors DefaultLiteralColor => _state.GetColorPreference(WellKnownPreference.JsonLiteralColor, DefaultColor); + + public AllowedColors ArrayBraceColor => _state.GetColorPreference(WellKnownPreference.JsonArrayBraceColor, DefaultBraceColor); + + public AllowedColors ObjectBraceColor => _state.GetColorPreference(WellKnownPreference.JsonObjectBraceColor, DefaultBraceColor); + + public AllowedColors CommaColor => _state.GetColorPreference(WellKnownPreference.JsonCommaColor, DefaultSyntaxColor); + + public AllowedColors NameColor => _state.GetColorPreference(WellKnownPreference.JsonNameColor, StringColor); + + public AllowedColors NameSeparatorColor => _state.GetColorPreference(WellKnownPreference.JsonNameSeparatorColor, DefaultSyntaxColor); + + public AllowedColors BoolColor => _state.GetColorPreference(WellKnownPreference.JsonBoolColor, DefaultLiteralColor); + + public AllowedColors NumericColor => _state.GetColorPreference(WellKnownPreference.JsonNumericColor, DefaultLiteralColor); + + public AllowedColors StringColor => _state.GetColorPreference(WellKnownPreference.JsonStringColor, DefaultLiteralColor); + + public AllowedColors NullColor => _state.GetColorPreference(WellKnownPreference.JsonNullColor, DefaultLiteralColor); + + public JsonConfig(HttpState state) + { + _state = state; + } + } +} diff --git a/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs b/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs new file mode 100644 index 0000000000..a3f9b55259 --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs @@ -0,0 +1,46 @@ +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Preferences +{ + public class RequestConfig : RequestOrResponseConfig + { + public RequestConfig(HttpState state) + : base(state) + { + } + + public override AllowedColors BodyColor => State.GetColorPreference(WellKnownPreference.RequestBodyColor, base.BodyColor); + + public override AllowedColors SchemeColor => State.GetColorPreference(WellKnownPreference.RequestSchemeColor, base.SchemeColor); + + public override AllowedColors HeaderKeyColor => State.GetColorPreference(WellKnownPreference.RequestHeaderKeyColor, base.HeaderKeyColor); + + public override AllowedColors HeaderSeparatorColor => State.GetColorPreference(WellKnownPreference.RequestHeaderSeparatorColor, base.HeaderSeparatorColor); + + public override AllowedColors HeaderValueSeparatorColor => State.GetColorPreference(WellKnownPreference.RequestHeaderValueSeparatorColor, base.HeaderValueSeparatorColor); + + public override AllowedColors HeaderValueColor => State.GetColorPreference(WellKnownPreference.RequestHeaderValueColor, base.HeaderValueColor); + + public override AllowedColors HeaderColor => State.GetColorPreference(WellKnownPreference.RequestHeaderColor, base.HeaderColor); + + public override AllowedColors GeneralColor => State.GetColorPreference(WellKnownPreference.RequestColor, base.GeneralColor); + + public override AllowedColors ProtocolColor => State.GetColorPreference(WellKnownPreference.RequestProtocolColor, base.ProtocolColor); + + public override AllowedColors ProtocolNameColor => State.GetColorPreference(WellKnownPreference.RequestProtocolNameColor, base.ProtocolNameColor); + + public override AllowedColors ProtocolVersionColor => State.GetColorPreference(WellKnownPreference.RequestProtocolVersionColor, base.ProtocolVersionColor); + + public override AllowedColors ProtocolSeparatorColor => State.GetColorPreference(WellKnownPreference.RequestProtocolSeparatorColor, base.ProtocolSeparatorColor); + + public override AllowedColors StatusColor => State.GetColorPreference(WellKnownPreference.RequestStatusColor, base.StatusColor); + + public override AllowedColors StatusCodeColor => State.GetColorPreference(WellKnownPreference.RequestStatusCodeColor, base.StatusCodeColor); + + public override AllowedColors StatusReasonPhraseColor => State.GetColorPreference(WellKnownPreference.RequestStatusReaseonPhraseColor, base.StatusReasonPhraseColor); + + public AllowedColors MethodColor => State.GetColorPreference(WellKnownPreference.RequestMethodColor, GeneralColor); + + public AllowedColors AddressColor => State.GetColorPreference(WellKnownPreference.RequestAddressColor, GeneralColor); + } +} diff --git a/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs b/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs new file mode 100644 index 0000000000..c509a89f5e --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs @@ -0,0 +1,44 @@ +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Preferences +{ + public abstract class RequestOrResponseConfig + { + protected HttpState State { get; } + + protected RequestOrResponseConfig(HttpState state) + { + State = state; + } + + public virtual AllowedColors BodyColor => State.GetColorPreference(WellKnownPreference.BodyColor, GeneralColor); + + public virtual AllowedColors SchemeColor => State.GetColorPreference(WellKnownPreference.SchemeColor, GeneralColor); + + public virtual AllowedColors HeaderKeyColor => State.GetColorPreference(WellKnownPreference.HeaderKeyColor, HeaderColor); + + public virtual AllowedColors HeaderSeparatorColor => State.GetColorPreference(WellKnownPreference.HeaderSeparatorColor, HeaderColor); + + public virtual AllowedColors HeaderValueSeparatorColor => State.GetColorPreference(WellKnownPreference.HeaderValueSeparatorColor, HeaderSeparatorColor); + + public virtual AllowedColors HeaderValueColor => State.GetColorPreference(WellKnownPreference.HeaderValueColor, HeaderColor); + + public virtual AllowedColors HeaderColor => State.GetColorPreference(WellKnownPreference.HeaderColor, GeneralColor); + + public virtual AllowedColors GeneralColor => State.GetColorPreference(WellKnownPreference.RequestOrResponseColor); + + public virtual AllowedColors ProtocolColor => State.GetColorPreference(WellKnownPreference.ProtocolColor, GeneralColor); + + public virtual AllowedColors ProtocolNameColor => State.GetColorPreference(WellKnownPreference.ProtocolNameColor, ProtocolColor); + + public virtual AllowedColors ProtocolVersionColor => State.GetColorPreference(WellKnownPreference.ProtocolVersionColor, ProtocolColor); + + public virtual AllowedColors ProtocolSeparatorColor => State.GetColorPreference(WellKnownPreference.ProtocolSeparatorColor, ProtocolColor); + + public virtual AllowedColors StatusColor => State.GetColorPreference(WellKnownPreference.StatusColor, GeneralColor); + + public virtual AllowedColors StatusCodeColor => State.GetColorPreference(WellKnownPreference.StatusCodeColor, StatusColor); + + public virtual AllowedColors StatusReasonPhraseColor => State.GetColorPreference(WellKnownPreference.StatusReaseonPhraseColor, StatusColor); + } +} diff --git a/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs b/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs new file mode 100644 index 0000000000..123a86ff79 --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs @@ -0,0 +1,42 @@ +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Preferences +{ + public class ResponseConfig : RequestOrResponseConfig + { + public ResponseConfig(HttpState state) + : base(state) + { + } + + public override AllowedColors BodyColor => State.GetColorPreference(WellKnownPreference.ResponseBodyColor, base.BodyColor); + + public override AllowedColors SchemeColor => State.GetColorPreference(WellKnownPreference.ResponseSchemeColor, base.SchemeColor); + + public override AllowedColors HeaderKeyColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderKeyColor, base.HeaderKeyColor); + + public override AllowedColors HeaderSeparatorColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderSeparatorColor, base.HeaderSeparatorColor); + + public override AllowedColors HeaderValueSeparatorColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderValueSeparatorColor, base.HeaderValueSeparatorColor); + + public override AllowedColors HeaderValueColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderValueColor, base.HeaderValueColor); + + public override AllowedColors HeaderColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderColor, base.HeaderColor); + + public override AllowedColors GeneralColor => State.GetColorPreference(WellKnownPreference.ResponseColor, base.GeneralColor); + + public override AllowedColors ProtocolColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolColor, base.ProtocolColor); + + public override AllowedColors ProtocolNameColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolNameColor, base.ProtocolNameColor); + + public override AllowedColors ProtocolVersionColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolVersionColor, base.ProtocolVersionColor); + + public override AllowedColors ProtocolSeparatorColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolSeparatorColor, base.ProtocolSeparatorColor); + + public override AllowedColors StatusColor => State.GetColorPreference(WellKnownPreference.ResponseStatusColor, base.StatusColor); + + public override AllowedColors StatusCodeColor => State.GetColorPreference(WellKnownPreference.ResponseStatusCodeColor, base.StatusCodeColor); + + public override AllowedColors StatusReasonPhraseColor => State.GetColorPreference(WellKnownPreference.ResponseStatusReaseonPhraseColor, base.StatusReasonPhraseColor); + } +} diff --git a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs new file mode 100644 index 0000000000..a6c16a6da7 --- /dev/null +++ b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Repl.ConsoleHandling; + +namespace Microsoft.HttpRepl.Preferences +{ + public static class WellKnownPreference + { + public static class Catalog + { + private static IReadOnlyList _names; + + public static IReadOnlyList Names + { + get + { + if (_names != null) + { + return _names; + } + + List matchingProperties = new List(); + + foreach (PropertyInfo property in typeof(WellKnownPreference).GetProperties(BindingFlags.Public | BindingFlags.Static)) + { + if (property.PropertyType == typeof(string) && property.GetMethod != null && property.GetValue(null) is string val) + { + matchingProperties.Add(val); + } + } + + return _names = matchingProperties; + } + } + } + + #region JSON + public static string JsonArrayBraceColor { get; } = "colors.json.arrayBrace"; + + public static string JsonObjectBraceColor { get; } = "colors.json.objectBrace"; + + public static string JsonNameColor { get; } = "colors.json.name"; + + public static string JsonNameSeparatorColor { get; } = "colors.json.nameSeparator"; + + public static string JsonIndentSize { get; } = "formatting.json.indentSize"; + + public static string JsonCommaColor { get; } = "colors.json.comma"; + + public static string JsonLiteralColor { get; } = "colors.json.literal"; + + public static string JsonNullColor { get; } = "colors.json.null"; + + public static string JsonBoolColor { get; } = "colors.json.bool"; + + public static string JsonNumericColor { get; } = "colors.json.numeric"; + + public static string JsonStringColor { get; } = "colors.json.string"; + + public static string JsonColor { get; } = "colors.json"; + + 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"; + + public static string RequestBodyColor { get; } = "colors.request.body"; + + public static string RequestSchemeColor { get; } = "colors.request.scheme"; + + public static string RequestHeaderKeyColor { get; } = "colors.request.header.key"; + + public static string RequestHeaderSeparatorColor { get; } = "colors.request.header.separator"; + + public static string RequestHeaderValueSeparatorColor { get; } = "colors.request.header.valueSeparator"; + + public static string RequestHeaderValueColor { get; } = "colors.request.header.value"; + + public static string RequestHeaderColor { get; } = "colors.request.header"; + + public static string RequestProtocolColor { get; } = "colors.request.protocol"; + + public static string RequestProtocolNameColor { get; } = "colors.request.protocol.name"; + + public static string RequestProtocolSeparatorColor { get; } = "colors.request.protocol.separator"; + + public static string RequestProtocolVersionColor { get; } = "colors.request.protocol.version"; + + public static string RequestStatusColor { get; } = "colors.request.status"; + + public static string RequestStatusCodeColor { get; } = "colors.request.status.code"; + + public static string RequestStatusReaseonPhraseColor { get; } = "colors.request.status.reasonPhrase"; + + public static string RequestMethodColor { get; } = "colors.request.method"; + + public static string RequestAddressColor { get; } = "colors.request.address"; + + + public static string ResponseColor { get; } = "colors.response"; + + public static string ResponseBodyColor { get; } = "colors.response.body"; + + public static string ResponseSchemeColor { get; } = "colors.response.scheme"; + + public static string ResponseHeaderKeyColor { get; } = "colors.response.header.key"; + + public static string ResponseHeaderSeparatorColor { get; } = "colors.response.header.separator"; + + public static string ResponseHeaderValueSeparatorColor { get; } = "colors.response.header.valueSeparator"; + + public static string ResponseHeaderValueColor { get; } = "colors.response.header.value"; + + public static string ResponseHeaderColor { get; } = "colors.response.header"; + + public static string ResponseProtocolColor { get; } = "colors.response.protocol"; + + public static string ResponseProtocolNameColor { get; } = "colors.response.protocol.name"; + + public static string ResponseProtocolSeparatorColor { get; } = "colors.response.protocol.separator"; + + public static string ResponseProtocolVersionColor { get; } = "colors.response.protocol.version"; + + public static string ResponseStatusColor { get; } = "colors.response.status"; + + public static string ResponseStatusCodeColor { get; } = "colors.response.status.code"; + + public static string ResponseStatusReaseonPhraseColor { get; } = "colors.response.status.reasonPhrase"; + + + public static string RequestOrResponseColor { get; } = "colors.requestOrResponse"; + + public static string BodyColor { get; } = "colors.body"; + + public static string SchemeColor { get; } = "colors.scheme"; + + public static string HeaderKeyColor { get; } = "colors.header.key"; + + public static string HeaderSeparatorColor { get; } = "colors.header.separator"; + + public static string HeaderValueSeparatorColor { get; } = "colors.header.valueSeparator"; + + public static string HeaderValueColor { get; } = "colors.header.value"; + + public static string HeaderColor { get; } = "colors.header"; + + public static string ProtocolColor { get; } = "colors.protocol"; + + public static string ProtocolNameColor { get; } = "colors.protocol.name"; + + public static string ProtocolSeparatorColor { get; } = "colors.protocol.separator"; + + public static string ProtocolVersionColor { get; } = "colors.protocol.version"; + + public static string StatusColor { get; } = "colors.status"; + + public static string StatusCodeColor { get; } = "colors.status.code"; + + public static string StatusReaseonPhraseColor { get; } = "colors.status.reasonPhrase"; + + + public static string DefaultEditorCommand { get; } = "editor.command.default"; + + public static string DefaultEditorArguments { get; } = "editor.command.default.arguments"; + + + public static AllowedColors GetColorPreference(this HttpState programState, string preference, AllowedColors defaultvalue = AllowedColors.None) + { + if (!programState.Preferences.TryGetValue(preference, out string preferenceValueString) || !Enum.TryParse(preferenceValueString, true, out AllowedColors result)) + { + result = defaultvalue; + } + + return result; + } + + public static int GetIntPreference(this HttpState programState, string preference, int defaultValue = 0) + { + if (!programState.Preferences.TryGetValue(preference, out string preferenceValueString) || !int.TryParse(preferenceValueString, out int result)) + { + result = defaultValue; + } + + return result; + } + + public static string GetStringPreference(this HttpState programState, string preference, string defaultValue = null) + { + if (!programState.Preferences.TryGetValue(preference, out string result)) + { + result = defaultValue; + } + + return result; + } + } +} diff --git a/src/Microsoft.HttpRepl/Program.cs b/src/Microsoft.HttpRepl/Program.cs new file mode 100644 index 0000000000..f64fcf377a --- /dev/null +++ b/src/Microsoft.HttpRepl/Program.cs @@ -0,0 +1,52 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; +using Microsoft.HttpRepl.Commands; + +namespace Microsoft.HttpRepl +{ + class Program + { + static async Task Main(string[] args) + { + var state = new HttpState(); + var dispatcher = DefaultCommandDispatcher.Create(state.GetPrompt, state); + + dispatcher.AddCommand(new ChangeDirectoryCommand()); + dispatcher.AddCommand(new ClearCommand()); + //dispatcher.AddCommand(new ConfigCommand()); + dispatcher.AddCommand(new DeleteCommand()); + dispatcher.AddCommand(new EchoCommand()); + dispatcher.AddCommand(new ExitCommand()); + dispatcher.AddCommand(new HeadCommand()); + dispatcher.AddCommand(new HelpCommand()); + dispatcher.AddCommand(new GetCommand()); + dispatcher.AddCommand(new ListCommand()); + dispatcher.AddCommand(new OptionsCommand()); + dispatcher.AddCommand(new PatchCommand()); + dispatcher.AddCommand(new PrefCommand()); + dispatcher.AddCommand(new PostCommand()); + dispatcher.AddCommand(new PutCommand()); + dispatcher.AddCommand(new RunCommand()); + dispatcher.AddCommand(new SetBaseCommand()); + dispatcher.AddCommand(new SetDiagCommand()); + dispatcher.AddCommand(new SetHeaderCommand()); + dispatcher.AddCommand(new SetSwaggerCommand()); + dispatcher.AddCommand(new UICommand()); + + CancellationTokenSource source = new CancellationTokenSource(); + var shell = new Shell(dispatcher); + shell.ShellState.ConsoleManager.AddBreakHandler(() => source.Cancel()); + if (args.Length > 0) + { + 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); + } + Task result = shell.RunAsync(source.Token); + await result.ConfigureAwait(false); + } + } +} diff --git a/src/Microsoft.HttpRepl/Properties/launchSettings.json b/src/Microsoft.HttpRepl/Properties/launchSettings.json new file mode 100644 index 0000000000..dcce8f72a3 --- /dev/null +++ b/src/Microsoft.HttpRepl/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Microsoft.HttpRepl": { + "commandName": "Project", + "commandLineArgs": "http://localhost" + } + } +} \ No newline at end of file diff --git a/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs b/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs new file mode 100644 index 0000000000..05372d7bec --- /dev/null +++ b/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.HttpRepl.Suggestions +{ + public class HeaderCompletion + { + private static readonly IEnumerable CommonHeaders = new[] + { + "A-IM", + "Accept", + "Accept-Charset", + "Accept-Encoding", + "Accept-Language", + "Accept-Datetime", + "Access-Control-Request-Method", + "Access-Control-Request-Headers", + "Authorization", + "Cache-Control", + "Connection", + "Content-Length", + "Content-MD5", + "Content-Type", + "Cookie", + "Date", + "Expect", + "Forwarded", + "From", + "Host", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Unmodified-Since", + "Max-Forwards", + "Origin", + "Pragma", + "Proxy-Authentication", + "Range", + "Referer", + "TE", + "User-Agent", + "Upgrade", + "Via", + "Warning", + //Non-standard + "Upgrade-Insecure-Requests", + "X-Requested-With", + "DNT", + "X-Forwarded-For", + "X-Forwarded-Host", + "X-Forwarded-Proto", + "Front-End-Https", + "X-Http-Method-Override", + "X-ATT-DeviceId", + "X-Wap-Profile", + "Proxy-Connection", + "X-UIDH", + "X-Csrf-Token", + "X-Request-ID", + "X-Correlation-ID" + }; + + private static readonly IReadOnlyList DefaultContentTypesList = null; + + public static IEnumerable GetCompletions(IReadOnlyCollection existingHeaders, string prefix) + { + List result = CommonHeaders.Where(x => x.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && !(existingHeaders?.Contains(x) ?? false)).ToList(); + + if (result.Count > 0) + { + return result; + } + + return null; + } + + public static IEnumerable GetValueCompletions(string method, string path, string header, string prefix, HttpState programState) + { + switch (header.ToUpperInvariant()) + { + case "CONTENT-TYPE": + IEnumerable results = programState.GetApplicableContentTypes(method, path) ?? DefaultContentTypesList; + + if (results is null) + { + return null; + } + + return results.Where(x => !string.IsNullOrEmpty(x) && x.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)); + default: + return null; + } + } + } +} diff --git a/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs b/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs new file mode 100644 index 0000000000..a50df8ae8d --- /dev/null +++ b/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.HttpRepl.Suggestions +{ + public static class ServerPathCompletion + { + public static IEnumerable GetCompletions(HttpState programState, string normalCompletionString) + { + + //If it's an absolute URI, nothing to suggest + if (Uri.TryCreate(normalCompletionString, UriKind.Absolute, out Uri _)) + { + return null; + } + + string path = normalCompletionString.Replace('\\', '/'); + int searchFrom = normalCompletionString.Length - 1; + int lastSlash = path.LastIndexOf('/', searchFrom); + string prefix; + + if (lastSlash < 0) + { + path = string.Empty; + prefix = normalCompletionString; + } + else + { + path = path.Substring(0, lastSlash + 1); + prefix = normalCompletionString.Substring(lastSlash + 1); + } + + IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path); + + if (s?.DirectoryNames == null) + { + return null; + } + + List results = new List(); + + foreach (string child in s.DirectoryNames) + { + if (child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + results.Add(path + child); + } + } + + return results; + } + } +} diff --git a/src/Microsoft.Repl/Commanding/CommandHistory.cs b/src/Microsoft.Repl/Commanding/CommandHistory.cs new file mode 100644 index 0000000000..83c7206a9d --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandHistory.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Repl.Commanding +{ + public class CommandHistory : ICommandHistory + { + private readonly int _maxEntries; + private readonly List _commandLines = new List(); + private int _currentCommand = -1; + private int _suspensionDepth; + + public CommandHistory(int maxEntries = 50) + { + _maxEntries = maxEntries; + } + + public void AddCommand(string command) + { + if (_suspensionDepth > 0) + { + return; + } + + _commandLines.Add(command); + if (_commandLines.Count > _maxEntries) + { + _commandLines.RemoveAt(0); + } + _currentCommand = -1; + } + + public string GetNextCommand() + { + if (_commandLines.Count == 0) + { + return string.Empty; + } + + if (_currentCommand == -1 || _currentCommand >= _commandLines.Count - 1) + { + _currentCommand = -1; + return string.Empty; + } + + return _commandLines[++_currentCommand]; + } + + public string GetPreviousCommand() + { + if (_commandLines.Count == 0) + { + return string.Empty; + } + + if (_currentCommand == -1) + { + _currentCommand = _commandLines.Count; + } + + if (_currentCommand > 0) + { + return _commandLines[--_currentCommand]; + } + + return _commandLines[0]; + } + + public IDisposable SuspendHistory() + { + ++_suspensionDepth; + return new Disposable(() => --_suspensionDepth); + } + } +} diff --git a/src/Microsoft.Repl/Commanding/CommandInputLocation.cs b/src/Microsoft.Repl/Commanding/CommandInputLocation.cs new file mode 100644 index 0000000000..1612384b61 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandInputLocation.cs @@ -0,0 +1,10 @@ +namespace Microsoft.Repl.Commanding +{ + public enum CommandInputLocation + { + CommandName, + Argument, + OptionName, + OptionValue + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs new file mode 100644 index 0000000000..501b4357da --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs @@ -0,0 +1,15 @@ +namespace Microsoft.Repl.Commanding +{ + public class CommandInputProcessingIssue + { + public CommandInputProcessingIssueKind Kind { get; } + + public string Text { get; } + + public CommandInputProcessingIssue(CommandInputProcessingIssueKind kind, string text) + { + Kind = kind; + Text = text; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs new file mode 100644 index 0000000000..cd69dccb85 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Repl.Commanding +{ + public enum CommandInputProcessingIssueKind + { + CommandMismatch, + ArgumentCountOutOfRange, + UnknownOption, + OptionUseCountOutOfRange, + MissingRequiredOptionInput, + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs b/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs new file mode 100644 index 0000000000..370b9f3d34 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Microsoft.Repl.Commanding +{ + public class CommandInputSpecification + { + public IReadOnlyList CommandName { get; } + + public char OptionPreamble { get; } + + public int MinimumArguments { get; } + + public int MaximumArguments { get; } + + public IReadOnlyList Options { get; } + + public CommandInputSpecification(IReadOnlyList name, char optionPreamble, IReadOnlyList options, int minimumArgs, int maximumArgs) + { + CommandName = name; + OptionPreamble = optionPreamble; + MinimumArguments = minimumArgs; + MaximumArguments = maximumArgs; + + if (MinimumArguments < 0) + { + MinimumArguments = 0; + } + + if (MaximumArguments < MinimumArguments) + { + MaximumArguments = MinimumArguments; + } + + Options = options; + } + + public static CommandInputSpecificationBuilder Create(string baseName, params string[] additionalNameParts) + { + List nameParts = new List {baseName}; + nameParts.AddRange(additionalNameParts); + return new CommandInputSpecificationBuilder(nameParts); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs b/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs new file mode 100644 index 0000000000..5ad5edf345 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; + +namespace Microsoft.Repl.Commanding +{ + public class CommandInputSpecificationBuilder + { + private readonly IReadOnlyList _name; + private char _optionPreamble; + private int _minimumArgs; + private int _maximumArgs; + private readonly List _options = new List(); + + public CommandInputSpecificationBuilder(IReadOnlyList name) + { + _name = name; + _optionPreamble = '-'; + } + + public CommandInputSpecificationBuilder WithOptionPreamble(char optionChar) + { + _optionPreamble = optionChar; + return this; + } + + public CommandInputSpecificationBuilder ExactArgCount(int count) + { + _minimumArgs = count; + _maximumArgs = count; + return this; + } + + public CommandInputSpecificationBuilder MinimumArgCount(int count) + { + _minimumArgs = count; + if (_maximumArgs < count) + { + _maximumArgs = count; + } + + return this; + } + + public CommandInputSpecificationBuilder MaximumArgCount(int count) + { + _maximumArgs = count; + + if (_minimumArgs > count) + { + _minimumArgs = count; + } + + return this; + } + + public CommandInputSpecificationBuilder WithOption(CommandOptionSpecification option) + { + _options.Add(option); + return this; + } + + public CommandInputSpecification Finish() + { + return new CommandInputSpecification(_name, _optionPreamble, _options, _minimumArgs, _maximumArgs); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs b/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs new file mode 100644 index 0000000000..d925e5b2c9 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Microsoft.Repl.Commanding +{ + public class CommandOptionSpecification + { + public string Id { get; } + + public IReadOnlyList Forms { get; } + + public int MaximumOccurrences { get; } + + public int MinimumOccurrences { get; } + + public bool AcceptsValue { get; } + + public bool RequiresValue { get; } + + public CommandOptionSpecification(string id, bool acceptsValue = false, bool requiresValue = false, int minimumOccurrences = 0, int maximumOccurrences = int.MaxValue, params string[] forms) + { + Id = id; + Forms = forms; + MinimumOccurrences = minimumOccurrences; + MaximumOccurrences = maximumOccurrences > minimumOccurrences ? maximumOccurrences : minimumOccurrences; + RequiresValue = requiresValue; + AcceptsValue = RequiresValue || acceptsValue; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs b/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs new file mode 100644 index 0000000000..38443486e8 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Commanding +{ + public abstract class CommandWithStructuredInputBase : ICommand + where TParseResult : ICoreParseResult + { + public abstract string GetHelpSummary(IShellState shellState, TProgramState programState); + + public string GetHelpDetails(IShellState shellState, TProgramState programState, TParseResult parseResult) + { + if (!DefaultCommandInput.TryProcess(InputSpec, parseResult, out DefaultCommandInput commandInput, out IReadOnlyList processingIssues) + && processingIssues.Any(x => x.Kind == CommandInputProcessingIssueKind.CommandMismatch)) + { + //If this is the right command, just not the right syntax, report the usage errors + return null; + } + + return GetHelpDetails(shellState, programState, commandInput, parseResult); + } + + protected abstract string GetHelpDetails(IShellState shellState, TProgramState programState, DefaultCommandInput commandInput, TParseResult parseResult); + + public IEnumerable Suggest(IShellState shellState, TProgramState programState, TParseResult parseResult) + { + DefaultCommandInput.TryProcess(InputSpec, parseResult, out DefaultCommandInput commandInput, out IReadOnlyList _); + + string normalCompletionString = parseResult.SelectedSection == parseResult.Sections.Count + ? string.Empty + : parseResult.Sections[parseResult.SelectedSection].Substring(0, parseResult.CaretPositionWithinSelectedSection); + + //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) + { + if (!string.Equals(InputSpec.CommandName[i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase)) + { + return null; + } + } + + if (InputSpec.CommandName[parseResult.SelectedSection].StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)) + { + return new[] {InputSpec.CommandName[parseResult.SelectedSection]}; + } + } + + if (commandInput is null) + { + return null; + } + + if (normalCompletionString.StartsWith(InputSpec.OptionPreamble)) + { + return GetOptionCompletions(commandInput, normalCompletionString); + } + + IEnumerable completions = Enumerable.Empty(); + CommandInputLocation? inputLocation = commandInput.SelectedElement?.Location; + + if (inputLocation != CommandInputLocation.OptionValue && commandInput.Arguments.Count < InputSpec.MaximumArguments) + { + IEnumerable results = GetArgumentSuggestionsForText(shellState, programState, parseResult, commandInput, normalCompletionString); + + if (results != null) + { + completions = results; + } + } + + switch (inputLocation) + { + case CommandInputLocation.OptionName: + { + IEnumerable results = GetOptionCompletions(commandInput, normalCompletionString); + + if (results != null) + { + completions = completions.Union(results); + } + + break; + } + case CommandInputLocation.OptionValue: + { + IEnumerable results = GetOptionValueCompletions(shellState, programState, commandInput.SelectedElement.Owner.NormalizedText, commandInput, parseResult, normalCompletionString); + + if (results != null) + { + completions = completions.Union(results); + } + + break; + } + case CommandInputLocation.Argument: + { + IEnumerable argumentResults = GetArgumentSuggestionsForText(shellState, programState, parseResult, commandInput, normalCompletionString); + + if (argumentResults != null) + { + completions = completions.Union(argumentResults); + } + + if (string.IsNullOrEmpty(normalCompletionString)) + { + IEnumerable results = GetOptionCompletions(commandInput, normalCompletionString); + + if (results != null) + { + completions = completions.Union(results); + } + } + + break; + } + } + + return completions; + } + + protected virtual IEnumerable GetOptionValueCompletions(IShellState shellState, TProgramState programState, string optionId, DefaultCommandInput commandInput, TParseResult parseResult, string normalizedCompletionText) + { + return null; + } + + protected virtual IEnumerable GetArgumentSuggestionsForText(IShellState shellState, TProgramState programState, TParseResult parseResult, DefaultCommandInput commandInput, string normalCompletionString) + { + return null; + } + + private IEnumerable GetOptionCompletions(DefaultCommandInput commandInput, string normalCompletionString) + { + return InputSpec.Options.Where(x => commandInput.Options[x.Id].Count < x.MaximumOccurrences) + .SelectMany(x => x.Forms) + .Where(x => x.StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)); + } + + public bool? CanHandle(IShellState shellState, TProgramState programState, TParseResult parseResult) + { + if (!DefaultCommandInput.TryProcess(InputSpec, parseResult, out DefaultCommandInput commandInput, out IReadOnlyList processingIssues)) + { + //If this is the right command, just not the right syntax, report the usage errors + if (processingIssues.All(x => x.Kind != CommandInputProcessingIssueKind.CommandMismatch)) + { + foreach (CommandInputProcessingIssue issue in processingIssues) + { + shellState.ConsoleManager.Error.WriteLine(GetStringForIssue(issue)); + } + + string help = GetHelpDetails(shellState, programState, parseResult); + shellState.ConsoleManager.WriteLine(help); + return false; + } + + //If there was a mismatch in the command name, this isn't our input to handle + return null; + } + + return CanHandle(shellState, programState, commandInput); + } + + protected virtual bool CanHandle(IShellState shellState, TProgramState programState, DefaultCommandInput commandInput) + { + return true; + } + + protected virtual string GetStringForIssue(CommandInputProcessingIssue issue) + { + //TODO: Make this nicer + return issue.Kind + " -- " + issue.Text; + } + + public Task ExecuteAsync(IShellState shellState, TProgramState programState, TParseResult parseResult, CancellationToken cancellationToken) + { + if (!DefaultCommandInput.TryProcess(InputSpec, parseResult, out DefaultCommandInput commandInput, out IReadOnlyList _)) + { + return Task.CompletedTask; + } + + return ExecuteAsync(shellState, programState, commandInput, parseResult, cancellationToken); + } + + protected abstract Task ExecuteAsync(IShellState shellState, TProgramState programState, DefaultCommandInput commandInput, TParseResult parseResult, CancellationToken cancellationToken); + + protected abstract CommandInputSpecification InputSpec { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs new file mode 100644 index 0000000000..932e8cdb1a --- /dev/null +++ b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Commanding +{ + public static class DefaultCommandDispatcher + { + public static DefaultCommandDispatcher Create(Func getPrompt, TProgramState programState) + { + return new DefaultCommandDispatcher(getPrompt, programState); + } + + public static DefaultCommandDispatcher Create(Action onReady, TProgramState programState) + { + return new DefaultCommandDispatcher(onReady, programState); + } + + public static DefaultCommandDispatcher Create(Func getPrompt, TProgramState programState, IParser parser) + where TParseResult : ICoreParseResult + { + return new DefaultCommandDispatcher(getPrompt, programState, parser); + } + + public static DefaultCommandDispatcher Create(Action onReady, TProgramState programState, IParser parser) + where TParseResult : ICoreParseResult + { + return new DefaultCommandDispatcher(onReady, programState, parser); + } + } + + public class DefaultCommandDispatcher : DefaultCommandDispatcher + { + public DefaultCommandDispatcher(Func getPrompt, TProgramState programState) + : base(getPrompt, programState, new CoreParser()) + { + } + + public DefaultCommandDispatcher(Action onReady, TProgramState programState) + : base(onReady, programState, new CoreParser()) + { + } + } + + public class DefaultCommandDispatcher : ICommandDispatcher + where TParseResult : ICoreParseResult + { + private readonly Action _onReady; + private readonly TProgramState _programState; + private readonly IParser _parser; + private readonly HashSet> _commands = new HashSet>(); + private bool _isReady; + + public DefaultCommandDispatcher(Func getPrompt, TProgramState programState, IParser parser) + : this(s => s.ConsoleManager.Write(getPrompt()), programState, parser) + { + } + + public DefaultCommandDispatcher(Action onReady, TProgramState programState, IParser parser) + { + _onReady = onReady; + _programState = programState; + _parser = parser; + } + + public void AddCommand(ICommand command) + { + _commands.Add(command); + } + + public IEnumerable> Commands => _commands; + + public IParser Parser => _parser; + + public IReadOnlyList CollectSuggesetions(IShellState shellState) + { + string line = shellState.InputManager.GetCurrentBuffer(); + TParseResult parseResult = _parser.Parse(line, shellState.ConsoleManager.CaretPosition); + HashSet suggestions = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (ICommand command in _commands) + { + IEnumerable commandSuggestions = command.Suggest(shellState, _programState, parseResult); + + if (commandSuggestions != null) + { + suggestions.UnionWith(commandSuggestions); + } + } + + return suggestions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + } + + public async Task ExecuteCommandAsync(IShellState shellState, CancellationToken cancellationToken) + { + _isReady = false; + shellState.ConsoleManager.WriteLine(); + string commandText = shellState.InputManager.GetCurrentBuffer(); + + if (!string.IsNullOrWhiteSpace(commandText)) + { + shellState.CommandHistory.AddCommand(shellState.InputManager.GetCurrentBuffer()); + + try + { + await ExecuteCommandInternalAsync(shellState, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + shellState.ConsoleManager.Error.WriteLine(ex.ToString().Bold().Red()); + } + + if (cancellationToken.IsCancellationRequested) + { + shellState.ConsoleManager.Error.WriteLine("Execution was cancelled".Bold().Red()); + } + } + + if (!_isReady) + { + shellState.ConsoleManager.WriteLine(); + OnReady(shellState); + } + + shellState.InputManager.ResetInput(); + } + + private async Task ExecuteCommandInternalAsync(IShellState shellState, CancellationToken cancellationToken) + { + string line = shellState.InputManager.GetCurrentBuffer(); + TParseResult parseResult = _parser.Parse(line, shellState.ConsoleManager.CaretPosition); + + if (!string.IsNullOrWhiteSpace(parseResult.CommandText)) + { + foreach (ICommand command in _commands) + { + bool? result = command.CanHandle(shellState, _programState, parseResult); + + if (result.HasValue) + { + if (result.Value) + { + await command.ExecuteAsync(shellState, _programState, parseResult, cancellationToken); + } + + //If the handler returned non-null, the input would be directed to it, but it's not valid input + return; + } + } + + shellState.ConsoleManager.Error.WriteLine("No matching command found".Red().Bold()); + } + } + + public void OnReady(IShellState shellState) + { + if (!_isReady) + { + _onReady(shellState); + shellState.ConsoleManager.ResetCommandStart(); + _isReady = true; + } + } + } +} diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs b/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs new file mode 100644 index 0000000000..0d3b97932e --- /dev/null +++ b/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Commanding +{ + public class DefaultCommandInput + where TParseResult : ICoreParseResult + { + public DefaultCommandInput(IReadOnlyList commandName, IReadOnlyList arguments, IReadOnlyDictionary> options, InputElement selectedElement) + { + CommandName = commandName; + Arguments = arguments; + Options = options; + SelectedElement = selectedElement; + } + + public static bool TryProcess(CommandInputSpecification spec, TParseResult parseResult, out DefaultCommandInput result, out IReadOnlyList processingIssues) + { + List issues = new List(); + List commandNameElements = new List(); + + if (spec.CommandName.Count > parseResult.Sections.Count) + { + 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)) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, parseResult.Sections[i])); + } + + 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) + { + result = null; + processingIssues = issues; + return false; + } + + List arguments = new List(); + Dictionary options = new Dictionary(); + InputElement currentOption = null; + CommandOptionSpecification currentOptionSpec = null; + InputElement selectedElement = null; + + for (int i = spec.CommandName.Count; i < parseResult.Sections.Count; ++i) + { + //If we're not looking at an option name + if (!parseResult.Sections[i].StartsWith(spec.OptionPreamble) || parseResult.IsQuotedSection(i)) + { + if (currentOption is null) + { + InputElement currentElement = new InputElement(CommandInputLocation.Argument, parseResult.Sections[i], parseResult.Sections[i], i); + + if (i == parseResult.SelectedSection) + { + selectedElement = currentElement; + } + + arguments.Add(currentElement); + } + else + { + //If the option isn't a defined one or it is and indicates that it accepts a value, add the section as an option value, + // otherwise add it as an argument + if (currentOptionSpec?.AcceptsValue ?? true) + { + InputElement currentElement = new InputElement(currentOption, CommandInputLocation.OptionValue, parseResult.Sections[i], parseResult.Sections[i], i); + + if (i == parseResult.SelectedSection) + { + selectedElement = currentElement; + } + + options[currentOption] = currentElement; + currentOption = null; + currentOptionSpec = null; + } + else + { + InputElement currentElement = new InputElement(CommandInputLocation.Argument, parseResult.Sections[i], parseResult.Sections[i], i); + + if (i == parseResult.SelectedSection) + { + selectedElement = currentElement; + } + + arguments.Add(currentElement); + } + } + } + //If we are looking at an option name + else + { + //Otherwise, check to see whether the previous option had a required argument before committing it + if (!(currentOption is null)) + { + options[currentOption] = null; + + if (currentOptionSpec?.RequiresValue ?? false) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.MissingRequiredOptionInput, currentOption.Text)); + } + } + + CommandOptionSpecification optionSpec = spec.Options.FirstOrDefault(x => x.Forms.Any(y => string.Equals(y, parseResult.Sections[i], StringComparison.Ordinal))); + + if (optionSpec is null) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.UnknownOption, parseResult.Sections[i])); + } + + currentOption = new InputElement(CommandInputLocation.OptionName, parseResult.Sections[i], optionSpec?.Id, i); + + if (i == parseResult.SelectedSection) + { + selectedElement = currentOption; + } + + currentOptionSpec = optionSpec; + } + } + + //Clear any option in progress + if (!(currentOption is null)) + { + options[currentOption] = null; + + if (currentOptionSpec?.RequiresValue ?? false) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.MissingRequiredOptionInput, currentOption.Text)); + } + } + + //Check to make sure our argument count is in range, if not add an issue + if (arguments.Count > spec.MaximumArguments || arguments.Count < spec.MinimumArguments) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.ArgumentCountOutOfRange, arguments.Count.ToString())); + } + + //Build up the dictionary of options by normal form, then validate counts for every option in the spec + Dictionary> optionsByNormalForm = new Dictionary>(StringComparer.Ordinal); + + foreach (KeyValuePair entry in options) + { + if (entry.Key.NormalizedText is null) + { + continue; + } + + if (!optionsByNormalForm.TryGetValue(entry.Key.NormalizedText, out IReadOnlyList rawBucket)) + { + optionsByNormalForm[entry.Key.NormalizedText] = rawBucket = new List(); + } + + List bucket = (List) rawBucket; + bucket.Add(entry.Value); + } + + foreach (CommandOptionSpecification optionSpec in spec.Options) + { + if (!optionsByNormalForm.TryGetValue(optionSpec.Id, out IReadOnlyList values)) + { + optionsByNormalForm[optionSpec.Id] = values = new List(); + } + + if (values.Count < optionSpec.MinimumOccurrences || values.Count > optionSpec.MaximumOccurrences) + { + issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.OptionUseCountOutOfRange, values.Count.ToString())); + } + } + + result = new DefaultCommandInput(commandNameElements, arguments, optionsByNormalForm, selectedElement); + processingIssues = issues; + return issues.Count == 0; + } + + public InputElement SelectedElement { get; } + + public IReadOnlyList CommandName { get; } + + public IReadOnlyList Arguments { get; } + + public IReadOnlyDictionary> Options { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/Commanding/ICommand.cs b/src/Microsoft.Repl/Commanding/ICommand.cs new file mode 100644 index 0000000000..be575437e2 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/ICommand.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Commanding +{ + public interface ICommand + where TParseResult : ICoreParseResult + { + string GetHelpSummary(IShellState shellState, TProgramState programState); + + string GetHelpDetails(IShellState shellState, TProgramState programState, TParseResult parseResult); + + IEnumerable Suggest(IShellState shellState, TProgramState programState, TParseResult parseResult); + + bool? CanHandle(IShellState shellState, TProgramState programState, TParseResult parseResult); + + Task ExecuteAsync(IShellState shellState, TProgramState programState, TParseResult parseResult, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs b/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs new file mode 100644 index 0000000000..c175490833 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Commanding +{ + public interface ICommandDispatcher + { + IParser Parser { get; } + + IReadOnlyList CollectSuggesetions(IShellState shellState); + + void OnReady(IShellState shellState); + + Task ExecuteCommandAsync(IShellState shellState, CancellationToken cancellationToken); + } + + public interface ICommandDispatcher : ICommandDispatcher + where TParseResult : ICoreParseResult + { + IEnumerable> Commands { get; } + } +} diff --git a/src/Microsoft.Repl/Commanding/ICommandHistory.cs b/src/Microsoft.Repl/Commanding/ICommandHistory.cs new file mode 100644 index 0000000000..8e25db8906 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/ICommandHistory.cs @@ -0,0 +1,15 @@ +using System; + +namespace Microsoft.Repl.Commanding +{ + public interface ICommandHistory + { + string GetPreviousCommand(); + + string GetNextCommand(); + + void AddCommand(string command); + + IDisposable SuspendHistory(); + } +} diff --git a/src/Microsoft.Repl/Commanding/InputElement.cs b/src/Microsoft.Repl/Commanding/InputElement.cs new file mode 100644 index 0000000000..c02c7f0f23 --- /dev/null +++ b/src/Microsoft.Repl/Commanding/InputElement.cs @@ -0,0 +1,29 @@ +namespace Microsoft.Repl.Commanding +{ + public class InputElement + { + public CommandInputLocation Location { get; } + + public string Text { get; } + + public string NormalizedText { get; } + + public InputElement Owner { get; } + + public int ParseResultSectionIndex { get; } + + public InputElement(CommandInputLocation location, string text, string normalizedText, int sectionIndex) + : this(null, location, text, normalizedText, sectionIndex) + { + } + + public InputElement(InputElement owner, CommandInputLocation location, string text, string normalizedText, int sectionIndex) + { + Owner = owner; + Location = location; + Text = text; + NormalizedText = normalizedText; + ParseResultSectionIndex = sectionIndex; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs b/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs new file mode 100644 index 0000000000..a71dc2c8a4 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs @@ -0,0 +1,27 @@ +using System; + +namespace Microsoft.Repl.ConsoleHandling +{ + [Flags] + public enum AllowedColors + { + Black = 0x00, + BoldBlack = Bold | Black, + Red = 0x01, + BoldRed = Bold | Red, + Green = 0x02, + BoldGreen = Bold | Green, + Yellow = 0x03, + BoldYellow = Bold | Yellow, + Blue = 0x04, + BoldBlue = Bold | Blue, + Magenta = 0x05, + BoldMagenta = Bold | Magenta, + Cyan = 0x06, + BoldCyan = Bold | Cyan, + White = 0x07, + BoldWhite = White | Bold, + Bold = 0x100, + None = 0x99 + } +} \ No newline at end of file diff --git a/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs b/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs new file mode 100644 index 0000000000..cab5bc6f05 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs @@ -0,0 +1,80 @@ +namespace Microsoft.Repl.ConsoleHandling +{ + public static class AnsiColorExtensions + { + public static string Black(this string text) + { + return "\x1B[30m" + text + "\x1B[39m"; + } + + public static string Red(this string text) + { + return "\x1B[31m" + text + "\x1B[39m"; + } + public static string Green(this string text) + { + return "\x1B[32m" + text + "\x1B[39m"; + } + + public static string Yellow(this string text) + { + return "\x1B[33m" + text + "\x1B[39m"; + } + + public static string Blue(this string text) + { + return "\x1B[34m" + text + "\x1B[39m"; + } + + public static string Magenta(this string text) + { + return "\x1B[35m" + text + "\x1B[39m"; + } + + public static string Cyan(this string text) + { + return "\x1B[36m" + text + "\x1B[39m"; + } + + public static string White(this string text) + { + return "\x1B[37m" + text + "\x1B[39m"; + } + + public static string Bold(this string text) + { + return "\x1B[1m" + text + "\x1B[22m"; + } + + public static string SetColor(this string text, AllowedColors color) + { + if (color.HasFlag(AllowedColors.Bold)) + { + text = text.Bold(); + color = color & ~AllowedColors.Bold; + } + + switch (color) + { + case AllowedColors.Black: + return text.Black(); + case AllowedColors.Red: + return text.Red(); + case AllowedColors.Green: + return text.Green(); + case AllowedColors.Yellow: + return text.Yellow(); + case AllowedColors.Blue: + return text.Blue(); + case AllowedColors.Magenta: + return text.Magenta(); + case AllowedColors.Cyan: + return text.Cyan(); + case AllowedColors.White: + return text.White(); + default: + return text; + } + } + } +} diff --git a/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs b/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs new file mode 100644 index 0000000000..24944f8871 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs @@ -0,0 +1,151 @@ +using System; +using System.IO; + +namespace Microsoft.Repl.ConsoleHandling +{ + public class AnsiConsole + { + private AnsiConsole(TextWriter writer) + { + Writer = writer; + + OriginalForegroundColor = Console.ForegroundColor; + } + + private int _boldRecursion; + + public static AnsiConsole GetOutput() + { + return new AnsiConsole(Console.Out); + } + + public static AnsiConsole GetError() + { + return new AnsiConsole(Console.Error); + } + + public TextWriter Writer { get; } + + public ConsoleColor OriginalForegroundColor { get; } + + private void SetColor(ConsoleColor color) + { + const int light = 0x08; + int c = (int)color; + + Console.ForegroundColor = + c < 0 ? color : // unknown, just use it + _boldRecursion > 0 ? (ConsoleColor)(c | light) : // ensure color is light + (ConsoleColor)(c & ~light); // ensure color is dark + } + + private void SetBold(bool bold) + { + _boldRecursion += bold ? 1 : -1; + if (_boldRecursion > 1 || (_boldRecursion == 1 && !bold)) + { + return; + } + + // switches on _boldRecursion to handle boldness + SetColor(Console.ForegroundColor); + } + + public void WriteLine(string message) + { + Write(message); + Writer.WriteLine(); + } + + public void Write(char message) + { + Writer.Write(message); + } + + public void Write(string message) + { + if (message is null) + { + return; + } + + var escapeScan = 0; + for (; ; ) + { + var escapeIndex = message.IndexOf("\x1b[", escapeScan, StringComparison.Ordinal); + if (escapeIndex == -1) + { + var text = message.Substring(escapeScan); + Writer.Write(text); + break; + } + else + { + var startIndex = escapeIndex + 2; + var endIndex = startIndex; + while (endIndex != message.Length && + message[endIndex] >= 0x20 && + message[endIndex] <= 0x3f) + { + endIndex += 1; + } + + var text = message.Substring(escapeScan, escapeIndex - escapeScan); + Writer.Write(text); + if (endIndex == message.Length) + { + break; + } + + switch (message[endIndex]) + { + case 'm': + if (int.TryParse(message.Substring(startIndex, endIndex - startIndex), out int value)) + { + switch (value) + { + case 1: + SetBold(true); + break; + case 22: + SetBold(false); + break; + case 30: + SetColor(ConsoleColor.Black); + break; + case 31: + SetColor(ConsoleColor.Red); + break; + case 32: + SetColor(ConsoleColor.Green); + break; + case 33: + SetColor(ConsoleColor.Yellow); + break; + case 34: + SetColor(ConsoleColor.Blue); + break; + case 35: + SetColor(ConsoleColor.Magenta); + break; + case 36: + SetColor(ConsoleColor.Cyan); + break; + case 37: + SetColor(ConsoleColor.Gray); + break; + case 39: + Console.ForegroundColor = OriginalForegroundColor; + break; + } + } + break; + } + + escapeScan = endIndex + 1; + } + } + } + } + +} diff --git a/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs b/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs new file mode 100644 index 0000000000..a15c1fd420 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Microsoft.Repl.ConsoleHandling +{ + public class ConsoleManager : IConsoleManager + { + private readonly List _breakHandlers = new List(); + + public Point Caret => new Point(Console.CursorLeft, Console.CursorTop); + + public Point CommandStart => new Point(Caret.X - CaretPosition % Console.BufferWidth, Caret.Y - CaretPosition / Console.BufferWidth); + + public int CaretPosition { get; private set; } + + public bool IsKeyAvailable => Console.KeyAvailable; + + public bool IsCaretVisible + { + get => Reporter.Output.IsCaretVisible; + set => Reporter.Output.IsCaretVisible = value; + } + + public ConsoleManager() + { + Error = new Writable(CaretUpdateScope, Reporter.Error); + Console.CancelKeyPress += OnCancelKeyPress; + } + + public void Clear() + { + using (CaretUpdateScope()) + { + Console.Clear(); + ResetCommandStart(); + } + } + + public void MoveCaret(int positions) + { + using (CaretUpdateScope()) + { + if (positions == 0) + { + return; + } + + while (positions < 0 && CaretPosition > 0) + { + if (-positions > Console.BufferWidth) + { + if (Console.CursorTop == 0) + { + Console.CursorLeft = 0; + positions = 0; + } + else + { + positions += Console.BufferWidth; + --Console.CursorTop; + } + } + else + { + int remaining = Console.CursorLeft + positions; + + if (remaining >= 0) + { + Console.CursorLeft = remaining; + } + else if (Console.CursorTop == 0) + { + Console.CursorLeft = 0; + } + else + { + --Console.CursorTop; + Console.CursorLeft = Console.BufferWidth + remaining; + } + + positions = 0; + } + } + + while (positions > 0) + { + if (positions > Console.BufferWidth) + { + positions -= Console.BufferWidth; + ++Console.CursorTop; + } + else + { + int spaceLeftOnLine = Console.BufferWidth - Console.CursorLeft - 1; + if (positions > spaceLeftOnLine) + { + ++Console.CursorTop; + Console.CursorLeft = positions - spaceLeftOnLine - 1; + } + else + { + Console.CursorLeft += positions; + } + + positions = 0; + } + } + } + } + + public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) + { + while (!Console.KeyAvailable && !cancellationToken.IsCancellationRequested) + { + Thread.Sleep(2); + } + + if (cancellationToken.IsCancellationRequested) + { + return default(ConsoleKeyInfo); + } + else + { + return Console.ReadKey(true); + } + } + + public void ResetCommandStart() + { + CaretPosition = 0; + } + + public void Write(char c) + { + using (CaretUpdateScope()) + { + Reporter.Output.Write(c); + } + } + + public void Write(string s) + { + using (CaretUpdateScope()) + { + Reporter.Output.Write(s); + } + } + + public void WriteLine() + { + using (CaretUpdateScope()) + { + Reporter.Output.WriteLine(); + } + } + + public void WriteLine(string s) + { + if (s is null) + { + return; + } + + using (CaretUpdateScope()) + { + Reporter.Output.WriteLine(s); + } + } + + public IDisposable AddBreakHandler(Action handler) + { + Disposable result = new Disposable(() => ReleaseBreakHandler(handler)); + _breakHandlers.Add(handler); + return result; + } + + private IDisposable CaretUpdateScope() + { + Point currentCaret = Caret; + return new Disposable(() => + { + int y = Caret.Y - currentCaret.Y; + int x = Caret.X - currentCaret.X; + CaretPosition += y * Console.BufferWidth + x; + }); + } + + private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + e.Cancel = true; + Action handler = _breakHandlers.LastOrDefault(); + handler?.Invoke(); + } + + private void ReleaseBreakHandler(Action handler) + { + _breakHandlers.Remove(handler); + } + + public IWritable Error { get; } + } +} diff --git a/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs b/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs new file mode 100644 index 0000000000..9ab7b26a15 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading; + +namespace Microsoft.Repl.ConsoleHandling +{ + public interface IConsoleManager : IWritable + { + Point Caret { get; } + + Point CommandStart { get; } + + int CaretPosition { get; } + + IWritable Error { get; } + + bool IsKeyAvailable { get; } + + void Clear(); + + void MoveCaret(int positions); + + ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); + + void ResetCommandStart(); + + IDisposable AddBreakHandler(Action onBreak); + } +} diff --git a/src/Microsoft.Repl/ConsoleHandling/IWritable.cs b/src/Microsoft.Repl/ConsoleHandling/IWritable.cs new file mode 100644 index 0000000000..f18fa123a1 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/IWritable.cs @@ -0,0 +1,15 @@ +namespace Microsoft.Repl.ConsoleHandling +{ + public interface IWritable + { + void Write(char c); + + void Write(string s); + + void WriteLine(); + + void WriteLine(string s); + + bool IsCaretVisible { get; set; } + } +} diff --git a/src/Microsoft.Repl/ConsoleHandling/Point.cs b/src/Microsoft.Repl/ConsoleHandling/Point.cs new file mode 100644 index 0000000000..4eeb68989d --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/Point.cs @@ -0,0 +1,50 @@ +namespace Microsoft.Repl.ConsoleHandling +{ + public struct Point + { + public readonly int X; + + public readonly int Y; + + public Point(int x, int y) + { + X = x; + Y = y; + } + + public static bool operator >(Point left, Point right) + { + return left.Y > right.Y || (left.Y == right.Y && right.X > left.X); + } + + public static bool operator <(Point left, Point right) + { + return left.Y < right.Y || (left.Y == right.Y && right.X < left.X); + } + + public static bool operator ==(Point left, Point right) + { + return left.X == right.X && left.Y == right.Y; + } + + public static bool operator !=(Point left, Point right) + { + return left.X != right.X || left.Y != right.Y; + } + + public override bool Equals(object obj) + { + return obj is Point other && other.X == X && other.Y == Y; + } + + public override int GetHashCode() + { + return X ^ Y; + } + + public override string ToString() + { + return $"(X={X}, Y={Y})"; + } + } +} diff --git a/src/Microsoft.Repl/ConsoleHandling/Reporter.cs b/src/Microsoft.Repl/ConsoleHandling/Reporter.cs new file mode 100644 index 0000000000..5b10d2be02 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/Reporter.cs @@ -0,0 +1,115 @@ +using System; + +namespace Microsoft.Repl.ConsoleHandling +{ + public class Reporter : IWritable + { + private static readonly Reporter NullReporter = new Reporter(null); + private static readonly object Sync = new object(); + + private readonly AnsiConsole _console; + + static Reporter() + { + Reset(); + } + + private Reporter(AnsiConsole console) + { + _console = console; + } + + public static Reporter Output { get; private set; } + public static Reporter Error { get; private set; } + public static Reporter Verbose { get; private set; } + + /// + /// Resets the Reporters to write to the current Console Out/Error. + /// + public static void Reset() + { + lock (Sync) + { + Output = new Reporter(AnsiConsole.GetOutput()); + Error = new Reporter(AnsiConsole.GetError()); + Verbose = IsVerbose ? + new Reporter(AnsiConsole.GetOutput()) : + NullReporter; + } + } + + public void WriteLine(string message) + { + if (message is null) + { + return; + } + + lock (Sync) + { + if (ShouldPassAnsiCodesThrough) + { + _console?.Writer?.WriteLine(message); + } + else + { + _console?.WriteLine(message); + } + } + } + + public void WriteLine() + { + lock (Sync) + { + _console?.Writer?.WriteLine(); + } + } + + public void Write(char message) + { + lock (Sync) + { + if (ShouldPassAnsiCodesThrough) + { + _console?.Writer?.Write(message); + } + else + { + _console?.Write(message); + } + } + } + + public void Write(string message) + { + lock (Sync) + { + if (ShouldPassAnsiCodesThrough) + { + _console?.Writer?.Write(message); + } + else + { + _console?.Write(message); + } + } + } + + private static bool IsVerbose => bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE") ?? "false", out bool value) && value; + + private bool ShouldPassAnsiCodesThrough => bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_ANSI_PASS_THRU") ?? "false", out bool value) && value; + + private bool _isCaretVisible = true; + + public bool IsCaretVisible + { + get => _isCaretVisible; + set + { + Console.CursorVisible = value; + _isCaretVisible = value; + } + } + } +} diff --git a/src/Microsoft.Repl/ConsoleHandling/Writable.cs b/src/Microsoft.Repl/ConsoleHandling/Writable.cs new file mode 100644 index 0000000000..b12c2ad4e7 --- /dev/null +++ b/src/Microsoft.Repl/ConsoleHandling/Writable.cs @@ -0,0 +1,54 @@ +using System; + +namespace Microsoft.Repl.ConsoleHandling +{ + internal class Writable : IWritable + { + private readonly Func _caretUpdater; + private readonly Reporter _reporter; + + public Writable(Func caretUpdater, Reporter reporter) + { + _caretUpdater = caretUpdater; + _reporter = reporter; + } + + public bool IsCaretVisible + { + get => _reporter.IsCaretVisible; + set => _reporter.IsCaretVisible = value; + } + + public void Write(char c) + { + using (_caretUpdater()) + { + _reporter.Write(c); + } + } + + public void Write(string s) + { + using (_caretUpdater()) + { + _reporter.Write(s); + } + } + + public void WriteLine() + { + using (_caretUpdater()) + { + _reporter.WriteLine(); + } + } + + public void WriteLine(string s) + { + using (_caretUpdater()) + { + _reporter.WriteLine(s); + } + } + } +} diff --git a/src/Microsoft.Repl/Disposable.cs b/src/Microsoft.Repl/Disposable.cs new file mode 100644 index 0000000000..78002fb1d9 --- /dev/null +++ b/src/Microsoft.Repl/Disposable.cs @@ -0,0 +1,42 @@ +using System; + +namespace Microsoft.Repl +{ + public class Disposable : IDisposable + { + private Action _onDispose; + + public Disposable(Action onDispose) + { + _onDispose = onDispose; + } + public virtual void Dispose() + { + _onDispose?.Invoke(); + _onDispose = null; + } + } + + public class Disposable : Disposable + where T : class + { + public Disposable(T value, Action onDispose) + : base (onDispose) + { + Value = value; + } + + public T Value { get; private set; } + + public override void Dispose() + { + if (Value is IDisposable d) + { + d.Dispose(); + Value = null; + } + + base.Dispose(); + } + } +} diff --git a/src/Microsoft.Repl/IShellState.cs b/src/Microsoft.Repl/IShellState.cs new file mode 100644 index 0000000000..2ec1cb1514 --- /dev/null +++ b/src/Microsoft.Repl/IShellState.cs @@ -0,0 +1,22 @@ +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Input; +using Microsoft.Repl.Suggestions; + +namespace Microsoft.Repl +{ + public interface IShellState + { + IInputManager InputManager { get; } + + ICommandHistory CommandHistory { get; } + + IConsoleManager ConsoleManager { get; } + + ICommandDispatcher CommandDispatcher { get; } + + ISuggestionManager SuggestionManager { get; } + + bool IsExiting { get; set; } + } +} diff --git a/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs b/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs new file mode 100644 index 0000000000..616a749611 --- /dev/null +++ b/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs @@ -0,0 +1,8 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Repl.Input +{ + public delegate Task AsyncKeyPressHandler(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken); +} diff --git a/src/Microsoft.Repl/Input/IInputManager.cs b/src/Microsoft.Repl/Input/IInputManager.cs new file mode 100644 index 0000000000..100dd22bea --- /dev/null +++ b/src/Microsoft.Repl/Input/IInputManager.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Repl.Input +{ + public interface IInputManager + { + bool IsOverwriteMode { get; set; } + + IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler); + + void ResetInput(); + + Task StartAsync(IShellState state, CancellationToken cancellationToken); + + void SetInput(IShellState state, string input); + + string GetCurrentBuffer(); + + void RemovePreviousCharacter(IShellState state); + + void RemoveCurrentCharacter(IShellState state); + + void Clear(IShellState state); + } +} diff --git a/src/Microsoft.Repl/Input/InputManager.cs b/src/Microsoft.Repl/Input/InputManager.cs new file mode 100644 index 0000000000..a635ccd37a --- /dev/null +++ b/src/Microsoft.Repl/Input/InputManager.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Repl.Input +{ + public class InputManager : IInputManager + { + private readonly Dictionary _handlers = new Dictionary(); + private readonly List _inputBuffer = new List(); + + public bool IsOverwriteMode { get; set; } + + public void Clear(IShellState state) + { + SetInput(state, string.Empty); + } + + public string GetCurrentBuffer() + { + return _inputBuffer.Stringify(); + } + + public IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler) + { + if (handler == null) + { + _handlers.Remove(key); + } + else + { + _handlers[key] = handler; + } + + return this; + } + + public void RemoveCurrentCharacter(IShellState state) + { + int caret = state.ConsoleManager.CaretPosition; + + if (caret == _inputBuffer.Count) + { + return; + } + + List update = _inputBuffer.ToList(); + update.RemoveAt(caret); + state.ConsoleManager.IsCaretVisible = false; + SetInput(state, update); + state.ConsoleManager.MoveCaret(caret - state.ConsoleManager.CaretPosition); + state.ConsoleManager.IsCaretVisible = true; + } + + public void RemovePreviousCharacter(IShellState state) + { + int caret = state.ConsoleManager.CaretPosition; + if (caret == 0) + { + return; + } + + List update = _inputBuffer.ToList(); + update.RemoveAt(caret - 1); + state.ConsoleManager.IsCaretVisible = false; + SetInput(state, update, false); + state.ConsoleManager.MoveCaret(caret - state.ConsoleManager.CaretPosition - 1); + state.ConsoleManager.IsCaretVisible = true; + } + + public void SetInput(IShellState state, string input) + { + SetInput(state, input.ToCharArray()); + } + + public void ResetInput() + { + _inputBuffer.Clear(); + } + + private string _ttyState; + + private void StashEchoState() + { + _ttyState = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX) + ? GetTtyState() + : null; + + if (!string.IsNullOrEmpty(_ttyState)) + { + //"gfmt1:cflag=4300:iflag=6b02:lflag=200005c7:oflag=3:discard=f:dsusp=19:eof=4:eol=ff:eol2=ff:erase=7f:intr=3:kill=15:lnext=16:min=1:quit=1c:reprint=12:start=11:status=14:stop=13:susp=1a:time=0:werase=17:ispeed=38400:ospeed=38400\n" + ProcessStartInfo psi = new ProcessStartInfo("stty", "gfmt1:erase=08:werase=08 -echo"); + Process p = Process.Start(psi); + p?.WaitForExit(); + } + } + + private static string GetTtyState() + { + ProcessStartInfo psi = new ProcessStartInfo("stty", "-g") + { + RedirectStandardOutput = true + }; + Process p = Process.Start(psi); + p?.WaitForExit(); + string result = p?.StandardOutput.ReadToEnd(); + return result; + } + + private void RestoreTtyState() + { + if (!string.IsNullOrEmpty(_ttyState)) + { + ProcessStartInfo psi = new ProcessStartInfo("stty", _ttyState); + Process p = Process.Start(psi); + p?.WaitForExit(); + } + } + + private void SetInput(IShellState state, IReadOnlyList input, bool moveCaret = true) + { + bool oldCaretVisibility = state.ConsoleManager.IsCaretVisible; + state.ConsoleManager.IsCaretVisible = false; + int lastCommonPosition = 0; + + for (; lastCommonPosition < input.Count && lastCommonPosition < _inputBuffer.Count && _inputBuffer[lastCommonPosition] == input[lastCommonPosition]; ++lastCommonPosition) + { + } + + state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition + lastCommonPosition); + string str = new string(input.Skip(lastCommonPosition).ToArray()); + int trailing = _inputBuffer.Count - input.Count; + + if (trailing > 0) + { + str = str.PadRight(trailing + str.Length); + } + + state.ConsoleManager.Write(str); + + if (trailing > 0 && moveCaret) + { + state.ConsoleManager.MoveCaret(-trailing); + } + + _inputBuffer.Clear(); + _inputBuffer.AddRange(input); + + if (oldCaretVisibility) + { + state.ConsoleManager.IsCaretVisible = true; + } + } + + public async Task StartAsync(IShellState state, CancellationToken cancellationToken) + { + StashEchoState(); + + try + { + List presses = null; + while (!state.IsExiting && !cancellationToken.IsCancellationRequested) + { + ConsoleKeyInfo keyPress = state.ConsoleManager.ReadKey(cancellationToken); + + if (_handlers.TryGetValue(keyPress.Key, out AsyncKeyPressHandler handler)) + { + using (CancellationTokenSource source = new CancellationTokenSource()) + using (state.ConsoleManager.AddBreakHandler(() => source.Cancel())) + { + if (presses != null) + { + FlushInput(state, ref presses); + } + + await handler(keyPress, state, source.Token).ConfigureAwait(false); + } + } + else if (!string.IsNullOrEmpty(_ttyState) && keyPress.Modifiers == ConsoleModifiers.Control) + { + if (presses != null) + { + FlushInput(state, ref presses); + } + + if (keyPress.Key == ConsoleKey.A) + { + state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition); + } + else if (keyPress.Key == ConsoleKey.E) + { + state.ConsoleManager.MoveCaret(_inputBuffer.Count - state.ConsoleManager.CaretPosition); + } + } + else if (!string.IsNullOrEmpty(_ttyState) && keyPress.Modifiers == ConsoleModifiers.Alt) + { + if (presses != null) + { + FlushInput(state, ref presses); + } + + //Move back a word + if (keyPress.Key == ConsoleKey.B) + { + int i = state.ConsoleManager.CaretPosition - 1; + + if (i < 0) + { + continue; + } + + bool letterMode = char.IsLetterOrDigit(_inputBuffer[i]); + + for (; i > 0 && (char.IsLetterOrDigit(_inputBuffer[i]) == letterMode); --i) + { + } + + if (letterMode && i > 0) + { + ++i; + } + + if (i > -1) + { + state.ConsoleManager.MoveCaret(i - state.ConsoleManager.CaretPosition); + } + } + //Move forward a word + else if (keyPress.Key == ConsoleKey.F) + { + int i = state.ConsoleManager.CaretPosition + 1; + + if (i >= _inputBuffer.Count) + { + continue; + } + + bool letterMode = char.IsLetterOrDigit(_inputBuffer[i]); + + for (; i < _inputBuffer.Count && (char.IsLetterOrDigit(_inputBuffer[i]) == letterMode); ++i) + { + } + + if (letterMode && i < _inputBuffer.Count - 1 && i > 0) + { + --i; + } + + state.ConsoleManager.MoveCaret(i - state.ConsoleManager.CaretPosition); + } + } + else if (!keyPress.Modifiers.HasFlag(ConsoleModifiers.Alt) && !keyPress.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + if (state.ConsoleManager.IsKeyAvailable) + { + if (presses == null) + { + presses = new List(); + } + + presses.Add(keyPress); + continue; + } + + if (presses != null) + { + presses.Add(keyPress); + FlushInput(state, ref presses); + continue; + } + + if (state.ConsoleManager.CaretPosition == _inputBuffer.Count) + { + _inputBuffer.Add(keyPress.KeyChar); + state.ConsoleManager.Write(keyPress.KeyChar); + } + else if (IsOverwriteMode) + { + _inputBuffer[state.ConsoleManager.CaretPosition] = keyPress.KeyChar; + state.ConsoleManager.Write(keyPress.KeyChar); + } + else + { + state.ConsoleManager.IsCaretVisible = false; + _inputBuffer.Insert(state.ConsoleManager.CaretPosition, keyPress.KeyChar); + int currentCaretPosition = state.ConsoleManager.CaretPosition; + string s = new string(_inputBuffer.ToArray(), state.ConsoleManager.CaretPosition, _inputBuffer.Count - state.ConsoleManager.CaretPosition); + state.ConsoleManager.Write(s); + state.ConsoleManager.MoveCaret(currentCaretPosition - state.ConsoleManager.CaretPosition + 1); + state.ConsoleManager.IsCaretVisible = true; + } + } + } + } + finally + { + RestoreTtyState(); + } + } + + private void FlushInput(IShellState state, ref List presses) + { + string str = new string(presses.Select(x => x.KeyChar).ToArray()); + + if (state.ConsoleManager.CaretPosition == _inputBuffer.Count) + { + _inputBuffer.AddRange(str); + state.ConsoleManager.Write(str); + } + else if (IsOverwriteMode) + { + for (int i = 0; i < str.Length; ++i) + { + if (state.ConsoleManager.CaretPosition + i < _inputBuffer.Count) + { + _inputBuffer[state.ConsoleManager.CaretPosition + i] = str[i]; + } + else + { + _inputBuffer.AddRange(str.Skip(i)); + break; + } + } + + state.ConsoleManager.Write(str); + } + else + { + state.ConsoleManager.IsCaretVisible = false; + _inputBuffer.InsertRange(state.ConsoleManager.CaretPosition, str); + int currentCaretPosition = state.ConsoleManager.CaretPosition; + string s = new string(_inputBuffer.ToArray(), state.ConsoleManager.CaretPosition, _inputBuffer.Count - state.ConsoleManager.CaretPosition); + state.ConsoleManager.Write(s); + state.ConsoleManager.MoveCaret(currentCaretPosition - state.ConsoleManager.CaretPosition + str.Length); + state.ConsoleManager.IsCaretVisible = true; + } + + presses = null; + } + } +} diff --git a/src/Microsoft.Repl/Input/KeyHandlers.cs b/src/Microsoft.Repl/Input/KeyHandlers.cs new file mode 100644 index 0000000000..7b23f08769 --- /dev/null +++ b/src/Microsoft.Repl/Input/KeyHandlers.cs @@ -0,0 +1,239 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Input +{ + public static class KeyHandlers + { + public static void RegisterDefaultKeyHandlers(IInputManager inputManager) + { + //Navigation in line + inputManager.RegisterKeyHandler(ConsoleKey.LeftArrow, LeftArrow); + inputManager.RegisterKeyHandler(ConsoleKey.RightArrow, RightArrow); + inputManager.RegisterKeyHandler(ConsoleKey.Home, Home); + inputManager.RegisterKeyHandler(ConsoleKey.End, End); + + //Command history + inputManager.RegisterKeyHandler(ConsoleKey.UpArrow, UpArrow); + inputManager.RegisterKeyHandler(ConsoleKey.DownArrow, DownArrow); + + //Completion + inputManager.RegisterKeyHandler(ConsoleKey.Tab, Tab); + + //Input manipulation + inputManager.RegisterKeyHandler(ConsoleKey.Escape, Escape); + inputManager.RegisterKeyHandler(ConsoleKey.Delete, Delete); + inputManager.RegisterKeyHandler(ConsoleKey.Backspace, Backspace); + + //Insert/Overwrite mode + inputManager.RegisterKeyHandler(ConsoleKey.Insert, Insert); + + //Execute command + inputManager.RegisterKeyHandler(ConsoleKey.Enter, Enter); + + //Map non-printable keys that aren't handled by default + inputManager.RegisterKeyHandler(ConsoleKey.F1, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F2, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F3, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F4, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F5, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F6, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F7, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F8, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F9, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F10, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F11, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F12, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F13, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F14, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F15, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F16, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F17, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F18, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F19, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F20, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F21, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F22, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F23, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.F24, Unhandled); + + inputManager.RegisterKeyHandler(ConsoleKey.Applications, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Attention, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserBack, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserFavorites, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserForward, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserHome, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserRefresh, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserSearch, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.BrowserStop, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Clear, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.CrSel, Unhandled); + + inputManager.RegisterKeyHandler(ConsoleKey.EraseEndOfFile, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Execute, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.ExSel, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Help, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.LaunchApp1, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.LaunchApp2, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.LaunchMail, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.LaunchMediaSelect, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.LeftWindows, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.MediaNext, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.MediaPlay, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.MediaPrevious, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.MediaStop, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.NoName, Unhandled); + + inputManager.RegisterKeyHandler(ConsoleKey.Pa1, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Packet, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.PageDown, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.PageUp, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Pause, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Play, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Print, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.PrintScreen, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Process, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.RightWindows, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Select, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Separator, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Sleep, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.VolumeDown, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.VolumeMute, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.VolumeUp, Unhandled); + inputManager.RegisterKeyHandler(ConsoleKey.Zoom, Unhandled); + } + + private static Task End(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + state.ConsoleManager.MoveCaret(state.InputManager.GetCurrentBuffer().Length - state.ConsoleManager.CaretPosition); + return Task.CompletedTask; + } + + public static Task Home(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition); + return Task.CompletedTask; + } + + public static Task LeftArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + if (state.ConsoleManager.CaretPosition > 0) + { + if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + state.ConsoleManager.MoveCaret(-1); + } + else + { + string line = state.InputManager.GetCurrentBuffer(); + ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.ConsoleManager.CaretPosition); + int targetSection = parseResult.SelectedSection - (parseResult.CaretPositionWithinSelectedSection > 0 ? 0 : 1); + + if (targetSection < 0) + { + targetSection = 0; + } + + int desiredPosition = parseResult.SectionStartLookup[targetSection]; + state.ConsoleManager.MoveCaret(desiredPosition - state.ConsoleManager.CaretPosition); + } + } + + return Task.CompletedTask; + } + + public static Task RightArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + string line = state.InputManager.GetCurrentBuffer(); + + if (state.ConsoleManager.CaretPosition < line.Length) + { + if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + state.ConsoleManager.MoveCaret(1); + } + else + { + ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.ConsoleManager.CaretPosition); + int targetSection = parseResult.SelectedSection + 1; + + if (targetSection >= parseResult.Sections.Count) + { + state.ConsoleManager.MoveCaret(line.Length - state.ConsoleManager.CaretPosition); + } + else + { + int desiredPosition = parseResult.SectionStartLookup[targetSection]; + state.ConsoleManager.MoveCaret(desiredPosition - state.ConsoleManager.CaretPosition); + } + } + } + + return Task.CompletedTask; + } + + public static Task UpArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + string line = state.CommandHistory.GetPreviousCommand(); + state.InputManager.SetInput(state, line); + return Task.CompletedTask; + } + + public static Task DownArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + string line = state.CommandHistory.GetNextCommand(); + state.InputManager.SetInput(state, line); + return Task.CompletedTask; + } + + public static Task Enter(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + return state.CommandDispatcher.ExecuteCommandAsync(state, cancellationToken); + } + + public static Task Backspace(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + state.InputManager.RemovePreviousCharacter(state); + return Task.CompletedTask; + } + + public static Task Unhandled(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public static Task Escape(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + state.InputManager.SetInput(state, string.Empty); + return Task.CompletedTask; + } + + public static Task Tab(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift)) + { + state.SuggestionManager.PreviousSuggestion(state); + } + else + { + state.SuggestionManager.NextSuggestion(state); + } + + return Task.CompletedTask; + } + + public static Task Delete(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + state.InputManager.RemoveCurrentCharacter(state); + return Task.CompletedTask; + } + + public static Task Insert(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) + { + state.InputManager.IsOverwriteMode = !state.InputManager.IsOverwriteMode; + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.Repl/Microsoft.Repl.csproj b/src/Microsoft.Repl/Microsoft.Repl.csproj new file mode 100644 index 0000000000..63af949be4 --- /dev/null +++ b/src/Microsoft.Repl/Microsoft.Repl.csproj @@ -0,0 +1,9 @@ + + + + netcoreapp2.2 + A framework for creating REPLs in .NET Core. + dotnet;repl + + + diff --git a/src/Microsoft.Repl/Parsing/CoreParseResult.cs b/src/Microsoft.Repl/Parsing/CoreParseResult.cs new file mode 100644 index 0000000000..8f345813f6 --- /dev/null +++ b/src/Microsoft.Repl/Parsing/CoreParseResult.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Repl.Parsing +{ + public class CoreParseResult : ICoreParseResult + { + public CoreParseResult(int caretPositionWithinCommandText, int caretPositionWithinSelectedSection, string commandText, IReadOnlyList sections, int selectedSection, IReadOnlyDictionary sectionStartLookup, HashSet quotedSections) + { + CaretPositionWithinCommandText = caretPositionWithinCommandText; + CaretPositionWithinSelectedSection = caretPositionWithinSelectedSection; + CommandText = commandText; + Sections = sections; + SelectedSection = selectedSection; + SectionStartLookup = sectionStartLookup; + _quotedSections = quotedSections; + } + + public int CaretPositionWithinCommandText { get; } + + public int CaretPositionWithinSelectedSection { get; } + + public string CommandText { get; } + + public IReadOnlyList Sections { get; } + + public int SelectedSection { get; } + + public IReadOnlyDictionary SectionStartLookup { get; } + + private readonly HashSet _quotedSections; + + public bool IsQuotedSection(int index) + { + return _quotedSections.Contains(index); + } + + public virtual ICoreParseResult Slice(int numberOfLeadingSectionsToRemove) + { + if (numberOfLeadingSectionsToRemove == 0) + { + return this; + } + + if (numberOfLeadingSectionsToRemove >= Sections.Count) + { + return new CoreParseResult(0, 0, string.Empty, new[] { string.Empty }, 0, new Dictionary { { 0, 0 } }, new HashSet()); + } + + string commandText = CommandText.Substring(SectionStartLookup[numberOfLeadingSectionsToRemove]); + int caretPositionWithinCommandText = CaretPositionWithinCommandText - SectionStartLookup[numberOfLeadingSectionsToRemove]; + + if (caretPositionWithinCommandText < 0) + { + caretPositionWithinCommandText = 0; + } + + Dictionary sectionStartLookup = new Dictionary(); + List sections = new List(); + for (int i = 0; i < Sections.Count - numberOfLeadingSectionsToRemove; ++i) + { + sectionStartLookup[i] = SectionStartLookup[numberOfLeadingSectionsToRemove + i] - SectionStartLookup[numberOfLeadingSectionsToRemove]; + sections.Add(Sections[numberOfLeadingSectionsToRemove + i]); + } + + int selectedSection = SelectedSection - numberOfLeadingSectionsToRemove; + + if (selectedSection < 0) + { + selectedSection = 0; + } + + HashSet quotedSections = new HashSet(_quotedSections.Where(x => x > 0).Select(x => x - 1)); + return new CoreParseResult(caretPositionWithinCommandText, CaretPositionWithinSelectedSection, commandText, sections, selectedSection, sectionStartLookup, quotedSections); + } + } +} diff --git a/src/Microsoft.Repl/Parsing/CoreParser.cs b/src/Microsoft.Repl/Parsing/CoreParser.cs new file mode 100644 index 0000000000..35be4867a5 --- /dev/null +++ b/src/Microsoft.Repl/Parsing/CoreParser.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Repl.Parsing +{ + public class CoreParser : IParser + { + public ICoreParseResult Parse(string commandText, int caretPosition) + { + List sections = commandText.Split(' ').ToList(); + Dictionary sectionStartLookup = new Dictionary(); + HashSet quotedSections = new HashSet(); + int runningIndex = 0; + int selectedSection = -1; + int caretPositionWithinSelectedSection = 0; + bool isInQuotedSection = false; + + for (int i = 0; i < sections.Count; ++i) + { + int thisSectionLength = sections[i].Length; + bool isLastSection = i == sections.Count - 1; + + //If currently in a quoted section, combine with the previous section, check to see if this section closes the quotes + if (isInQuotedSection) + { + //Combine with the previous section + sections[i - 1] += " " + sections[i]; + sections.RemoveAt(i--); + + //Check for the closing quote + int sectionLength = sections[i].Length; + if (sections[i][sectionLength - 1] == '"') + { + if (sectionLength > 1 && sections[i][sectionLength - 2] != '\\') + { + isInQuotedSection = false; + } + } + } + //Not in a quoted section, check to see if we're starting one + else + { + sectionStartLookup[i] = runningIndex; + + if (sections[i].Length > 0) + { + if (sections[i][0] == '"') + { + isInQuotedSection = true; + } + } + } + + //Update the running index, adding one for all but the last element to account for the spaces between the sections + runningIndex += thisSectionLength + (isLastSection ? 0 : 1); + + //If the selected section hasn't been determined yet, and the end of the text is past the caret, set the selected + // section to the current section and set the initial value for the caret position within the selected section. + // Note that the caret position within the selected section, unlike the other positions, accounts for escape + // sequences and must be fixed up when escape sequences are removed + if (selectedSection == -1 && runningIndex > caretPosition) + { + selectedSection = i; + caretPositionWithinSelectedSection = caretPosition - sectionStartLookup[i]; + } + } + + //Unescape the sections + // Note that this isn't combined with the above loop to avoid additional complexity in the quoted section case + for (int i = 0; i < sections.Count; ++i) + { + string s = sections[i]; + + //Trim quotes if needed + if (s.Length > 1) + { + if (s[0] == s[s.Length - 1] && s[0] == '"') + { + s = s.Substring(1, s.Length - 2); + quotedSections.Add(i); + + //Fix up the caret position in the text + if (selectedSection == i) + { + //If the caret was on the closing quote, back up to the last character of the section + if (caretPositionWithinSelectedSection == s.Length - 1) + { + caretPositionWithinSelectedSection -= 2; + } + //If the caret was after the opening quote, back up one + else if (caretPositionWithinSelectedSection > 0) + { + --caretPositionWithinSelectedSection; + } + } + } + } + + for (int j = 0; j < s.Length - 1; ++j) + { + if (s[j] == '\\') + { + if (s[j + 1] == '\\' || s[j + 1] == '"') + { + s = s.Substring(0, j) + s.Substring(j + 1); + + //If we're changing the selected section, and we're removing a character + // from before the caret position, back the caret position up to account for it + if (selectedSection == i && j < caretPositionWithinSelectedSection) + { + --caretPositionWithinSelectedSection; + } + } + } + } + + sections[i] = s; + } + + if (selectedSection == -1) + { + selectedSection = sections.Count - 1; + caretPositionWithinSelectedSection = sections[selectedSection].Length; + } + + return new CoreParseResult(caretPosition, caretPositionWithinSelectedSection, commandText, sections, selectedSection, sectionStartLookup, quotedSections); + } + } +} diff --git a/src/Microsoft.Repl/Parsing/ICoreParseResult.cs b/src/Microsoft.Repl/Parsing/ICoreParseResult.cs new file mode 100644 index 0000000000..20ac2c7b4e --- /dev/null +++ b/src/Microsoft.Repl/Parsing/ICoreParseResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Microsoft.Repl.Parsing +{ + public interface ICoreParseResult + { + int CaretPositionWithinCommandText { get; } + + int CaretPositionWithinSelectedSection { get; } + + string CommandText { get; } + + IReadOnlyList Sections { get; } + + bool IsQuotedSection(int index); + + int SelectedSection { get; } + + IReadOnlyDictionary SectionStartLookup { get; } + + ICoreParseResult Slice(int numberOfLeadingSectionsToRemove); + } +} diff --git a/src/Microsoft.Repl/Parsing/IParser.cs b/src/Microsoft.Repl/Parsing/IParser.cs new file mode 100644 index 0000000000..c909f7d94f --- /dev/null +++ b/src/Microsoft.Repl/Parsing/IParser.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Repl.Parsing +{ + public interface IParser + { + ICoreParseResult Parse(string commandText, int caretPosition); + } + + public interface IParser : IParser + { + new TParseResult Parse(string commandText, int caretPosition); + } +} diff --git a/src/Microsoft.Repl/Scripting/IScriptExecutor.cs b/src/Microsoft.Repl/Scripting/IScriptExecutor.cs new file mode 100644 index 0000000000..7d832d8306 --- /dev/null +++ b/src/Microsoft.Repl/Scripting/IScriptExecutor.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Repl.Scripting +{ + public interface IScriptExecutor + { + Task ExecuteScriptAsync(IShellState shellState, IEnumerable commandTexts, CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Repl/Scripting/ScriptExecutor.cs b/src/Microsoft.Repl/Scripting/ScriptExecutor.cs new file mode 100644 index 0000000000..3ec4bf5d21 --- /dev/null +++ b/src/Microsoft.Repl/Scripting/ScriptExecutor.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Scripting +{ + public class ScriptExecutor : IScriptExecutor + where TParseResult : ICoreParseResult + { + private readonly bool _hideScriptLinesFromHistory; + + public ScriptExecutor(bool hideScriptLinesFromHistory = true) + { + _hideScriptLinesFromHistory = hideScriptLinesFromHistory; + } + + public async Task ExecuteScriptAsync(IShellState shellState, IEnumerable commandTexts, CancellationToken cancellationToken) + { + if (shellState.CommandDispatcher is ICommandDispatcher dispatcher) + { + IDisposable suppressor = _hideScriptLinesFromHistory ? shellState.CommandHistory.SuspendHistory() : null; + + using (suppressor) + { + foreach (string commandText in commandTexts) + { + if (string.IsNullOrWhiteSpace(commandText)) + { + continue; + } + + if (cancellationToken.IsCancellationRequested) + { + break; + } + + dispatcher.OnReady(shellState); + shellState.ConsoleManager.ResetCommandStart(); + shellState.InputManager.SetInput(shellState, commandText); + await dispatcher.ExecuteCommandAsync(shellState, cancellationToken).ConfigureAwait(false); + } + } + } + } + } +} diff --git a/src/Microsoft.Repl/Shell.cs b/src/Microsoft.Repl/Shell.cs new file mode 100644 index 0000000000..49f450eae0 --- /dev/null +++ b/src/Microsoft.Repl/Shell.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Repl.Commanding; +using Microsoft.Repl.Input; +using Microsoft.Repl.Suggestions; + +namespace Microsoft.Repl +{ + public class Shell + { + public Shell(IShellState shellState) + { + KeyHandlers.RegisterDefaultKeyHandlers(shellState.InputManager); + ShellState = shellState; + } + + public Shell(ICommandDispatcher dispatcher, ISuggestionManager suggestionManager = null) + : this(new ShellState(dispatcher, suggestionManager)) + { + } + + public IShellState ShellState { get; } + + public Task RunAsync(CancellationToken cancellationToken) + { + ShellState.CommandDispatcher.OnReady(ShellState); + return ShellState.InputManager.StartAsync(ShellState, cancellationToken); + } + } +} diff --git a/src/Microsoft.Repl/ShellState.cs b/src/Microsoft.Repl/ShellState.cs new file mode 100644 index 0000000000..ef0ccf2a64 --- /dev/null +++ b/src/Microsoft.Repl/ShellState.cs @@ -0,0 +1,31 @@ +using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; +using Microsoft.Repl.Input; +using Microsoft.Repl.Suggestions; + +namespace Microsoft.Repl +{ + public class ShellState : IShellState + { + public ShellState(ICommandDispatcher commandDispatcher, ISuggestionManager suggestionManager = null, IInputManager inputManager = null, ICommandHistory commandHistory = null, IConsoleManager consoleManager = null) + { + InputManager = inputManager ?? new InputManager(); + CommandHistory = commandHistory ?? new CommandHistory(); + ConsoleManager = consoleManager ?? new ConsoleManager(); + CommandDispatcher = commandDispatcher; + SuggestionManager = suggestionManager ?? new SuggestionManager(); + } + + public IInputManager InputManager { get; } + + public ICommandHistory CommandHistory { get; } + + public IConsoleManager ConsoleManager { get; } + + public ICommandDispatcher CommandDispatcher { get; } + + public bool IsExiting { get; set; } + + public ISuggestionManager SuggestionManager { get; } + } +} diff --git a/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs b/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs new file mode 100644 index 0000000000..71237fcd27 --- /dev/null +++ b/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Repl.Suggestions +{ + public static class FileSystemCompletion + { + public static IEnumerable GetCompletions(string prefix) + { + if (prefix.StartsWith('\"')) + { + prefix = prefix.Substring(1); + + int lastQuote = prefix.LastIndexOf('\"'); + + if (lastQuote > -1) + { + prefix = prefix.Remove(lastQuote, 1); + } + + while (prefix.EndsWith($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}")) + { + prefix = prefix.Substring(0, prefix.Length - 1); + } + } + + int lastPathIndex = prefix.LastIndexOfAny(new[] { '\\', '/' }); + if (lastPathIndex < 0) + { + return null; + } + + string dir = prefix.Substring(0, lastPathIndex + 1); + + if (dir.IndexOfAny(Path.GetInvalidPathChars()) > -1) + { + return null; + } + + string partPrefix = prefix.Substring(lastPathIndex + 1); + if (Directory.Exists(dir)) + { + return Directory.EnumerateDirectories(dir).Where(x => Path.GetFileName(x).StartsWith(partPrefix, StringComparison.OrdinalIgnoreCase)) + .Union(Directory.EnumerateFiles(dir).Where(x => Path.GetFileName(x).StartsWith(partPrefix, StringComparison.OrdinalIgnoreCase))).Select(x => x.IndexOf(' ') > -1 ? $"\"{x}\"" : x); + } + + return null; + } + } +} diff --git a/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs b/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs new file mode 100644 index 0000000000..f7e11a06fb --- /dev/null +++ b/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Repl.Suggestions +{ + public interface ISuggestionManager + { + void NextSuggestion(IShellState shellState); + + void PreviousSuggestion(IShellState shellState); + } +} diff --git a/src/Microsoft.Repl/Suggestions/SuggestionManager.cs b/src/Microsoft.Repl/Suggestions/SuggestionManager.cs new file mode 100644 index 0000000000..1fd9e861da --- /dev/null +++ b/src/Microsoft.Repl/Suggestions/SuggestionManager.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Microsoft.Repl.Parsing; + +namespace Microsoft.Repl.Suggestions +{ + public class SuggestionManager : ISuggestionManager + { + private int _currentSuggestion; + private IReadOnlyList _suggestions; + private ICoreParseResult _expected; + + public void NextSuggestion(IShellState shellState) + { + string line = shellState.InputManager.GetCurrentBuffer(); + ICoreParseResult parseResult = shellState.CommandDispatcher.Parser.Parse(line, shellState.ConsoleManager.CaretPosition); + string currentSuggestion; + + //Check to see if we're continuing before querying for suggestions again + if (_expected != null + && string.Equals(_expected.CommandText, parseResult.CommandText, StringComparison.Ordinal) + && _expected.SelectedSection == parseResult.SelectedSection + && _expected.CaretPositionWithinSelectedSection == parseResult.CaretPositionWithinSelectedSection) + { + if (_suggestions == null || _suggestions.Count == 0) + { + return; + } + + _currentSuggestion = (_currentSuggestion + 1) % _suggestions.Count; + currentSuggestion = _suggestions[_currentSuggestion]; + } + else + { + _currentSuggestion = 0; + _suggestions = shellState.CommandDispatcher.CollectSuggesetions(shellState); + + if (_suggestions == null || _suggestions.Count == 0) + { + return; + } + + currentSuggestion = _suggestions[0]; + } + + //We now have a suggestion, take the command text leading up to the section being suggested for, + // concatenate that and the suggestion text, turn it in to keys, submit it to the input manager, + // reset the caret, store the parse result of the new text as what's expected for a continuation + string newText = parseResult.CommandText.Substring(0, parseResult.SectionStartLookup[parseResult.SelectedSection]) + currentSuggestion; + _expected = shellState.CommandDispatcher.Parser.Parse(newText, newText.Length); + shellState.InputManager.SetInput(shellState, newText); + } + + public void PreviousSuggestion(IShellState shellState) + { + string line = shellState.InputManager.GetCurrentBuffer(); + ICoreParseResult parseResult = shellState.CommandDispatcher.Parser.Parse(line, shellState.ConsoleManager.CaretPosition); + string currentSuggestion; + + //Check to see if we're continuing before querying for suggestions again + if (_expected != null + && string.Equals(_expected.CommandText, parseResult.CommandText, StringComparison.Ordinal) + && _expected.SelectedSection == parseResult.SelectedSection + && _expected.CaretPositionWithinSelectedSection == parseResult.CaretPositionWithinSelectedSection) + { + if (_suggestions == null || _suggestions.Count == 0) + { + return; + } + + _currentSuggestion = (_currentSuggestion - 1 + _suggestions.Count) % _suggestions.Count; + currentSuggestion = _suggestions[_currentSuggestion]; + } + else + { + _suggestions = shellState.CommandDispatcher.CollectSuggesetions(shellState); + _currentSuggestion = _suggestions.Count - 1; + + if (_suggestions == null || _suggestions.Count == 0) + { + return; + } + + currentSuggestion = _suggestions[_suggestions.Count - 1]; + } + + //We now have a suggestion, take the command text leading up to the section being suggested for, + // concatenate that and the suggestion text, turn it in to keys, submit it to the input manager, + // reset the caret, store the parse result of the new text as what's expected for a continuation + string newText = parseResult.CommandText.Substring(0, parseResult.SectionStartLookup[parseResult.SelectedSection]) + currentSuggestion; + _expected = shellState.CommandDispatcher.Parser.Parse(newText, newText.Length); + shellState.InputManager.SetInput(shellState, newText); + } + } +} diff --git a/src/Microsoft.Repl/Utils.cs b/src/Microsoft.Repl/Utils.cs new file mode 100644 index 0000000000..37ffcd97fb --- /dev/null +++ b/src/Microsoft.Repl/Utils.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Microsoft.Repl +{ + public static class Utils + { + public static string Stringify(this IReadOnlyList keys) + { + return string.Join("", keys); + } + } +} diff --git a/test/Microsoft.HttpRepl.Tests/JsonVisitorTests.cs b/test/Microsoft.HttpRepl.Tests/JsonVisitorTests.cs new file mode 100644 index 0000000000..30155b022f --- /dev/null +++ b/test/Microsoft.HttpRepl.Tests/JsonVisitorTests.cs @@ -0,0 +1,60 @@ +using Microsoft.HttpRepl.Formatting; +using Microsoft.HttpRepl.Preferences; +using Microsoft.Repl.ConsoleHandling; +using Xunit; + +namespace Microsoft.HttpRepl.Tests +{ + public class JsonVisitorTests + { + [Fact] + public void JsonVisitor_ObjectWithComments() + { + string testData = @"[ + { + //Object 1 + ""property"": ""value"", + ""and"": ""again"" + }, + { + //Object 2 + }, + [ + //An array + ], + null, + 1, + 3.2, + ""test"", + false +]"; + + string formatted = JsonVisitor.FormatAndColorize(new MockJsonConfig(), testData); + } + + private class MockJsonConfig : IJsonConfig + { + public int IndentSize => 2; + + public AllowedColors DefaultColor => AllowedColors.None; + + public AllowedColors ArrayBraceColor => AllowedColors.None; + + public AllowedColors ObjectBraceColor => AllowedColors.None; + + public AllowedColors CommaColor => AllowedColors.None; + + public AllowedColors NameColor => AllowedColors.None; + + public AllowedColors NameSeparatorColor => AllowedColors.None; + + public AllowedColors BoolColor => AllowedColors.None; + + public AllowedColors NumericColor => AllowedColors.None; + + public AllowedColors StringColor => AllowedColors.None; + + public AllowedColors NullColor => AllowedColors.None; + } + } +} diff --git a/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj b/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj new file mode 100644 index 0000000000..7a79d0121e --- /dev/null +++ b/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.2 + + false + + + + + + + + + + + + + + + diff --git a/test/Microsoft.Repl.Tests/Microsoft.Repl.Tests.csproj b/test/Microsoft.Repl.Tests/Microsoft.Repl.Tests.csproj new file mode 100644 index 0000000000..52fbd0de16 --- /dev/null +++ b/test/Microsoft.Repl.Tests/Microsoft.Repl.Tests.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.2 + + false + + + + + + + + + + + + + diff --git a/test/Microsoft.Repl.Tests/ParserTests.cs b/test/Microsoft.Repl.Tests/ParserTests.cs new file mode 100644 index 0000000000..422123db85 --- /dev/null +++ b/test/Microsoft.Repl.Tests/ParserTests.cs @@ -0,0 +1,26 @@ +using Microsoft.Repl.Parsing; +using Xunit; + +namespace Microsoft.Repl.Tests +{ + public class ParserTests + { + [Fact] + public void ParserTests_Basic() + { + string testString = "\"this is a test\" of \"Escape\\\\Sequences\\\"\""; + + CoreParser parser = new CoreParser(); + ICoreParseResult result = parser.Parse(testString, 29); + + Assert.Equal(3, result.Sections.Count); + Assert.Equal(2, result.SelectedSection); + Assert.Equal(0, result.SectionStartLookup[0]); + Assert.Equal(17, result.SectionStartLookup[1]); + Assert.Equal(20, result.SectionStartLookup[2]); + Assert.Equal(7, result.CaretPositionWithinSelectedSection); + Assert.Equal(29, result.CaretPositionWithinCommandText); + Assert.Equal(testString, result.CommandText); + } + } +} From 7f8f210a24fcebdd661037fd6cf6b1ed4bc1d06c Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Wed, 1 Aug 2018 12:35:27 -0700 Subject: [PATCH 2/9] Add license headers to all files Bump version of JSON.NET to 11.0.2 --- build/dependencies.props | 4 ++-- .../AggregateDirectoryStructure.cs | 5 ++++- .../Commands/BaseHttpCommand.cs | 5 ++++- .../Commands/ChangeDirectoryCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/ClearCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/ConfigCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/DeleteCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/EchoCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/ExitCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/Formatter.cs | 13 +++++-------- src/Microsoft.HttpRepl/Commands/GetCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/HeadCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/HelpCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/ListCommand.cs | 5 ++++- .../Commands/OptionsCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/PatchCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/PostCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/PrefCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/PutCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/RunCommand.cs | 5 ++++- .../Commands/SetBaseCommand.cs | 5 ++++- .../Commands/SetDiagCommand.cs | 5 ++++- .../Commands/SetHeaderCommand.cs | 5 ++++- .../Commands/SetSwaggerCommand.cs | 5 ++++- src/Microsoft.HttpRepl/Commands/TreeNode.cs | 7 +++++-- src/Microsoft.HttpRepl/Commands/UICommand.cs | 5 ++++- src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs | 5 ++++- .../Diagnostics/DiagEndpoint.cs | 5 ++++- .../Diagnostics/DiagEndpointMetadata.cs | 5 ++++- src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs | 5 ++++- .../Diagnostics/DiagnosticsState.cs | 5 ++++- src/Microsoft.HttpRepl/DirectoryStructure.cs | 5 ++++- .../DirectoryStructureExtensions.cs | 5 ++++- src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs | 5 ++++- src/Microsoft.HttpRepl/HttpState.cs | 5 ++++- src/Microsoft.HttpRepl/IDirectoryStructure.cs | 14 ++++---------- src/Microsoft.HttpRepl/IRequestInfo.cs | 16 ++++++++++++++++ src/Microsoft.HttpRepl/OpenApi/Either.cs | 5 ++++- .../OpenApi/EitherConverter.cs | 5 ++++- .../OpenApi/EndpointMetadata.cs | 5 ++++- .../OpenApi/EndpointMetadataReader.cs | 5 ++++- .../OpenApi/IEndpointMetadataReader.cs | 5 ++++- .../OpenApi/OpenApiV3EndpointMetadataReader.cs | 5 ++++- src/Microsoft.HttpRepl/OpenApi/Parameter.cs | 5 ++++- src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs | 5 ++++- src/Microsoft.HttpRepl/OpenApi/Schema.cs | 5 ++++- .../OpenApi/SwaggerV1EndpointMetadataReader.cs | 5 ++++- .../OpenApi/SwaggerV2EndpointMetadataReader.cs | 5 ++++- .../Preferences/IJsonConfig.cs | 7 +++++-- src/Microsoft.HttpRepl/Preferences/JsonConfig.cs | 5 ++++- .../Preferences/RequestConfig.cs | 5 ++++- .../Preferences/RequestOrResponseConfig.cs | 5 ++++- .../Preferences/ResponseConfig.cs | 5 ++++- .../Preferences/WellKnownPreference.cs | 5 ++++- src/Microsoft.HttpRepl/Program.cs | 5 ++++- .../Suggestions/HeaderCompletion.cs | 5 ++++- .../Suggestions/ServerPathCompletion.cs | 5 ++++- src/Microsoft.Repl/Commanding/CommandHistory.cs | 5 ++++- .../Commanding/CommandInputLocation.cs | 7 +++++-- .../Commanding/CommandInputProcessingIssue.cs | 7 +++++-- .../CommandInputProcessingIssueKind.cs | 7 +++++-- .../Commanding/CommandInputSpecification.cs | 7 +++++-- .../CommandInputSpecificationBuilder.cs | 7 +++++-- .../Commanding/CommandOptionSpecification.cs | 7 +++++-- .../Commanding/CommandWithStructuredInputBase.cs | 7 +++++-- .../Commanding/DefaultCommandDispatcher.cs | 5 ++++- .../Commanding/DefaultCommandInput.cs | 7 +++++-- src/Microsoft.Repl/Commanding/ICommand.cs | 5 ++++- .../Commanding/ICommandDispatcher.cs | 5 ++++- src/Microsoft.Repl/Commanding/ICommandHistory.cs | 5 ++++- src/Microsoft.Repl/Commanding/InputElement.cs | 7 +++++-- .../ConsoleHandling/AllowedColors.cs | 7 +++++-- .../ConsoleHandling/AnsiColorExtensions.cs | 5 ++++- .../ConsoleHandling/AnsiConsole.cs | 5 ++++- .../ConsoleHandling/ConsoleManager.cs | 5 ++++- .../ConsoleHandling/IConsoleManager.cs | 5 ++++- src/Microsoft.Repl/ConsoleHandling/IWritable.cs | 5 ++++- src/Microsoft.Repl/ConsoleHandling/Point.cs | 5 ++++- src/Microsoft.Repl/ConsoleHandling/Reporter.cs | 5 ++++- src/Microsoft.Repl/ConsoleHandling/Writable.cs | 5 ++++- src/Microsoft.Repl/Disposable.cs | 5 ++++- src/Microsoft.Repl/IShellState.cs | 5 ++++- src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs | 5 ++++- src/Microsoft.Repl/Input/IInputManager.cs | 5 ++++- src/Microsoft.Repl/Input/InputManager.cs | 5 ++++- src/Microsoft.Repl/Input/KeyHandlers.cs | 5 ++++- src/Microsoft.Repl/Parsing/CoreParseResult.cs | 5 ++++- src/Microsoft.Repl/Parsing/CoreParser.cs | 5 ++++- src/Microsoft.Repl/Parsing/ICoreParseResult.cs | 5 ++++- src/Microsoft.Repl/Parsing/IParser.cs | 5 ++++- src/Microsoft.Repl/Scripting/IScriptExecutor.cs | 5 ++++- src/Microsoft.Repl/Scripting/ScriptExecutor.cs | 5 ++++- src/Microsoft.Repl/Shell.cs | 5 ++++- src/Microsoft.Repl/ShellState.cs | 5 ++++- .../Suggestions/FileSystemCompletion.cs | 5 ++++- .../Suggestions/ISuggestionManager.cs | 5 ++++- .../Suggestions/SuggestionManager.cs | 5 ++++- src/Microsoft.Repl/Utils.cs | 5 ++++- 98 files changed, 415 insertions(+), 126 deletions(-) create mode 100644 src/Microsoft.HttpRepl/IRequestInfo.cs diff --git a/build/dependencies.props b/build/dependencies.props index 0c7d2bca9d..ee0b0b8ccf 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -17,7 +17,7 @@ 2.0.3 4.5.1 4.5.0 - 10.0.1 + 11.0.2 9.0.1 2.3.1 2.4.0 diff --git a/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs b/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs index ed763a3e8f..19dcb289f8 100644 --- a/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs +++ b/src/Microsoft.HttpRepl/AggregateDirectoryStructure.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; diff --git a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs index 3db2f39b7b..374544790b 100644 --- a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Diagnostics; using System.IO; diff --git a/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs b/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs index 3a983698cf..c2baaf0de5 100644 --- a/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ChangeDirectoryCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.HttpRepl/Commands/ClearCommand.cs b/src/Microsoft.HttpRepl/Commands/ClearCommand.cs index a695dfc544..4244946c95 100644 --- a/src/Microsoft.HttpRepl/Commands/ClearCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ClearCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs b/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs index 56003280ab..773bee0f04 100644 --- a/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ConfigCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Net.Http; diff --git a/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs b/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs index a861b35142..7c9b789db3 100644 --- a/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/DeleteCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class DeleteCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/EchoCommand.cs b/src/Microsoft.HttpRepl/Commands/EchoCommand.cs index 19eb33788b..691eb5679f 100644 --- a/src/Microsoft.HttpRepl/Commands/EchoCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/EchoCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.HttpRepl/Commands/ExitCommand.cs b/src/Microsoft.HttpRepl/Commands/ExitCommand.cs index ebc0635876..af76ad95df 100644 --- a/src/Microsoft.HttpRepl/Commands/ExitCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ExitCommand.cs @@ -1,4 +1,7 @@ -using System.Threading; +// 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.Threading; using System.Threading.Tasks; using Microsoft.Repl; using Microsoft.Repl.Commanding; diff --git a/src/Microsoft.HttpRepl/Commands/Formatter.cs b/src/Microsoft.HttpRepl/Commands/Formatter.cs index 5a450e3406..d1783dc357 100644 --- a/src/Microsoft.HttpRepl/Commands/Formatter.cs +++ b/src/Microsoft.HttpRepl/Commands/Formatter.cs @@ -1,18 +1,15 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class Formatter { - //private readonly List _prefix = new List(); private int _prefix; private int _maxDepth; public void RegisterEntry(int prefixLength, int depth) { - //while (_prefix.Count < depth + 1) - //{ - // _prefix.Add(0); - //} - if (depth > _maxDepth) { _maxDepth = depth; @@ -30,4 +27,4 @@ return (indent + prefix).PadRight(_prefix + 3 + _maxDepth * 4) + entry; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/Commands/GetCommand.cs b/src/Microsoft.HttpRepl/Commands/GetCommand.cs index 76765ce374..e5441cecd1 100644 --- a/src/Microsoft.HttpRepl/Commands/GetCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/GetCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class GetCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/HeadCommand.cs b/src/Microsoft.HttpRepl/Commands/HeadCommand.cs index 97023f5860..fbd3de4971 100644 --- a/src/Microsoft.HttpRepl/Commands/HeadCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/HeadCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class HeadCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs index de909a4906..f205fc1243 100644 --- a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.HttpRepl/Commands/ListCommand.cs b/src/Microsoft.HttpRepl/Commands/ListCommand.cs index 078a20d398..b1aa0718a3 100644 --- a/src/Microsoft.HttpRepl/Commands/ListCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/ListCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Globalization; using System.Linq; diff --git a/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs b/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs index f6a3c8903c..5da5cadfaf 100644 --- a/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/OptionsCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class OptionsCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/PatchCommand.cs b/src/Microsoft.HttpRepl/Commands/PatchCommand.cs index c5d9c875be..bfb9cc9192 100644 --- a/src/Microsoft.HttpRepl/Commands/PatchCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/PatchCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class PatchCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/PostCommand.cs b/src/Microsoft.HttpRepl/Commands/PostCommand.cs index 216fe8ef1d..61edb6d4c8 100644 --- a/src/Microsoft.HttpRepl/Commands/PostCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/PostCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class PostCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/PrefCommand.cs b/src/Microsoft.HttpRepl/Commands/PrefCommand.cs index 7f16af09b3..2a64d2f017 100644 --- a/src/Microsoft.HttpRepl/Commands/PrefCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/PrefCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.HttpRepl/Commands/PutCommand.cs b/src/Microsoft.HttpRepl/Commands/PutCommand.cs index a65439dfb7..4f37e1776c 100644 --- a/src/Microsoft.HttpRepl/Commands/PutCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/PutCommand.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Commands +// 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. + +namespace Microsoft.HttpRepl.Commands { public class PutCommand : BaseHttpCommand { diff --git a/src/Microsoft.HttpRepl/Commands/RunCommand.cs b/src/Microsoft.HttpRepl/Commands/RunCommand.cs index 6dd2f4a378..3228dbccf1 100644 --- a/src/Microsoft.HttpRepl/Commands/RunCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/RunCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.IO; using System.Threading; diff --git a/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs b/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs index c346f541d4..77e5f0adde 100644 --- a/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetBaseCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs b/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs index 4e86452b7c..535d1a2026 100644 --- a/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetDiagCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Net.Http; diff --git a/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs b/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs index c95d22a6be..3db62f47b3 100644 --- a/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetHeaderCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs b/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs index 9c9a1f36cc..be99891d5e 100644 --- a/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/SetSwaggerCommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.IO; using System.Linq; diff --git a/src/Microsoft.HttpRepl/Commands/TreeNode.cs b/src/Microsoft.HttpRepl/Commands/TreeNode.cs index 256d5d9dae..1ae5db3cbc 100644 --- a/src/Microsoft.HttpRepl/Commands/TreeNode.cs +++ b/src/Microsoft.HttpRepl/Commands/TreeNode.cs @@ -1,4 +1,7 @@ -using System; +// 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.HttpRepl.Commands @@ -44,4 +47,4 @@ namespace Microsoft.HttpRepl.Commands return self + Environment.NewLine + string.Join(Environment.NewLine, _children); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/Commands/UICommand.cs b/src/Microsoft.HttpRepl/Commands/UICommand.cs index ddf4ca4097..0eb02826f9 100644 --- a/src/Microsoft.HttpRepl/Commands/UICommand.cs +++ b/src/Microsoft.HttpRepl/Commands/UICommand.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Diagnostics; using System.Runtime.InteropServices; diff --git a/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs b/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs index ba16701d58..8e5c2943e4 100644 --- a/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs +++ b/src/Microsoft.HttpRepl/Diagnostics/ConfigItem.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Diagnostics +// 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. + +namespace Microsoft.HttpRepl.Diagnostics { public class ConfigItem { diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs index 80e619dcc3..a38e50bada 100644 --- a/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpoint.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Diagnostics +// 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. + +namespace Microsoft.HttpRepl.Diagnostics { public class DiagEndpoint { diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs index 53e5aa3918..5989daf094 100644 --- a/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagEndpointMetadata.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Diagnostics +// 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. + +namespace Microsoft.HttpRepl.Diagnostics { public class DiagEndpointMetadata { diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs index c0fbf2df9c..a9d0692cfd 100644 --- a/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagItem.cs @@ -1,4 +1,7 @@ -namespace Microsoft.HttpRepl.Diagnostics +// 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. + +namespace Microsoft.HttpRepl.Diagnostics { public class DiagItem { diff --git a/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs b/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs index 3fe27cce19..90033d9062 100644 --- a/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs +++ b/src/Microsoft.HttpRepl/Diagnostics/DiagnosticsState.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.HttpRepl.Diagnostics { diff --git a/src/Microsoft.HttpRepl/DirectoryStructure.cs b/src/Microsoft.HttpRepl/DirectoryStructure.cs index f682488b9b..8787f962fc 100644 --- a/src/Microsoft.HttpRepl/DirectoryStructure.cs +++ b/src/Microsoft.HttpRepl/DirectoryStructure.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; diff --git a/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs b/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs index 2b7ad5341a..822fa41e6f 100644 --- a/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs +++ b/src/Microsoft.HttpRepl/DirectoryStructureExtensions.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Linq; namespace Microsoft.HttpRepl diff --git a/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs b/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs index fa40ee62ab..2538e2fdf2 100644 --- a/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs +++ b/src/Microsoft.HttpRepl/Formatting/JsonVisitor.cs @@ -1,4 +1,7 @@ -using System.IO; +// 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.IO; using System.Text; using Microsoft.HttpRepl.Preferences; using Microsoft.Repl.ConsoleHandling; diff --git a/src/Microsoft.HttpRepl/HttpState.cs b/src/Microsoft.HttpRepl/HttpState.cs index 55cfdd6188..d012a8fc45 100644 --- a/src/Microsoft.HttpRepl/HttpState.cs +++ b/src/Microsoft.HttpRepl/HttpState.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.IO; using System.Linq; diff --git a/src/Microsoft.HttpRepl/IDirectoryStructure.cs b/src/Microsoft.HttpRepl/IDirectoryStructure.cs index 5c02e8e476..e3641e3ca8 100644 --- a/src/Microsoft.HttpRepl/IDirectoryStructure.cs +++ b/src/Microsoft.HttpRepl/IDirectoryStructure.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.HttpRepl { @@ -12,13 +15,4 @@ namespace Microsoft.HttpRepl IRequestInfo RequestInfo { get; } } - - public interface IRequestInfo - { - IReadOnlyDictionary> ContentTypesByMethod { get; } - - IReadOnlyList Methods { get; } - - string GetRequestBodyForContentType(string contentType, string method); - } } diff --git a/src/Microsoft.HttpRepl/IRequestInfo.cs b/src/Microsoft.HttpRepl/IRequestInfo.cs new file mode 100644 index 0000000000..24054f6142 --- /dev/null +++ b/src/Microsoft.HttpRepl/IRequestInfo.cs @@ -0,0 +1,16 @@ +// 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.Collections.Generic; + +namespace Microsoft.HttpRepl +{ + public interface IRequestInfo + { + IReadOnlyDictionary> ContentTypesByMethod { get; } + + IReadOnlyList Methods { get; } + + string GetRequestBodyForContentType(string contentType, string method); + } +} diff --git a/src/Microsoft.HttpRepl/OpenApi/Either.cs b/src/Microsoft.HttpRepl/OpenApi/Either.cs index f1e7dfacb3..8e3069de89 100644 --- a/src/Microsoft.HttpRepl/OpenApi/Either.cs +++ b/src/Microsoft.HttpRepl/OpenApi/Either.cs @@ -1,3 +1,6 @@ +// 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. + namespace Microsoft.HttpRepl.OpenApi { public class Either @@ -30,4 +33,4 @@ namespace Microsoft.HttpRepl.OpenApi return new Either(value); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs b/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs index b89a85c9ef..a7197abc8b 100644 --- a/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs +++ b/src/Microsoft.HttpRepl/OpenApi/EitherConverter.cs @@ -1,3 +1,6 @@ +// 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 Newtonsoft.Json; @@ -29,4 +32,4 @@ namespace Microsoft.HttpRepl.OpenApi throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs index e46387b3e0..27bb4d1b35 100644 --- a/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs +++ b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadata.cs @@ -1,3 +1,6 @@ +// 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.Collections.Generic; namespace Microsoft.HttpRepl.OpenApi @@ -14,4 +17,4 @@ namespace Microsoft.HttpRepl.OpenApi public IReadOnlyDictionary>> AvailableRequests { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs index 8635d6fbee..243b8fa92e 100644 --- a/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs +++ b/src/Microsoft.HttpRepl/OpenApi/EndpointMetadataReader.cs @@ -1,3 +1,6 @@ +// 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.Collections.Generic; using Newtonsoft.Json.Linq; @@ -35,4 +38,4 @@ namespace Microsoft.HttpRepl.OpenApi return null; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs index 287740c7af..385f9dfce0 100644 --- a/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs +++ b/src/Microsoft.HttpRepl/OpenApi/IEndpointMetadataReader.cs @@ -1,3 +1,6 @@ +// 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.Collections.Generic; using Newtonsoft.Json.Linq; @@ -9,4 +12,4 @@ namespace Microsoft.HttpRepl.OpenApi IEnumerable ReadMetadata(JObject document); } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs index 5fa968efc6..c009951608 100644 --- a/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs +++ b/src/Microsoft.HttpRepl/OpenApi/OpenApiV3EndpointMetadataReader.cs @@ -1,3 +1,6 @@ +// 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; using System.Linq; @@ -95,4 +98,4 @@ namespace Microsoft.HttpRepl.OpenApi return metadata; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/Parameter.cs b/src/Microsoft.HttpRepl/OpenApi/Parameter.cs index faf21e5f9a..0a44f489c5 100644 --- a/src/Microsoft.HttpRepl/OpenApi/Parameter.cs +++ b/src/Microsoft.HttpRepl/OpenApi/Parameter.cs @@ -1,3 +1,6 @@ +// 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. + namespace Microsoft.HttpRepl.OpenApi { public class Parameter @@ -10,4 +13,4 @@ namespace Microsoft.HttpRepl.OpenApi public Schema Schema { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs index b01730b116..db36c082bf 100644 --- a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs +++ b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs @@ -1,3 +1,6 @@ +// 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.Globalization; using System.Linq; @@ -215,4 +218,4 @@ namespace Microsoft.HttpRepl.OpenApi // return root; //} } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/Schema.cs b/src/Microsoft.HttpRepl/OpenApi/Schema.cs index 028c0b13ca..c84bbdda0f 100644 --- a/src/Microsoft.HttpRepl/OpenApi/Schema.cs +++ b/src/Microsoft.HttpRepl/OpenApi/Schema.cs @@ -1,3 +1,6 @@ +// 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.Collections.Generic; using System.Linq; using Newtonsoft.Json; @@ -122,4 +125,4 @@ namespace Microsoft.HttpRepl.OpenApi public bool UniqueItems { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs index 7f4fef4fb1..d62fc8c3b1 100644 --- a/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs +++ b/src/Microsoft.HttpRepl/OpenApi/SwaggerV1EndpointMetadataReader.cs @@ -1,3 +1,6 @@ +// 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; using System.Linq; @@ -103,4 +106,4 @@ namespace Microsoft.HttpRepl.OpenApi return metadata; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs b/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs index 5e7ecfe6aa..66b16d2ed5 100644 --- a/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs +++ b/src/Microsoft.HttpRepl/OpenApi/SwaggerV2EndpointMetadataReader.cs @@ -1,3 +1,6 @@ +// 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; using System.Linq; @@ -84,4 +87,4 @@ namespace Microsoft.HttpRepl.OpenApi return metadata; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs b/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs index 21918e44ef..96d36ad7af 100644 --- a/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs +++ b/src/Microsoft.HttpRepl/Preferences/IJsonConfig.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.ConsoleHandling; +// 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 Microsoft.Repl.ConsoleHandling; namespace Microsoft.HttpRepl.Preferences { @@ -26,4 +29,4 @@ namespace Microsoft.HttpRepl.Preferences AllowedColors NullColor { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs b/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs index a1902e245e..67ce8aa2ed 100644 --- a/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs +++ b/src/Microsoft.HttpRepl/Preferences/JsonConfig.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.ConsoleHandling; +// 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 Microsoft.Repl.ConsoleHandling; namespace Microsoft.HttpRepl.Preferences { diff --git a/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs b/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs index a3f9b55259..ae2b41e1fc 100644 --- a/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs +++ b/src/Microsoft.HttpRepl/Preferences/RequestConfig.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.ConsoleHandling; +// 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 Microsoft.Repl.ConsoleHandling; namespace Microsoft.HttpRepl.Preferences { diff --git a/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs b/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs index c509a89f5e..c4385ad032 100644 --- a/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs +++ b/src/Microsoft.HttpRepl/Preferences/RequestOrResponseConfig.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.ConsoleHandling; +// 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 Microsoft.Repl.ConsoleHandling; namespace Microsoft.HttpRepl.Preferences { diff --git a/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs b/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs index 123a86ff79..d3a6c9c26f 100644 --- a/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs +++ b/src/Microsoft.HttpRepl/Preferences/ResponseConfig.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.ConsoleHandling; +// 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 Microsoft.Repl.ConsoleHandling; namespace Microsoft.HttpRepl.Preferences { diff --git a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs index a6c16a6da7..1cd17a04ce 100644 --- a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs +++ b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Reflection; using Microsoft.Repl.ConsoleHandling; diff --git a/src/Microsoft.HttpRepl/Program.cs b/src/Microsoft.HttpRepl/Program.cs index f64fcf377a..a0e1c9addf 100644 --- a/src/Microsoft.HttpRepl/Program.cs +++ b/src/Microsoft.HttpRepl/Program.cs @@ -1,4 +1,7 @@ -using System.Threading; +// 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.Threading; using System.Threading.Tasks; using Microsoft.Repl; using Microsoft.Repl.Commanding; diff --git a/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs b/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs index 05372d7bec..74ceab2507 100644 --- a/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs +++ b/src/Microsoft.HttpRepl/Suggestions/HeaderCompletion.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; diff --git a/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs b/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs index a50df8ae8d..2311b714b3 100644 --- a/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs +++ b/src/Microsoft.HttpRepl/Suggestions/ServerPathCompletion.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; diff --git a/src/Microsoft.Repl/Commanding/CommandHistory.cs b/src/Microsoft.Repl/Commanding/CommandHistory.cs index 83c7206a9d..27489df0d1 100644 --- a/src/Microsoft.Repl/Commanding/CommandHistory.cs +++ b/src/Microsoft.Repl/Commanding/CommandHistory.cs @@ -1,4 +1,7 @@ -using System; +// 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 diff --git a/src/Microsoft.Repl/Commanding/CommandInputLocation.cs b/src/Microsoft.Repl/Commanding/CommandInputLocation.cs index 1612384b61..e1ed19a50c 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputLocation.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputLocation.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.Commanding +// 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. + +namespace Microsoft.Repl.Commanding { public enum CommandInputLocation { @@ -7,4 +10,4 @@ OptionName, OptionValue } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs index 501b4357da..f294718ff0 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssue.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.Commanding +// 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. + +namespace Microsoft.Repl.Commanding { public class CommandInputProcessingIssue { @@ -12,4 +15,4 @@ Text = text; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs index cd69dccb85..dfc8a5f563 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputProcessingIssueKind.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.Commanding +// 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. + +namespace Microsoft.Repl.Commanding { public enum CommandInputProcessingIssueKind { @@ -8,4 +11,4 @@ OptionUseCountOutOfRange, MissingRequiredOptionInput, } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs b/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs index 370b9f3d34..0e24c3fffa 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputSpecification.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.Repl.Commanding { @@ -41,4 +44,4 @@ namespace Microsoft.Repl.Commanding return new CommandInputSpecificationBuilder(nameParts); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs b/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs index 5ad5edf345..019782d0b8 100644 --- a/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs +++ b/src/Microsoft.Repl/Commanding/CommandInputSpecificationBuilder.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.Repl.Commanding { @@ -63,4 +66,4 @@ namespace Microsoft.Repl.Commanding return new CommandInputSpecification(_name, _optionPreamble, _options, _minimumArgs, _maximumArgs); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs b/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs index d925e5b2c9..6f7e4469fe 100644 --- a/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs +++ b/src/Microsoft.Repl/Commanding/CommandOptionSpecification.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.Repl.Commanding { @@ -26,4 +29,4 @@ namespace Microsoft.Repl.Commanding AcceptsValue = RequiresValue || acceptsValue; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs b/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs index 38443486e8..1957205913 100644 --- a/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs +++ b/src/Microsoft.Repl/Commanding/CommandWithStructuredInputBase.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; @@ -190,4 +193,4 @@ namespace Microsoft.Repl.Commanding protected abstract CommandInputSpecification InputSpec { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs index 932e8cdb1a..2b85c2bc57 100644 --- a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs +++ b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs b/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs index 0d3b97932e..ead59738b3 100644 --- a/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs +++ b/src/Microsoft.Repl/Commanding/DefaultCommandInput.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using Microsoft.Repl.Parsing; @@ -190,4 +193,4 @@ namespace Microsoft.Repl.Commanding public IReadOnlyDictionary> Options { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/Commanding/ICommand.cs b/src/Microsoft.Repl/Commanding/ICommand.cs index be575437e2..3b754924f0 100644 --- a/src/Microsoft.Repl/Commanding/ICommand.cs +++ b/src/Microsoft.Repl/Commanding/ICommand.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Repl.Parsing; diff --git a/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs b/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs index c175490833..d2d9000123 100644 --- a/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs +++ b/src/Microsoft.Repl/Commanding/ICommandDispatcher.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Repl.Parsing; diff --git a/src/Microsoft.Repl/Commanding/ICommandHistory.cs b/src/Microsoft.Repl/Commanding/ICommandHistory.cs index 8e25db8906..cd506fb2b3 100644 --- a/src/Microsoft.Repl/Commanding/ICommandHistory.cs +++ b/src/Microsoft.Repl/Commanding/ICommandHistory.cs @@ -1,4 +1,7 @@ -using System; +// 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; namespace Microsoft.Repl.Commanding { diff --git a/src/Microsoft.Repl/Commanding/InputElement.cs b/src/Microsoft.Repl/Commanding/InputElement.cs index c02c7f0f23..1f0c7cd764 100644 --- a/src/Microsoft.Repl/Commanding/InputElement.cs +++ b/src/Microsoft.Repl/Commanding/InputElement.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.Commanding +// 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. + +namespace Microsoft.Repl.Commanding { public class InputElement { @@ -26,4 +29,4 @@ ParseResultSectionIndex = sectionIndex; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs b/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs index a71dc2c8a4..fd9245db28 100644 --- a/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs +++ b/src/Microsoft.Repl/ConsoleHandling/AllowedColors.cs @@ -1,4 +1,7 @@ -using System; +// 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; namespace Microsoft.Repl.ConsoleHandling { @@ -24,4 +27,4 @@ namespace Microsoft.Repl.ConsoleHandling Bold = 0x100, None = 0x99 } -} \ No newline at end of file +} diff --git a/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs b/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs index cab5bc6f05..8d535bf15f 100644 --- a/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs +++ b/src/Microsoft.Repl/ConsoleHandling/AnsiColorExtensions.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.ConsoleHandling +// 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. + +namespace Microsoft.Repl.ConsoleHandling { public static class AnsiColorExtensions { diff --git a/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs b/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs index 24944f8871..205c7713b5 100644 --- a/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs +++ b/src/Microsoft.Repl/ConsoleHandling/AnsiConsole.cs @@ -1,4 +1,7 @@ -using System; +// 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.IO; namespace Microsoft.Repl.ConsoleHandling diff --git a/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs b/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs index a15c1fd420..ad4231ad5e 100644 --- a/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs +++ b/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; using System.Threading; diff --git a/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs b/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs index 9ab7b26a15..ca93dbe88d 100644 --- a/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs +++ b/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs @@ -1,4 +1,7 @@ -using System; +// 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; namespace Microsoft.Repl.ConsoleHandling diff --git a/src/Microsoft.Repl/ConsoleHandling/IWritable.cs b/src/Microsoft.Repl/ConsoleHandling/IWritable.cs index f18fa123a1..fcb5441a69 100644 --- a/src/Microsoft.Repl/ConsoleHandling/IWritable.cs +++ b/src/Microsoft.Repl/ConsoleHandling/IWritable.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.ConsoleHandling +// 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. + +namespace Microsoft.Repl.ConsoleHandling { public interface IWritable { diff --git a/src/Microsoft.Repl/ConsoleHandling/Point.cs b/src/Microsoft.Repl/ConsoleHandling/Point.cs index 4eeb68989d..e705eada63 100644 --- a/src/Microsoft.Repl/ConsoleHandling/Point.cs +++ b/src/Microsoft.Repl/ConsoleHandling/Point.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.ConsoleHandling +// 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. + +namespace Microsoft.Repl.ConsoleHandling { public struct Point { diff --git a/src/Microsoft.Repl/ConsoleHandling/Reporter.cs b/src/Microsoft.Repl/ConsoleHandling/Reporter.cs index 5b10d2be02..a7da8d0060 100644 --- a/src/Microsoft.Repl/ConsoleHandling/Reporter.cs +++ b/src/Microsoft.Repl/ConsoleHandling/Reporter.cs @@ -1,4 +1,7 @@ -using System; +// 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; namespace Microsoft.Repl.ConsoleHandling { diff --git a/src/Microsoft.Repl/ConsoleHandling/Writable.cs b/src/Microsoft.Repl/ConsoleHandling/Writable.cs index b12c2ad4e7..1b32e939f4 100644 --- a/src/Microsoft.Repl/ConsoleHandling/Writable.cs +++ b/src/Microsoft.Repl/ConsoleHandling/Writable.cs @@ -1,4 +1,7 @@ -using System; +// 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; namespace Microsoft.Repl.ConsoleHandling { diff --git a/src/Microsoft.Repl/Disposable.cs b/src/Microsoft.Repl/Disposable.cs index 78002fb1d9..40b892ace1 100644 --- a/src/Microsoft.Repl/Disposable.cs +++ b/src/Microsoft.Repl/Disposable.cs @@ -1,4 +1,7 @@ -using System; +// 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; namespace Microsoft.Repl { diff --git a/src/Microsoft.Repl/IShellState.cs b/src/Microsoft.Repl/IShellState.cs index 2ec1cb1514..1941fa65f9 100644 --- a/src/Microsoft.Repl/IShellState.cs +++ b/src/Microsoft.Repl/IShellState.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.Commanding; +// 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 Microsoft.Repl.Commanding; using Microsoft.Repl.ConsoleHandling; using Microsoft.Repl.Input; using Microsoft.Repl.Suggestions; diff --git a/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs b/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs index 616a749611..4ac3793979 100644 --- a/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs +++ b/src/Microsoft.Repl/Input/AsyncKeyPressHandler.cs @@ -1,4 +1,7 @@ -using System; +// 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; diff --git a/src/Microsoft.Repl/Input/IInputManager.cs b/src/Microsoft.Repl/Input/IInputManager.cs index 100dd22bea..b67f3e3936 100644 --- a/src/Microsoft.Repl/Input/IInputManager.cs +++ b/src/Microsoft.Repl/Input/IInputManager.cs @@ -1,4 +1,7 @@ -using System; +// 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; diff --git a/src/Microsoft.Repl/Input/InputManager.cs b/src/Microsoft.Repl/Input/InputManager.cs index a635ccd37a..e1d6055683 100644 --- a/src/Microsoft.Repl/Input/InputManager.cs +++ b/src/Microsoft.Repl/Input/InputManager.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Diagnostics; using System.Linq; diff --git a/src/Microsoft.Repl/Input/KeyHandlers.cs b/src/Microsoft.Repl/Input/KeyHandlers.cs index 7b23f08769..192c55319c 100644 --- a/src/Microsoft.Repl/Input/KeyHandlers.cs +++ b/src/Microsoft.Repl/Input/KeyHandlers.cs @@ -1,4 +1,7 @@ -using System; +// 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.Parsing; diff --git a/src/Microsoft.Repl/Parsing/CoreParseResult.cs b/src/Microsoft.Repl/Parsing/CoreParseResult.cs index 8f345813f6..1655e4a36a 100644 --- a/src/Microsoft.Repl/Parsing/CoreParseResult.cs +++ b/src/Microsoft.Repl/Parsing/CoreParseResult.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Linq; diff --git a/src/Microsoft.Repl/Parsing/CoreParser.cs b/src/Microsoft.Repl/Parsing/CoreParser.cs index 35be4867a5..7075f764a0 100644 --- a/src/Microsoft.Repl/Parsing/CoreParser.cs +++ b/src/Microsoft.Repl/Parsing/CoreParser.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Linq; namespace Microsoft.Repl.Parsing diff --git a/src/Microsoft.Repl/Parsing/ICoreParseResult.cs b/src/Microsoft.Repl/Parsing/ICoreParseResult.cs index 20ac2c7b4e..55bf5b2264 100644 --- a/src/Microsoft.Repl/Parsing/ICoreParseResult.cs +++ b/src/Microsoft.Repl/Parsing/ICoreParseResult.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.Repl.Parsing { diff --git a/src/Microsoft.Repl/Parsing/IParser.cs b/src/Microsoft.Repl/Parsing/IParser.cs index c909f7d94f..86ce9ed940 100644 --- a/src/Microsoft.Repl/Parsing/IParser.cs +++ b/src/Microsoft.Repl/Parsing/IParser.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.Parsing +// 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. + +namespace Microsoft.Repl.Parsing { public interface IParser { diff --git a/src/Microsoft.Repl/Scripting/IScriptExecutor.cs b/src/Microsoft.Repl/Scripting/IScriptExecutor.cs index 7d832d8306..fcd5be2b53 100644 --- a/src/Microsoft.Repl/Scripting/IScriptExecutor.cs +++ b/src/Microsoft.Repl/Scripting/IScriptExecutor.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.Repl/Scripting/ScriptExecutor.cs b/src/Microsoft.Repl/Scripting/ScriptExecutor.cs index 3ec4bf5d21..6fafa6cbe4 100644 --- a/src/Microsoft.Repl/Scripting/ScriptExecutor.cs +++ b/src/Microsoft.Repl/Scripting/ScriptExecutor.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.Repl/Shell.cs b/src/Microsoft.Repl/Shell.cs index 49f450eae0..8d6cddb035 100644 --- a/src/Microsoft.Repl/Shell.cs +++ b/src/Microsoft.Repl/Shell.cs @@ -1,4 +1,7 @@ -using System.Threading; +// 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.Threading; using System.Threading.Tasks; using Microsoft.Repl.Commanding; using Microsoft.Repl.Input; diff --git a/src/Microsoft.Repl/ShellState.cs b/src/Microsoft.Repl/ShellState.cs index ef0ccf2a64..cb94bc63a2 100644 --- a/src/Microsoft.Repl/ShellState.cs +++ b/src/Microsoft.Repl/ShellState.cs @@ -1,4 +1,7 @@ -using Microsoft.Repl.Commanding; +// 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 Microsoft.Repl.Commanding; using Microsoft.Repl.ConsoleHandling; using Microsoft.Repl.Input; using Microsoft.Repl.Suggestions; diff --git a/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs b/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs index 71237fcd27..5b852a9b59 100644 --- a/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs +++ b/src/Microsoft.Repl/Suggestions/FileSystemCompletion.cs @@ -1,4 +1,7 @@ -using System; +// 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; using System.IO; using System.Linq; diff --git a/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs b/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs index f7e11a06fb..51e09c3a1c 100644 --- a/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs +++ b/src/Microsoft.Repl/Suggestions/ISuggestionManager.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Repl.Suggestions +// 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. + +namespace Microsoft.Repl.Suggestions { public interface ISuggestionManager { diff --git a/src/Microsoft.Repl/Suggestions/SuggestionManager.cs b/src/Microsoft.Repl/Suggestions/SuggestionManager.cs index 1fd9e861da..ba68ee9d46 100644 --- a/src/Microsoft.Repl/Suggestions/SuggestionManager.cs +++ b/src/Microsoft.Repl/Suggestions/SuggestionManager.cs @@ -1,4 +1,7 @@ -using System; +// 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; using Microsoft.Repl.Parsing; diff --git a/src/Microsoft.Repl/Utils.cs b/src/Microsoft.Repl/Utils.cs index 37ffcd97fb..eaf8160222 100644 --- a/src/Microsoft.Repl/Utils.cs +++ b/src/Microsoft.Repl/Utils.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +// 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.Collections.Generic; namespace Microsoft.Repl { From 7487da155f9272f46890d354be2dacf8912308c9 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Wed, 1 Aug 2018 12:42:16 -0700 Subject: [PATCH 3/9] Switch signing certificate for Newtonsoft.JSON to Microsoft3rdPartyAppComponentDual --- src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj index cec7ac17d5..cf53e4b62e 100644 --- a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj +++ b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj @@ -23,8 +23,8 @@ - - + + From af5d8a824479f8a708447b8467e94b3282a75634 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Fri, 3 Aug 2018 09:18:39 -0700 Subject: [PATCH 4/9] 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 --- Directory.Build.props | 3 +- build/dependencies.props | 2 +- .../Commands/BaseHttpCommand.cs | 63 ++++++++-------- .../Commands/HelpCommand.cs | 23 +++--- .../Microsoft.HttpRepl.csproj | 4 +- src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs | 72 ------------------- .../Preferences/WellKnownPreference.cs | 2 - src/Microsoft.HttpRepl/Program.cs | 24 +++++++ .../Commanding/DefaultCommandDispatcher.cs | 1 + src/Microsoft.Repl/Input/IInputManager.cs | 2 + src/Microsoft.Repl/Input/InputManager.cs | 34 +++++++-- src/Microsoft.Repl/Input/KeyHandlers.cs | 6 ++ src/Microsoft.Repl/Microsoft.Repl.csproj | 1 + .../Microsoft.HttpRepl.Tests.csproj | 2 +- 14 files changed, 119 insertions(+), 120 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 8ca8a8757b..9921cb358e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,4 @@ - + @@ -16,6 +16,7 @@ true MicrosoftNuGet Microsoft + Microsoft3rdPartyAppComponentDual true true diff --git a/build/dependencies.props b/build/dependencies.props index ee0b0b8ccf..bffcc722e2 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -17,7 +17,7 @@ 2.0.3 4.5.1 4.5.0 - 11.0.2 + 11.0.2 9.0.1 2.3.1 2.4.0 diff --git a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs index 374544790b..ee9b4bcf0e 100644 --- a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs @@ -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 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 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 commandInput, HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken) { string contentType = null; if (content.Headers.TryGetValues("Content-Type", out IEnumerable 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 _)) + //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); diff --git a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs index f205fc1243..646dd4c544 100644 --- a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs @@ -30,15 +30,7 @@ namespace Microsoft.HttpRepl.Commands { if (parseResult.Sections.Count == 1) { - foreach (ICommand 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 dispatcher, HttpState programState) + { + foreach (ICommand command in dispatcher.Commands) + { + string help = command.GetHelpSummary(shellState, programState); + + if (!string.IsNullOrEmpty(help)) + { + shellState.ConsoleManager.WriteLine(help); + } + } + } } } diff --git a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj index cf53e4b62e..2dce88c68b 100644 --- a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj +++ b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs index db36c082bf..c8566095b7 100644 --- a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs +++ b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs @@ -145,77 +145,5 @@ namespace Microsoft.HttpRepl.OpenApi return toResolve; } - - //public static async Task 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; - //} } } diff --git a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs index 1cd17a04ce..424c0bb9a4 100644 --- a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs +++ b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs @@ -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"; diff --git a/src/Microsoft.HttpRepl/Program.cs b/src/Microsoft.HttpRepl/Program.cs index a0e1c9addf..6509e7a38c 100644 --- a/src/Microsoft.HttpRepl/Program.cs +++ b/src/Microsoft.HttpRepl/Program.cs @@ -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 [] [options]"); + shell.ShellState.ConsoleManager.WriteLine(); + shell.ShellState.ConsoleManager.WriteLine("Arguments:"); + shell.ShellState.ConsoleManager.WriteLine(" - 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)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); diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs index 2b85c2bc57..22be314827 100644 --- a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs +++ b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs @@ -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()); } } diff --git a/src/Microsoft.Repl/Input/IInputManager.cs b/src/Microsoft.Repl/Input/IInputManager.cs index b67f3e3936..e8e98b2d54 100644 --- a/src/Microsoft.Repl/Input/IInputManager.cs +++ b/src/Microsoft.Repl/Input/IInputManager.cs @@ -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); diff --git a/src/Microsoft.Repl/Input/InputManager.cs b/src/Microsoft.Repl/Input/InputManager.cs index e1d6055683..cd1157c6ff 100644 --- a/src/Microsoft.Repl/Input/InputManager.cs +++ b/src/Microsoft.Repl/Input/InputManager.cs @@ -12,7 +12,7 @@ namespace Microsoft.Repl.Input { public class InputManager : IInputManager { - private readonly Dictionary _handlers = new Dictionary(); + private readonly Dictionary> _handlers = new Dictionary>(); private readonly List _inputBuffer = new List(); 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 handlers)) + { + _handlers[key] = handlers = new Dictionary(); + } + 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 handlers)) + { + _handlers[key] = handlers = new Dictionary(); + } + + 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 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) diff --git a/src/Microsoft.Repl/Input/KeyHandlers.cs b/src/Microsoft.Repl/Input/KeyHandlers.cs index 192c55319c..750db8d2f6 100644 --- a/src/Microsoft.Repl/Input/KeyHandlers.cs +++ b/src/Microsoft.Repl/Input/KeyHandlers.cs @@ -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); diff --git a/src/Microsoft.Repl/Microsoft.Repl.csproj b/src/Microsoft.Repl/Microsoft.Repl.csproj index 63af949be4..cb7311eeeb 100644 --- a/src/Microsoft.Repl/Microsoft.Repl.csproj +++ b/src/Microsoft.Repl/Microsoft.Repl.csproj @@ -4,6 +4,7 @@ netcoreapp2.2 A framework for creating REPLs in .NET Core. dotnet;repl + false diff --git a/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj b/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj index 7a79d0121e..a50dd1df30 100644 --- a/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj +++ b/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj @@ -10,7 +10,7 @@ - + From 8a9b407761436362c16b1de2b6d00e324273a6b9 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Fri, 3 Aug 2018 12:27:02 -0700 Subject: [PATCH 5/9] Use netcoreapp2.1 instead of 2.2 --- src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj | 4 ++-- src/Microsoft.Repl/Microsoft.Repl.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj index 2dce88c68b..99b86075c3 100644 --- a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj +++ b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj @@ -1,8 +1,8 @@ - + Exe - netcoreapp2.2 + netcoreapp2.1 true dotnet-httprepl latest diff --git a/src/Microsoft.Repl/Microsoft.Repl.csproj b/src/Microsoft.Repl/Microsoft.Repl.csproj index cb7311eeeb..d6cc6e83e6 100644 --- a/src/Microsoft.Repl/Microsoft.Repl.csproj +++ b/src/Microsoft.Repl/Microsoft.Repl.csproj @@ -1,7 +1,7 @@ - + - netcoreapp2.2 + netcoreapp2.1 A framework for creating REPLs in .NET Core. dotnet;repl false From 43c74cfa5360be63faaaf361e5d4385e2796e138 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Fri, 3 Aug 2018 20:58:07 -0700 Subject: [PATCH 6/9] 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; } From b242704bda1dbef522d6be69f7b1d4b2aa2b258e Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Sun, 5 Aug 2018 13:55:29 -0700 Subject: [PATCH 7/9] Remove unnecessary exclusions from NuGetPackageVerifier.json --- NuGetPackageVerifier.json | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 4167e36811..fb43448889 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -24,33 +24,12 @@ "DotnetTool" ] }, - "dotnet-httprepl": { - "packageTypes": [ - "DotnetTool" - ], - "Exclusions": { - "ASSEMBLY_DESCRIPTION": { - "tools/netcoreapp2.2/any/System.Net.Http.Formatting.dll": "Referenced assembly, not built as part of this process" - }, - "VERSION_INFORMATIONALVERSION": { - "tools/netcoreapp2.2/any/Newtonsoft.Json.dll": "Referenced assembly, not built as part of this process", - "tools/netcoreapp2.2/any/Newtonsoft.Json.Bson.dll": "Referenced assembly, not built as part of this process" - } - } - }, "Microsoft.AspNetCore.DeveloperCertificates.XPlat": { "Exclusions": { "DOC_MISSING": { "lib/netcoreapp2.2/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package" } } - }, - "Microsoft.Repl": { - "Exclusions": { - "DOC_MISSING": { - "lib/netcoreapp2.2/Microsoft.Repl.dll": "Docs not required to shipoob package" - } - } } } }, From eb591fb29dfc87c9815eba024038788c731afcbc Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Sun, 5 Aug 2018 14:13:08 -0700 Subject: [PATCH 8/9] Attempt to fix exclusions in NuGetPackageVerifier.json again --- NuGetPackageVerifier.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index fb43448889..63b866c8ed 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -24,6 +24,20 @@ "DotnetTool" ] }, + "dotnet-httprepl": { + "packageTypes": [ + "DotnetTool" + ], + "Exclusions": { + "ASSEMBLY_DESCRIPTION": { + "tools/netcoreapp2.1/any/System.Net.Http.Formatting.dll": "Referenced assembly, not built as part of this process" + }, + "VERSION_INFORMATIONALVERSION": { + "tools/netcoreapp2.1/any/Newtonsoft.Json.dll": "Referenced assembly, not built as part of this process", + "tools/netcoreapp2.1/any/Newtonsoft.Json.Bson.dll": "Referenced assembly, not built as part of this process" + } + } + }, "Microsoft.AspNetCore.DeveloperCertificates.XPlat": { "Exclusions": { "DOC_MISSING": { From b53d50f6f0fb79f5c4794f090d6c80382a7e9d98 Mon Sep 17 00:00:00 2001 From: Mike Lorbetske Date: Tue, 7 Aug 2018 16:16:33 -0700 Subject: [PATCH 9/9] Fix incorrectly resolved cross-document pointers --- src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs index c8566095b7..5bff058b95 100644 --- a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs +++ b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs @@ -19,6 +19,8 @@ namespace Microsoft.HttpRepl.OpenApi private static async Task ResolvePointersAsync(Uri loadLocation, JToken root, JToken toResolve, HttpClient client) { + JToken cursor = root; + if (toResolve is JArray arr) { for (int i = 0; i < arr.Count; ++i) @@ -69,7 +71,7 @@ namespace Microsoft.HttpRepl.OpenApi return new JValue((object)null); } - return await ResolvePointersAsync(loadTarget, newRoot, newRoot, client).ConfigureAwait(false); + cursor = await ResolvePointersAsync(loadTarget, newRoot, newRoot, client).ConfigureAwait(false); } //We're in the right document, grab the bookmark (fragment) of the URI and get the element at that path @@ -81,7 +83,6 @@ namespace Microsoft.HttpRepl.OpenApi } string[] parts = fragment.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - JToken cursor = root; for (int i = 0; i < parts.Length; ++i) {