From 7330f4b6fcd7f6239f84009ffd011d86e1127ffe Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Thu, 3 Aug 2023 01:40:01 +0100 Subject: [PATCH 01/13] revert adoption of proposal api :) normalize autoClosing pair. Revert "revert adoption of proposal api :)" This reverts commit c1f2127008f1838b1fd7473674a833bf76424cdf. --- .../dynamicGrammarSemanticTokenProvider.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/polyglot-notebooks-vscode-common/src/dynamicGrammarSemanticTokenProvider.ts b/src/polyglot-notebooks-vscode-common/src/dynamicGrammarSemanticTokenProvider.ts index 5cac16afd9..e44a998207 100644 --- a/src/polyglot-notebooks-vscode-common/src/dynamicGrammarSemanticTokenProvider.ts +++ b/src/polyglot-notebooks-vscode-common/src/dynamicGrammarSemanticTokenProvider.ts @@ -538,6 +538,8 @@ export function parseLanguageConfiguration(content: string): any { fixRegExpProperty(languageConfigurationObject, 'wordPattern'); + fixAutoClosingPairs(languageConfigurationObject); + if (typeof languageConfigurationObject.indentationRules === 'object') { fixRegExpProperty(languageConfigurationObject.indentationRules, 'decreaseIndentPattern'); fixRegExpProperty(languageConfigurationObject.indentationRules, 'increaseIndentPattern'); @@ -556,6 +558,26 @@ export function parseLanguageConfiguration(content: string): any { return languageConfigurationObject; } +function fixAutoClosingPairs(value: any) { + if (value.autoClosingPairs && Array.isArray(value.autoClosingPairs)) { + const newAutoClosingPairs: any[] = []; + for (let i = 0; i < value.autoClosingPairs.length; i++) { + const pair = value.autoClosingPairs[i]; + if (Array.isArray(pair) && pair.length === 2) { + newAutoClosingPairs.push({ + open: pair[0], + close: pair[1] + } + ) + } else if (typeof pair === 'object' && pair.open && pair.close) { + newAutoClosingPairs.push(pair); + } + } + + value.autoClosingPairs = newAutoClosingPairs; + } +} + function fixRegExpProperty(value: any, propertyName: string) { if (typeof value[propertyName] === 'string') { value[propertyName] = new RegExp(value[propertyName]); From 8915ad98609cad23d5d81765041c2a895afb4782 Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Thu, 3 Aug 2023 09:42:23 +0100 Subject: [PATCH 02/13] prepare for stable release --- .../package.json | 4 ++-- .../do-version-upgrade.ps1 | 3 --- src/polyglot-notebooks-vscode/package.json | 7 +++--- src/polyglot-notebooks-vscode/src/vscode.d.ts | 23 +++++++++++++++++++ ...languageConfigurationAutoClosingPairs.d.ts | 17 ++++++++++++++ src/polyglot-notebooks-vscode/update-api.ps1 | 5 +--- 6 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/polyglot-notebooks-vscode/src/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts diff --git a/src/polyglot-notebooks-vscode-insiders/package.json b/src/polyglot-notebooks-vscode-insiders/package.json index 49fdc4f132..c6d2b5c6ef 100644 --- a/src/polyglot-notebooks-vscode-insiders/package.json +++ b/src/polyglot-notebooks-vscode-insiders/package.json @@ -220,7 +220,7 @@ }, "dotnet-interactive.requiredInteractiveToolVersion": { "type": "string", - "default": "1.0.437401", + "default": "1.0.440202", "description": "%description.requiredInteractiveToolVersion%" } } @@ -845,4 +845,4 @@ "vscode-uri": "3.0.6", "@vscode/l10n": "0.0.10" } -} \ No newline at end of file +} diff --git a/src/polyglot-notebooks-vscode/do-version-upgrade.ps1 b/src/polyglot-notebooks-vscode/do-version-upgrade.ps1 index e6f0c837d0..fd25d3642e 100644 --- a/src/polyglot-notebooks-vscode/do-version-upgrade.ps1 +++ b/src/polyglot-notebooks-vscode/do-version-upgrade.ps1 @@ -22,9 +22,6 @@ try { $stablePackageJsonContents.scripts.package = $stablePackageJsonContents.scripts.package.Replace("--pre-release","").Trim() - # ensure the stable is using the available proposed apis - $stablePackageJsonContents.enabledApiProposals = @("notebookMessaging") - $stablePackageJsonContents | ConvertTo-Json -depth 100 | Out-File "$stableDirectory\package.json" # copy grammar files diff --git a/src/polyglot-notebooks-vscode/package.json b/src/polyglot-notebooks-vscode/package.json index 0b9d8a2953..723dd808ba 100644 --- a/src/polyglot-notebooks-vscode/package.json +++ b/src/polyglot-notebooks-vscode/package.json @@ -7,13 +7,14 @@ "author": "Microsoft Corporation", "license": "MIT", "enabledApiProposals": [ - "notebookMessaging" + "notebookMessaging", + "languageConfigurationAutoClosingPairs" ], "preview": false, "//version": "%description.version%", "version": "42.42.42", "engines": { - "vscode": "^1.78.0" + "vscode": "^1.81.0" }, "bugs": { "url": "https://github.com/dotnet/interactive/issues" @@ -219,7 +220,7 @@ }, "dotnet-interactive.requiredInteractiveToolVersion": { "type": "string", - "default": "1.0.437401", + "default": "1.0.440202", "description": "%description.requiredInteractiveToolVersion%" } } diff --git a/src/polyglot-notebooks-vscode/src/vscode.d.ts b/src/polyglot-notebooks-vscode/src/vscode.d.ts index d819f00fa1..8e9c1cef48 100644 --- a/src/polyglot-notebooks-vscode/src/vscode.d.ts +++ b/src/polyglot-notebooks-vscode/src/vscode.d.ts @@ -1733,6 +1733,11 @@ declare module 'vscode' { */ kind?: QuickPickItemKind; + /** + * The icon path or {@link ThemeIcon} for the QuickPickItem. + */ + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; + /** * A human-readable string which is rendered less prominent in the same line. Supports rendering of * {@link ThemeIcon theme icons} via the `$()`-syntax. @@ -16299,6 +16304,24 @@ declare module 'vscode' { */ createTestItem(id: string, label: string, uri?: Uri): TestItem; + /** + * Marks an item's results as being outdated. This is commonly called when + * code or configuration changes and previous results should no longer + * be considered relevant. The same logic used to mark results as outdated + * may be used to drive {@link TestRunRequest.continuous continuous test runs}. + * + * If an item is passed to this method, test results for the item and all of + * its children will be marked as outdated. If no item is passed, then all + * test owned by the TestController will be marked as outdated. + * + * Any test runs started before the moment this method is called, including + * runs which may still be ongoing, will be marked as outdated and deprioritized + * in the editor's UI. + * + * @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated. + */ + invalidateTestResults(items?: TestItem | readonly TestItem[]): void; + /** * Unregisters the test controller, disposing of its associated tests * and unpersisted results. diff --git a/src/polyglot-notebooks-vscode/src/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts b/src/polyglot-notebooks-vscode/src/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts new file mode 100644 index 0000000000..641e2f5f0d --- /dev/null +++ b/src/polyglot-notebooks-vscode/src/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/173738 + + export interface LanguageConfiguration { + autoClosingPairs?: { + open: string; + close: string; + notIn?: string[]; + }[]; + } +} diff --git a/src/polyglot-notebooks-vscode/update-api.ps1 b/src/polyglot-notebooks-vscode/update-api.ps1 index a4b21ff368..5d0cef6ab8 100644 --- a/src/polyglot-notebooks-vscode/update-api.ps1 +++ b/src/polyglot-notebooks-vscode/update-api.ps1 @@ -10,10 +10,7 @@ try { function DownloadVsCodeApi([string] $branchName, [string] $destinationDirectory, [bool] $isInsiders = $false) { Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/vscode/$branchName/src/vscode-dts/vscode.d.ts" -OutFile "$PSScriptRoot\$destinationDirectory\vscode.d.ts" Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/vscode/$branchName/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts" -OutFile "$PSScriptRoot\$destinationDirectory\vscode.proposed.notebookMessaging.d.ts" - if ($isInsiders) { - Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/vscode/$branchName/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts" -OutFile "$PSScriptRoot\$destinationDirectory\vscode.proposed.languageConfigurationAutoClosingPairs.d.ts" - } - + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/microsoft/vscode/$branchName/src/vscode-dts/vscode.proposed.languageConfigurationAutoClosingPairs.d.ts" -OutFile "$PSScriptRoot\$destinationDirectory\vscode.proposed.languageConfigurationAutoClosingPairs.d.ts" } # stable From 5f8558a6c961fe996ec1dc196b476150d805e53c Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Thu, 3 Aug 2023 16:48:21 +0100 Subject: [PATCH 03/13] Update release.yml --- .github/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/release.yml b/.github/release.yml index ef8219db82..9ecbb42d85 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -23,6 +23,9 @@ changelog: - title: Area-Auth labels: - Area-Auth + - title: Area-API + labels: + - Area-API - title: Area-C# labels: - Area-C# From 95fe30bf6c036892e2ed2df0fa52b2c1977d554b Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 3 Aug 2023 12:35:09 -0700 Subject: [PATCH 04/13] Update README.md (#3117) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43dc6e8359..d092573d9a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The multi-language experience of .NET Interactive is truly a collaborative effor - **PowerShell Team:** PowerShell support - **Azure Data Team:** SQL and KQL support -- **Azure Notebooks Team**: Python, R, and Jupyter subkernel support (coming soon...) +- **Azure Notebooks Team**: Python, R, and Jupyter subkernel support ## Telemetry From d0d9e4000d5e6c38d3ef6746b6f541a10a317482 Mon Sep 17 00:00:00 2001 From: bleaphar <33817274+bleaphar@users.noreply.github.com> Date: Fri, 4 Aug 2023 10:06:11 -0700 Subject: [PATCH 05/13] Updates to HTTP Parser (#3118) * initial implementation * Implement parsing for versions and headers. Includes some code clean up to fix up warnings (mainly around nullable annotations). Also moves reference parser implementation into a sub-folder. * Adding support to parser for header body and lexer changes * Improve parsing of trivia. Add support for parsing leading trivia. * Addition of Comment Support * Beginning changes to support error reporting * Changing method node to be optional The additions in this file make it possible to put in just a url as a valid request node * Removal of files, Restructure of tests Also the addition of a lexer test * Addressing various pr comments * Test Changes and Lexer File Removal * Removal of .ToLower() * Changes for variables and multiple requests This code is not fully working currently * Separators are running correctly * Addition of preliminary binding * Removal of the separator * Cleanup * Updating csproj for no warn * parenthesis add * Adding default case --------- Co-authored-by: Jon Sequeira Co-authored-by: Shyam Namboodiripad --- .../ParserTests.cs | 142 +++++++++++--- ...soft.DotNet.Interactive.HttpRequest.csproj | 2 +- .../HttpEmbeddedExpressionNode.cs | 35 ++++ .../HttpExpressionEndNode.cs | 15 ++ .../HttpExpressionNode.cs | 15 ++ .../HttpExpressionStartNode.cs | 15 ++ .../HttpRequestNode.cs | 7 +- .../HttpRequestParser.cs | 177 ++++++++++++++---- .../HttpRequestSeparatorNode.cs | 15 ++ .../HttpSyntaxNode.cs | 18 +- .../HttpUrlNode.cs | 17 ++ 11 files changed, 377 insertions(+), 81 deletions(-) create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionEndNode.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionStartNode.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestSeparatorNode.cs diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs index 5ef8b92484..e8c661ffae 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs @@ -12,7 +12,7 @@ namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; -public class ParserTests: IDisposable +public class ParserTests : IDisposable { public ParserTests() @@ -32,7 +32,7 @@ private static HttpRequestParseResult Parse(string code) if (result.SyntaxTree is not null && result.SyntaxTree.RootNode is not null) { result.SyntaxTree.RootNode.TextWithTrivia.Should().Be(code); - } + } return result; } @@ -85,8 +85,6 @@ public void multiple_whitespaces_are_treated_as_a_single_token() { var result = Parse(" \t "); - using var _ = new AssertionScope(); - result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which .MethodNode.ChildTokens.First().Should().BeOfType(); @@ -98,14 +96,14 @@ public void multiple_whitespaces_are_treated_as_a_single_token() [Fact] public void multiple_newlines_are_parsed_into_different_tokens() - { - var result = Parse("\n\v\r\n\n"); + { + var result = Parse("\n\v\r\n\n"); result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.Select(t => new {t.TextWithTrivia, t.Kind}).Should().BeEquivalentSequenceTo( - new {TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }, + .MethodNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( + new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }, new { TextWithTrivia = "\v", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\r\n", Kind = HttpTokenKind.NewLine }, + new { TextWithTrivia = "\r\n", Kind = HttpTokenKind.NewLine }, new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }); } @@ -132,11 +130,10 @@ public void whitespace_is_legal_at_the_beginning_of_a_request() { var result = Parse(" GET https://example.com"); - using var _ = new AssertionScope(); result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which .MethodNode.ChildTokens.First().Kind - .Should().Be(HttpTokenKind.Whitespace); + .Should().Be(HttpTokenKind.Whitespace); } [Fact] @@ -144,7 +141,7 @@ public void newline_is_legal_at_the_beginning_of_a_request() { var result = Parse( """ - + GET https://example.com """); @@ -244,6 +241,46 @@ public void request_node_without_method_node_created_correctly() result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which .MethodNode.Should().BeNull(); } + + [Fact] + public void url_node_can_give_url() + { + var result = Parse( + """ + GET https://{{host}}/api/{{version}}comments/1 + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var urlNode = requestNode.UrlNode; + urlNode.GetUri(x => x.Text switch + { + "host" => "example.com", + "version" => "123-", + _ => throw new NotImplementedException() + }).ToString().Should().Be("https://example.com/api/123-comments/1"); + } + + /* + [Fact] + public void error_is_reported_for_incorrect_uri() + { + var result = Parse( + """ + GET https://{{host}}/api/{{version}}comments/1 + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var urlNode = requestNode.UrlNode; + urlNode.TryGetUri(x => x.Text switch + { + "host" => "example.com" + }).Should().BeFalse(); + + }*/ } public class Headers @@ -263,8 +300,6 @@ public void header_with_body_is_parsed_correctly() """); - using var _ = new AssertionScope(); - var requestNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which; @@ -284,7 +319,7 @@ public void header_separator_is_present() var result = Parse( """ POST https://example.com/comments HTTP/1.1 - Content-Type: application + Content-Type: application """); var requestNode = result.SyntaxTree.RootNode @@ -306,8 +341,6 @@ public void headers_are_parsed_correctly() Cookie: expor=;HSD=Ak_1ZasdqwASDASD;SSID=SASASSDFsdfsdf213123;APISID=WRQWRQWRQWRcc123123; """); - using var _ = new AssertionScope(); - var headersNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which .ChildNodes.Should().ContainSingle().Which; @@ -315,19 +348,19 @@ public void headers_are_parsed_correctly() var headerNodes = headersNode.HeaderNodes.ToArray(); headerNodes.Should().HaveCount(5); - headerNodes[0].NameNode.Text.Should().Be("Accept"); + headerNodes[0].NameNode.Text.Should().Be("Accept"); headerNodes[0].ValueNode.Text.Should().Be("*/*"); - headerNodes[1].NameNode.Text.Should().Be("Accept-Encoding"); + headerNodes[1].NameNode.Text.Should().Be("Accept-Encoding"); headerNodes[1].ValueNode.Text.Should().Be("gzip, deflate, br"); - headerNodes[2].NameNode.Text.Should().Be("Accept-Language"); + headerNodes[2].NameNode.Text.Should().Be("Accept-Language"); headerNodes[2].ValueNode.Text.Should().Be("en-US,en;q=0.9"); - headerNodes[3].NameNode.Text.Should().Be("ContentLength"); + headerNodes[3].NameNode.Text.Should().Be("ContentLength"); headerNodes[3].ValueNode.Text.Should().Be("7060"); - headerNodes[4].NameNode.Text.Should().Be("Cookie"); + headerNodes[4].NameNode.Text.Should().Be("Cookie"); headerNodes[4].ValueNode.Text.Should().Be("expor=;HSD=Ak_1ZasdqwASDASD;SSID=SASASSDFsdfsdf213123;APISID=WRQWRQWRQWRcc123123;"); } } @@ -347,12 +380,12 @@ public void body_separator_is_present() sample - """); + """); var requestNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which; - requestNode.BodySeparatorNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); + requestNode.BodySeparatorNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); } [Fact] @@ -368,14 +401,13 @@ public void when_headers_are_not_present_there_should_be_no_header_nodes() """); - using var _ = new AssertionScope(); var requestNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which; - requestNode.HeadersNode.HeaderNodes.Count.Should().Be(0); + requestNode.HeadersNode.Should().BeNull(); } - + [Fact] public void body_is_parsed_correctly_when_headers_are_not_present() { @@ -389,7 +421,6 @@ public void body_is_parsed_correctly_when_headers_are_not_present() """); - using var _ = new AssertionScope(); var requestNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which; @@ -421,7 +452,6 @@ public void multiple_new_lines_before_body_are_parsed_correctly() """); - using var _ = new AssertionScope(); var requestNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which; @@ -448,7 +478,6 @@ public void comments_are_parsed_correctly() GET https://example.com HTTP/1.1" """); - using var _ = new AssertionScope(); var methodNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which @@ -459,7 +488,58 @@ public void comments_are_parsed_correctly() } + } + + public class Tree + { + [Fact] + public void multiple_request_are_parsed_correctly() + { + var result = Parse( + """ + GET https://example.com + + ### + + GET https://example1.com + + ### + + GET https://example2.com + """); + + + var requestNodes = result.SyntaxTree.RootNode + .ChildNodes.OfType(); + + requestNodes.Select(r => r.Text).Should() + .BeEquivalentSequenceTo(new[] { "GET https://example.com", + "GET https://example1.com", "GET https://example2.com"}); + } + } + + public class Variables + { + [Fact] + public void expression_is_parsed_correctly() + { + var result = Parse( + """ + GET https://{{host}}/api/{{version}}comments/1 HTTP/1.1 + Authorization: {{token}} + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + requestNode.UrlNode.DescendantNodesAndTokens().OfType().Select(e => e.Text) + .Should().BeEquivalentSequenceTo(new[] { "host", "version" }); + + requestNode.HeadersNode.DescendantNodesAndTokens().OfType() + .Should().ContainSingle().Which.Text.Should().Be("token"); + } + //TODO Test all parsers for expression } } // TODO: Test string with variable declarations but no requests @@ -622,4 +702,4 @@ public async Task HeaderMissingValue_ShouldBeTreatedAsBody_WithError() Assert.AreEqual(string.Format(Strings.HttpHeaderMissingValue, "foo:"), doc.Items[3].Errors[0].Message); } -}*/ \ No newline at end of file +}*/ diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj b/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj index c4b15b0c85..aa46df844d 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj +++ b/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj @@ -3,7 +3,7 @@ net7.0 latest - $(NoWarn);2003;CS8002;VSTHRD002;NU5100 + $(NoWarn);2003;CS8002;(CS8509);VSTHRD002;NU5100 false enable diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs new file mode 100644 index 0000000000..6a987eab52 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using Microsoft.CodeAnalysis.Text; + +using System.Collections.Generic; + +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal class HttpEmbeddedExpressionNode : HttpSyntaxNode +{ + internal HttpEmbeddedExpressionNode( + SourceText sourceText, + HttpSyntaxTree? syntaxTree, + HttpExpressionStartNode startNode, + HttpExpressionNode expressionNode, + HttpExpressionEndNode endNode) : base(sourceText, syntaxTree) + { + StartNode = startNode; + Add(StartNode); + + ExpressionNode = expressionNode; + Add(ExpressionNode); + + EndNode = endNode; + Add(EndNode); + } + + public HttpExpressionStartNode StartNode { get; } + public HttpExpressionNode ExpressionNode { get; } + public HttpExpressionEndNode EndNode { get; } + +} diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionEndNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionEndNode.cs new file mode 100644 index 0000000000..34473009dc --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionEndNode.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal class HttpExpressionEndNode : HttpSyntaxNode +{ + internal HttpExpressionEndNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) + { + } +} diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs new file mode 100644 index 0000000000..968602d0f6 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal class HttpExpressionNode : HttpSyntaxNode +{ + internal HttpExpressionNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) + { + } +} diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionStartNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionStartNode.cs new file mode 100644 index 0000000000..7a55f77223 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionStartNode.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal class HttpExpressionStartNode : HttpSyntaxNode +{ + internal HttpExpressionStartNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) + { + } +} diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs index f449351b23..65319ad557 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs @@ -25,10 +25,9 @@ internal HttpRequestNode( MethodNode = methodNode; Add(MethodNode); } - + UrlNode = urlNode; - Add(UrlNode); if (versionNode is not null) @@ -41,11 +40,11 @@ internal HttpRequestNode( { HeadersNode = headersNode; Add(HeadersNode); - } + } if(bodySeparatorNode is not null) { - BodySeparatorNode = bodySeparatorNode; + BodySeparatorNode = bodySeparatorNode; Add(bodySeparatorNode); } diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs index 5197096b1a..332e88ca77 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; +using System; using System.Collections.Generic; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -55,6 +56,10 @@ public HttpSyntaxParser(SourceText sourceText) { _syntaxTree.RootNode.Add(requestNode); } + if(ParseRequestSeparator() is { } separatorNode) + { + _syntaxTree.RootNode.Add(separatorNode); + } } return _syntaxTree; @@ -62,14 +67,23 @@ public HttpSyntaxParser(SourceText sourceText) private HttpSyntaxToken CurrentToken => _tokens![_currentTokenIndex]; - private HttpSyntaxToken? NextToken - { - get - { + private HttpSyntaxToken? NextToken + { + get + { var nextTokenIndex = _currentTokenIndex + 1; return nextTokenIndex >= _tokens!.Count ? null : _tokens![nextTokenIndex]; - } - } + } + } + + private HttpSyntaxToken? NextNextToken + { + get + { + var nextNextTokenIndex = _currentTokenIndex + 2; + return nextNextTokenIndex >= _tokens!.Count ? null : _tokens![nextNextTokenIndex]; + } + } private bool MoreTokens() => _tokens!.Count > _currentTokenIndex; @@ -92,13 +106,15 @@ private T ParseLeadingTrivia(T node) where T : HttpSyntaxNode } else if (CurrentToken.Kind is HttpTokenKind.NewLine) { - ConsumeCurrentTokenInto(node); + ConsumeCurrentTokenInto(node); } - else if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" }) - { + else if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" } && + !(NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" } && + NextNextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" })) + { node.Add(ParseComment()); - } - else if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "/" } && + } + else if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "/" } && NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "/" }) { node.Add(ParseComment()); @@ -142,7 +158,7 @@ private HttpRequestNode ParseRequest() var bodySeparatorNode = ParseBodySeparator(); var bodyNode = ParseBody(); - return new HttpRequestNode( + var requestNode = new HttpRequestNode( _sourceText, _syntaxTree, methodNode, @@ -151,6 +167,30 @@ private HttpRequestNode ParseRequest() headersNode, bodySeparatorNode, bodyNode); + return requestNode; + } + + private HttpRequestSeparatorNode? ParseRequestSeparator() + { + if (!MoreTokens() || !IsRequestSeparator()) + { + return null; + } + + var node = new HttpRequestSeparatorNode(_sourceText, _syntaxTree); + ParseLeadingTrivia(node); + if (IsRequestSeparator()) + { + ConsumeCurrentTokenInto(node); + ConsumeCurrentTokenInto(node); + ConsumeCurrentTokenInto(node); + while (MoreTokens() && CurrentToken.Kind is not (HttpTokenKind.NewLine or HttpTokenKind.Whitespace)) + { + ConsumeCurrentTokenInto(node); + } + } + + return ParseTrailingTrivia(node); } private HttpMethodNode? ParseMethod() @@ -167,12 +207,12 @@ private HttpRequestNode ParseRequest() if (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Word) { if (CurrentToken.Text.ToLower() is ("get" or "post" or "patch" or "put" or "delete" or "head" or "options" or "trace")) - { + { ConsumeCurrentTokenInto(node); } else - { - var tokenSpan = _sourceText.GetSubText(CurrentToken.Span).Lines.GetLinePositionSpan(CurrentToken.Span); + { + var tokenSpan = _sourceText.GetSubText(CurrentToken.Span).Lines.GetLinePositionSpan(CurrentToken.Span); var diagnostic = new Diagnostic(LinePositionSpan.FromCodeAnalysisLinePositionSpan(tokenSpan), DiagnosticSeverity.Warning, CurrentToken.Text, $"Unrecognized HTTP verb {CurrentToken.Text}"); node.AddDiagnostic(diagnostic); @@ -193,15 +233,65 @@ private HttpUrlNode ParseUrl() while (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Word or HttpTokenKind.Punctuation) { - ConsumeCurrentTokenInto(node); + if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" } && + NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" }) + { + node.Add(ParseEmbeddedExpression()); + } + else + { + ConsumeCurrentTokenInto(node); + } } return ParseTrailingTrivia(node, stopAfterNewLine: true); } + private HttpEmbeddedExpressionNode ParseEmbeddedExpression() + { + var startNode = ParseExpressionStart(); + var expressionNode = ParseExpression(); + var endNode = ParseExpressionEnd(); + + return new HttpEmbeddedExpressionNode(_sourceText, _syntaxTree, startNode, expressionNode, endNode); + } + + private HttpExpressionStartNode ParseExpressionStart() + { + var node = new HttpExpressionStartNode(_sourceText, _syntaxTree); + + ConsumeCurrentTokenInto(node); + ConsumeCurrentTokenInto(node); + + return ParseTrailingTrivia(node); + } + + private HttpExpressionNode ParseExpression() + { + var node = new HttpExpressionNode(_sourceText, _syntaxTree); + ParseLeadingTrivia(node); + + while (MoreTokens() && !(CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "}" } && + NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "}" })) + { + ConsumeCurrentTokenInto(node); + } + return ParseTrailingTrivia(node); + } + + private HttpExpressionEndNode ParseExpressionEnd() + { + var node = new HttpExpressionEndNode(_sourceText, _syntaxTree); + + ConsumeCurrentTokenInto(node); + ConsumeCurrentTokenInto(node); + + return ParseTrailingTrivia(node); + } + private HttpVersionNode? ParseVersion() { - if (!MoreTokens()) + if (!MoreTokens() || IsRequestSeparator()) { return null; } @@ -214,7 +304,8 @@ private HttpUrlNode ParseUrl() { ConsumeCurrentTokenInto(node); - while (MoreTokens() && CurrentToken.Kind is not HttpTokenKind.NewLine) + while (MoreTokens() && CurrentToken.Kind is not HttpTokenKind.NewLine && + !IsRequestSeparator()) { ConsumeCurrentTokenInto(node); } @@ -225,17 +316,22 @@ private HttpUrlNode ParseUrl() private HttpHeadersNode? ParseHeaders() { - if (!MoreTokens()) + if (!MoreTokens() || IsRequestSeparator()) { return null; } var headerNodes = new List(); - while (MoreTokens() && CurrentToken.Kind is not (HttpTokenKind.NewLine or HttpTokenKind.Whitespace)) + while (MoreTokens() && CurrentToken.Kind is not (HttpTokenKind.NewLine or HttpTokenKind.Whitespace) && + !IsRequestSeparator()) { headerNodes.Add(ParseHeader()); } + if (headerNodes.Count == 0) + { + return null; + } return new HttpHeadersNode(_sourceText, _syntaxTree, headerNodes); } @@ -297,19 +393,18 @@ private HttpHeaderValueNode ParseHeaderValue() ParseLeadingTrivia(node); - if (MoreTokens() && CurrentToken.Kind is not HttpTokenKind.NewLine) + while (MoreTokens() && CurrentToken.Kind is not HttpTokenKind.NewLine) { - ConsumeCurrentTokenInto(node); - - while (MoreTokens()) + if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" } && + NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" }) + { + node.Add(ParseEmbeddedExpression()); + } + else { - if (CurrentToken.Kind is HttpTokenKind.NewLine) - { - break; - } - ConsumeCurrentTokenInto(node); } + } return ParseTrailingTrivia(node, stopAfterNewLine: true); @@ -317,7 +412,7 @@ private HttpHeaderValueNode ParseHeaderValue() private HttpBodySeparatorNode? ParseBodySeparator() { - if (!MoreTokens()) + if (!MoreTokens() || IsRequestSeparator()) { return null; } @@ -326,11 +421,13 @@ private HttpHeaderValueNode ParseHeaderValue() ParseLeadingTrivia(node); - if (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Whitespace or HttpTokenKind.NewLine) + if (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Whitespace or HttpTokenKind.NewLine && + !IsRequestSeparator()) { ConsumeCurrentTokenInto(node); - while (MoreTokens() && CurrentToken.Kind is (HttpTokenKind.Whitespace or HttpTokenKind.NewLine)) + while (MoreTokens() && CurrentToken.Kind is (HttpTokenKind.Whitespace or HttpTokenKind.NewLine) && + !IsRequestSeparator()) { ConsumeCurrentTokenInto(node); } @@ -341,7 +438,7 @@ private HttpHeaderValueNode ParseHeaderValue() private HttpBodyNode? ParseBody() { - if (!MoreTokens()) + if (!MoreTokens() || IsRequestSeparator()) { return null; } @@ -350,11 +447,12 @@ private HttpHeaderValueNode ParseHeaderValue() ParseLeadingTrivia(node); - if (MoreTokens() && CurrentToken.Kind is not (HttpTokenKind.Whitespace or HttpTokenKind.NewLine)) + if (MoreTokens() && CurrentToken.Kind is not (HttpTokenKind.Whitespace or HttpTokenKind.NewLine) && + !IsRequestSeparator()) { ConsumeCurrentTokenInto(node); - while (MoreTokens()) + while (MoreTokens() && !IsRequestSeparator()) { ConsumeCurrentTokenInto(node); @@ -397,7 +495,7 @@ private HttpCommentStartNode ParseCommentStart() if (MoreTokens() && CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" }) { ConsumeCurrentTokenInto(node); - } + } else if (MoreTokens() && CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "/" } && NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "/" }) { @@ -408,6 +506,13 @@ private HttpCommentStartNode ParseCommentStart() return ParseTrailingTrivia(node); } + private bool IsRequestSeparator() + { + return CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" } && + (NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" } && + NextNextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "#" }); + } + } private class HttpLexer diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestSeparatorNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestSeparatorNode.cs new file mode 100644 index 0000000000..7c4f11a2c2 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestSeparatorNode.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable + +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal class HttpRequestSeparatorNode : HttpSyntaxNode +{ + internal HttpRequestSeparatorNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) + { + } +} diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs index eb71c9ac11..169c0ff921 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs @@ -113,30 +113,30 @@ public IEnumerable DescendantNodesAndTokensAndSelf() } public IEnumerable DescendantNodesAndTokens() => - FlattenDepthFirst(_childNodesAndTokens, n => n switch + FlattenBreadthFirst(_childNodesAndTokens, n => n switch { HttpSyntaxNode node => node.ChildNodesAndTokens, _ => Array.Empty() }); - private static IEnumerable FlattenDepthFirst( - IEnumerable source, - Func> children) + private static IEnumerable FlattenBreadthFirst( + IEnumerable source, + Func> children) { - var stack = new Stack(); + var queue = new Queue(); foreach (var item in source) { - stack.Push(item); + queue.Enqueue(item); } - while (stack.Count > 0) + while (queue.Count > 0) { - var current = stack.Pop(); + var current = queue.Dequeue(); foreach (var item in children(current)) { - stack.Push(item); + queue.Enqueue(item); } yield return current; diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs index fecb4437d2..237e1962ce 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs @@ -4,6 +4,8 @@ #nullable enable using Microsoft.CodeAnalysis.Text; +using System; +using System.Text; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -12,4 +14,19 @@ internal class HttpUrlNode : HttpSyntaxNode internal HttpUrlNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) { } + + internal Uri GetUri(Func value) + { + var urlText = new StringBuilder(); + + foreach(var node in ChildNodesAndTokens) + { + urlText.Append(node switch + { + HttpEmbeddedExpressionNode n => value(n.ExpressionNode), + _ => node.Text + }); + } + return new Uri(urlText.ToString(), UriKind.Absolute); + } } From 21d6c384d9028fbeb732b7295c23d4de03d472fd Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Fri, 4 Aug 2023 15:03:23 -0700 Subject: [PATCH 06/13] HTTP parser variable binding support (#3120) * split tests into separate files * binding for HttpUrlNode * initial work on HttpRequestMessage binding --- ...otNet.Interactive.HttpRequest.Tests.csproj | 9 +- .../ParserTests.Body.cs | 110 ++++ .../ParserTests.Comments.cs | 31 ++ .../ParserTests.Headers.cs | 94 ++++ .../ParserTests.Lexer.cs | 57 ++ .../ParserTests.Method.cs | 77 +++ .../ParserTests.Request.cs | 126 +++++ .../ParserTests.Trivia.cs | 46 ++ .../ParserTests.Url.cs | 107 ++++ .../ParserTests.Variables.cs | 37 ++ .../ParserTests.Version.cs | 24 + .../ParserTests.cs | 523 +----------------- ...soft.DotNet.Interactive.HttpRequest.csproj | 4 + .../HttpBindingDelegate.cs | 7 + .../HttpBindingResult.cs | 48 ++ .../HttpEmbeddedExpressionNode.cs | 5 +- .../HttpExpressionNode.cs | 8 +- .../HttpRequestNode.cs | 42 +- .../HttpRequestParseResult.cs | 17 +- .../HttpRequestParser.cs | 10 +- .../HttpSyntaxNodeOrToken.cs | 25 +- .../HttpUrlNode.cs | 49 +- 22 files changed, 888 insertions(+), 568 deletions(-) create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Body.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Url.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Variables.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Version.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingDelegate.cs create mode 100644 src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingResult.cs diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Microsoft.DotNet.Interactive.HttpRequest.Tests.csproj b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Microsoft.DotNet.Interactive.HttpRequest.Tests.csproj index d1232d43d3..6737d1bd25 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Microsoft.DotNet.Interactive.HttpRequest.Tests.csproj +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Microsoft.DotNet.Interactive.HttpRequest.Tests.csproj @@ -3,10 +3,9 @@ net7.0 false - $(NoWarn);VSTHRD200 - - $(NoWarn);8002 + + $(NoWarn);VSTHRD200;CS8002;CS8509 @@ -20,6 +19,10 @@ + + + + diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Body.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Body.cs new file mode 100644 index 0000000000..9cb86550ed --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Body.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Body + { + [Fact] + public void body_separator_is_present() + { + var result = Parse( + """ + POST https://example.com/comments HTTP/1.1 + Content-Type: application/xml + Authorization: token xxx + + + sample + + + """); + + var requestNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which; + + requestNode.BodySeparatorNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); + } + + [Fact] + public void when_headers_are_not_present_there_should_be_no_header_nodes() + { + var result = Parse( + """ + POST https://example.com/comments HTTP/1.1 + + + sample + + + """); + + var requestNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which; + + requestNode.HeadersNode.Should().BeNull(); + } + + [Fact] + public void body_is_parsed_correctly_when_headers_are_not_present() + { + var result = Parse( + """ + POST https://example.com/comments HTTP/1.1 + + + sample + + + """); + + var requestNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which; + + requestNode.BodyNode.Text.Should().Be( + """ + + sample + + + """); + } + + [Fact] + public void multiple_new_lines_before_body_are_parsed_correctly() + { + var result = Parse( + """ + POST https://example.com/comments HTTP/1.1 + Content-Type: application/xml + Authorization: token xxx + + + + + + sample + + + """); + + var requestNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which; + + requestNode.BodyNode.Text.Should().Be( + """ + + sample + + + """); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs new file mode 100644 index 0000000000..2624af4194 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Comments + { + [Fact] + public void comments_are_parsed_correctly() + { + var result = Parse( + """ + # This is a comment + GET https://example.com HTTP/1.1" + """); + + var methodNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode; + + methodNode.ChildNodes.Should().ContainSingle().Which.Text.Should().Be( + "# This is a comment"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs new file mode 100644 index 0000000000..d9cbb290a4 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Headers + { + [Fact] + public void header_with_body_is_parsed_correctly() + { + var result = Parse( + """ + POST https://example.com/comments HTTP/1.1 + Content-Type: application/xml + Authorization: token xxx + + + sample + + + """); + + var requestNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which; + + requestNode.HeadersNode.HeaderNodes.Count.Should().Be(2); + requestNode.BodyNode.Text.Should().Be( + """ + + sample + + + """); + } + + [Fact] + public void header_separator_is_present() + { + var result = Parse( + """ + POST https://example.com/comments HTTP/1.1 + Content-Type: application + """); + + var requestNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which; + + requestNode.HeadersNode.HeaderNodes.Single().SeparatorNode.Text.Should().Be(":"); + } + + [Fact] + public void headers_are_parsed_correctly() + { + var result = Parse( + """ + GET https://example.com HTTP/1.1 + Accept: */* + Accept-Encoding : gzip, deflate, br + Accept-Language : en-US,en;q=0.9 + ContentLength:7060 + Cookie: expor=;HSD=Ak_1ZasdqwASDASD;SSID=SASASSDFsdfsdf213123;APISID=WRQWRQWRQWRcc123123; + """); + + var headersNode = result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .ChildNodes.Should().ContainSingle().Which; + + var headerNodes = headersNode.HeaderNodes.ToArray(); + headerNodes.Should().HaveCount(5); + + headerNodes[0].NameNode.Text.Should().Be("Accept"); + headerNodes[0].ValueNode.Text.Should().Be("*/*"); + + headerNodes[1].NameNode.Text.Should().Be("Accept-Encoding"); + headerNodes[1].ValueNode.Text.Should().Be("gzip, deflate, br"); + + headerNodes[2].NameNode.Text.Should().Be("Accept-Language"); + headerNodes[2].ValueNode.Text.Should().Be("en-US,en;q=0.9"); + + headerNodes[3].NameNode.Text.Should().Be("ContentLength"); + headerNodes[3].ValueNode.Text.Should().Be("7060"); + + headerNodes[4].NameNode.Text.Should().Be("Cookie"); + headerNodes[4].ValueNode.Text.Should().Be("expor=;HSD=Ak_1ZasdqwASDASD;SSID=SASASSDFsdfsdf213123;APISID=WRQWRQWRQWRcc123123;"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs new file mode 100644 index 0000000000..63a9ff0ed6 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Microsoft.DotNet.Interactive.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Lexer + { + [Fact] + public void multiple_whitespaces_are_treated_as_a_single_token() + { + var result = Parse(" \t "); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.ChildTokens.First().Should().BeOfType(); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.ChildTokens.Single().TextWithTrivia.Should().Be(" \t "); + } + + [Fact] + public void multiple_newlines_are_parsed_into_different_tokens() + { + var result = Parse("\n\v\r\n\n"); + + result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which + .MethodNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( + new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }, + new { TextWithTrivia = "\v", Kind = HttpTokenKind.NewLine }, + new { TextWithTrivia = "\r\n", Kind = HttpTokenKind.NewLine }, + new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }); + } + + [Fact] + public void multiple_punctuations_are_parsed_into_different_tokens() + { + var result = Parse(".!?.:/"); + result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which + .UrlNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( + new { TextWithTrivia = ".", Kind = HttpTokenKind.Punctuation }, + new { TextWithTrivia = "!", Kind = HttpTokenKind.Punctuation }, + new { TextWithTrivia = "?", Kind = HttpTokenKind.Punctuation }, + new { TextWithTrivia = ".", Kind = HttpTokenKind.Punctuation }, + new { TextWithTrivia = ":", Kind = HttpTokenKind.Punctuation }, + new { TextWithTrivia = "/", Kind = HttpTokenKind.Punctuation }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs new file mode 100644 index 0000000000..e1b397e26e --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Method + { + [Fact] + public void whitespace_is_legal_at_the_beginning_of_a_request() + { + var result = Parse(" GET https://example.com"); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.ChildTokens.First().Kind + .Should().Be(HttpTokenKind.Whitespace); + } + + [Fact] + public void newline_is_legal_at_the_beginning_of_a_request() + { + var result = Parse( + """ + + GET https://example.com + """); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); + } + + [Theory] + [InlineData("GET https://example.com", "GET")] + [InlineData("POST https://example.com", "POST")] + [InlineData("OPTIONS https://example.com", "OPTIONS")] + [InlineData("TRACE https://example.com", "TRACE")] + public void common_verbs_are_parsed_correctly(string line, string method) + { + var result = Parse(line); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.Text.Should().Be(method); + } + + [Theory] + [InlineData(@"GET https://example.com", "GET")] + [InlineData(@"Get https://example.com", "Get")] + [InlineData(@"OPTIONS https://example.com", "OPTIONS")] + [InlineData(@"options https://example.com", "options")] + public void it_can_parse_verbs_regardless_of_their_casing(string line, string method) + { + var result = Parse(line); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.Text.Should().Be(method); + } + + [Fact] + public void diagnostic_object_is_reported_for_unrecognized_verb() + { + var result = Parse("OOOOPS https://example.com"); + + result.GetDiagnostics() + .Should().ContainSingle().Which.Message.Should().Be("Unrecognized HTTP verb OOOOPS"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs new file mode 100644 index 0000000000..77cbfab580 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs @@ -0,0 +1,126 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using System.Net.Http; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Microsoft.DotNet.Interactive.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Request + { + [Fact] + public void multiple_request_are_parsed_correctly() + { + var result = Parse( + """ + GET https://example.com + + ### + + GET https://example1.com + + ### + + GET https://example2.com + """); + + var requestNodes = result.SyntaxTree.RootNode + .ChildNodes.OfType(); + + requestNodes.Select(r => r.Text).Should() + .BeEquivalentSequenceTo(new[] + { + "GET https://example.com", + "GET https://example1.com", + "GET https://example2.com" + }); + } + + [Fact] + public void request_node_containing_only_url_and_no_variable_expressions_returns_HttpRequestMessage_with_GET_method() + { + var result = Parse( + """ + https://example.com + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var bindingResult = requestNode.TryGetHttpRequestMessage(node => node.CreateBindingFailure("oops")); + + bindingResult.IsSuccessful.Should().BeTrue(); + bindingResult.Value.RequestUri.ToString().Should().Be("https://example.com/"); + bindingResult.Value.Method.Should().Be(HttpMethod.Get); + } + + [Fact] + public void request_node_containing_method_and_url_and_no_variable_expressions_returns_HttpRequestMessage_with_specified_method() + { + var result = Parse( + """ + POST https://example.com + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var bindingResult = requestNode.TryGetHttpRequestMessage(node => node.CreateBindingFailure("oops")); + + bindingResult.IsSuccessful.Should().BeTrue(); + bindingResult.Value.RequestUri.ToString().Should().Be("https://example.com/"); + bindingResult.Value.Method.Should().Be(HttpMethod.Post); + } + + [Fact] + public void request_node_binds_variable_expressions_in_url() + { + var result = Parse( + """ + https://{{host}}/api/{{version}}comments/1 + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var bindingResult = requestNode.TryGetHttpRequestMessage(node => + { + return node.Text switch + { + "host" => node.CreateBindingSuccess("example.com"), + "version" => node.CreateBindingSuccess("123-") + }; + }); + + bindingResult.IsSuccessful.Should().BeTrue(); + bindingResult.Value.RequestUri.ToString().Should().Be("https://example.com/api/123-comments/1"); + } + + [Fact] + public void error_is_reported_for_undefined_variable() + { + var result = Parse( + """ + GET https://example.com/api/{{version}}comments/1 + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var message = "Variable 'version' was not defined."; + + HttpBindingDelegate bind = node => node.CreateBindingFailure(message); + + var bindingResult = requestNode.TryGetHttpRequestMessage(bind); + bindingResult.IsSuccessful.Should().BeFalse(); + bindingResult.Diagnostics.Should().ContainSingle().Which.Message.Should().Be(message); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs new file mode 100644 index 0000000000..c406ffb66e --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Trivia + { + [Fact] + public void it_can_parse_an_empty_string() + { + var result = Parse(""); + result.SyntaxTree.Should().BeNull(); + + // TODO: Test error reporting. + } + + [Fact] + public void it_can_parse_a_string_with_only_whitespace() + { + var result = Parse(" \t "); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.ChildTokens.First().TextWithTrivia.Should().Be(" \t "); + + // TODO: Test error reporting. + } + + [Fact] + public void it_can_parse_a_string_with_only_newlines() + { + var result = Parse("\r\n\n\r\n"); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .MethodNode.TextWithTrivia.Should().Be("\r\n\n\r\n"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Url.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Url.cs new file mode 100644 index 0000000000..1c8c874c25 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Url.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Url + { + [Fact] + public void whitespace_is_legal_after_url() + { + var result = Parse("GET https://example.com "); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .UrlNode.ChildTokens.Last().Kind.Should().Be(HttpTokenKind.Whitespace); + } + + [Fact] + public void newline_is_legal_at_the_after_url() + { + var result = Parse( + """ + GET https://example.com + + """); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .UrlNode.ChildTokens.Last().Kind.Should().Be(HttpTokenKind.NewLine); + } + + [Theory] + [InlineData("https://example.com?hat&ost=foo")] + [InlineData("https://example.com?q=3081#blah-2%203")] + public void common_url_structures_are_parsed_correctly(string url) + { + var result = Parse($"GET {url}"); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .UrlNode.Text.Should().Be(url); + } + + [Fact] + public void request_node_without_method_node_created_correctly() + { + var result = Parse("https://example.com"); + + result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which + .MethodNode.Should().BeNull(); + } + + [Fact] + public void url_node_can_return_url() + { + var result = Parse( + """ + GET https://{{host}}/api/{{version}}comments/1 + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var urlNode = requestNode.UrlNode; + var bindingResult = urlNode.TryGetUri(node => + { + return node.Text switch + { + "host" => node.CreateBindingSuccess("example.com"), + "version" => node.CreateBindingSuccess("123-") + }; + }); + + bindingResult.IsSuccessful.Should().BeTrue(); + bindingResult.Value.ToString().Should().Be("https://example.com/api/123-comments/1"); + } + + [Fact] + public void error_is_reported_for_undefined_variable() + { + var result = Parse( + """ + GET https://example.com/api/{{version}}comments/1 + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + var urlNode = requestNode.UrlNode; + + var message = "Variable 'version' was not defined."; + + HttpBindingDelegate bind = node => node.CreateBindingFailure(message); + + var bindingResult = urlNode.TryGetUri(bind); + bindingResult.IsSuccessful.Should().BeFalse(); + bindingResult.Diagnostics.Should().ContainSingle().Which.Message.Should().Be(message); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Variables.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Variables.cs new file mode 100644 index 0000000000..4e9f96ab72 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Variables.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Microsoft.DotNet.Interactive.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Variables + { + [Fact] + public void expression_is_parsed_correctly() + { + var result = Parse( + """ + GET https://{{host}}/api/{{version}}comments/1 HTTP/1.1 + Authorization: {{token}} + """); + + var requestNode = result.SyntaxTree.RootNode.ChildNodes + .Should().ContainSingle().Which; + + requestNode.UrlNode.DescendantNodesAndTokens().OfType().Select(e => e.Text) + .Should().BeEquivalentSequenceTo(new[] { "host", "version" }); + + requestNode.HeadersNode.DescendantNodesAndTokens().OfType() + .Should().ContainSingle().Which.Text.Should().Be("token"); + } + + //TODO Test all parsers for expression + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Version.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Version.cs new file mode 100644 index 0000000000..a6b46f005c --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Version.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; +using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; +using Xunit; + +namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; + +public partial class ParserTests +{ + public class Version + { + [Fact] + public void http_version_is_parsed_correctly() + { + var result = Parse("GET https://example.com HTTP/1.1"); + + result.SyntaxTree.RootNode + .ChildNodes.Should().ContainSingle().Which + .VersionNode.Text.Should().Be("HTTP/1.1"); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs index e8c661ffae..8a0a082781 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs @@ -2,546 +2,39 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Linq; -using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Execution; -using Microsoft.DotNet.Interactive.HttpRequest.Tests.Utility; -using Microsoft.DotNet.Interactive.Tests.Utility; -using Xunit; namespace Microsoft.DotNet.Interactive.HttpRequest.Tests; -public class ParserTests : IDisposable +public partial class ParserTests : IDisposable { + private readonly AssertionScope _assertionScope; public ParserTests() { - assertionScope = new AssertionScope(); + _assertionScope = new AssertionScope(); } public void Dispose() { - assertionScope.Dispose(); + _assertionScope.Dispose(); } - private readonly AssertionScope assertionScope; private static HttpRequestParseResult Parse(string code) { var result = HttpRequestParser.Parse(code); - if (result.SyntaxTree is not null && result.SyntaxTree.RootNode is not null) + if (result.SyntaxTree?.RootNode is not null) { result.SyntaxTree.RootNode.TextWithTrivia.Should().Be(code); } - return result; - } - - public class Trivia - { - [Fact] - public void it_can_parse_an_empty_string() - { - var result = Parse(""); - result.SyntaxTree.Should().BeNull(); - - // TODO: Test error reporting. - } - - [Fact] - public void it_can_parse_a_string_with_only_whitespace() - { - var result = Parse(" \t "); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.First().TextWithTrivia.Should().Be(" \t "); - - // TODO: Test error reporting. - } - - [Fact] - public void it_can_parse_a_string_with_only_newlines() - { - var result = Parse("\r\n\n\r\n"); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.TextWithTrivia.Should().Be("\r\n\n\r\n"); - - // TODO: Test error reporting. - // Regardless of number of newlines, there should be consistent nodes - // set of whitespace, newline, and punctuation - //each token kind - //combination of characters should be checked by the lexer - } - } - - - public class Lexer - { - [Fact] - - public void multiple_whitespaces_are_treated_as_a_single_token() - { - var result = Parse(" \t "); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.First().Should().BeOfType(); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.Single().TextWithTrivia.Should().Be(" \t "); - } - - [Fact] - public void multiple_newlines_are_parsed_into_different_tokens() - { - var result = Parse("\n\v\r\n\n"); - - result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( - new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\v", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\r\n", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }); - } - - [Fact] - public void multiple_punctuations_are_parsed_into_different_tokens() - { - var result = Parse(".!?.:/"); - result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which - .UrlNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( - new { TextWithTrivia = ".", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = "!", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = "?", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = ".", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = ":", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = "/", Kind = HttpTokenKind.Punctuation }); - - } - } - - public class Method - { - [Fact] - public void whitespace_is_legal_at_the_beginning_of_a_request() - { - var result = Parse(" GET https://example.com"); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.First().Kind - .Should().Be(HttpTokenKind.Whitespace); - } - - [Fact] - public void newline_is_legal_at_the_beginning_of_a_request() - { - var result = Parse( - """ - - GET https://example.com - """); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); - } - - - [Fact] - public void whitespace_is_legal_at_the_end_of_a_request() - { - var result = Parse("GET https://example.com "); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .UrlNode.ChildTokens.Last().Kind.Should().Be(HttpTokenKind.Whitespace); - } - - [Fact] - public void newline_is_legal_at_the_end_of_a_request() - { - var result = Parse( - """ - GET https://example.com - - """); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .UrlNode.ChildTokens.Last().Kind.Should().Be(HttpTokenKind.NewLine); - } - - [Theory] - [InlineData("GET https://example.com", "GET")] - [InlineData("POST https://example.com", "POST")] - [InlineData("OPTIONS https://example.com", "OPTIONS")] - [InlineData("TRACE https://example.com", "TRACE")] - public void common_verbs_are_parsed_correctly(string line, string method) - { - var result = Parse(line); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.Text.Should().Be(method); - } - - [Theory] - [InlineData("https://example.com?hat&ost=foo")] - [InlineData("https://example.com?q=3081#blah-2%203")] - public void common_url_structures_are_parsed_correctly(string url) - { - var result = Parse($"GET {url}"); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .UrlNode.Text.Should().Be(url); - } - - [Theory] - [InlineData(@"GET https://example.com", "GET")] - [InlineData(@"Get https://example.com", "Get")] - [InlineData(@"OPTIONS https://example.com", "OPTIONS")] - [InlineData(@"options https://example.com", "options")] - public void it_can_parse_verbs_regardless_of_their_casing(string line, string method) - { - var result = Parse(line); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.Text.Should().Be(method); - } - - [Fact] - public void http_version_is_parsed_correctly() - { - var result = Parse("GET https://example.com HTTP/1.1"); - - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .VersionNode.Text.Should().Be("HTTP/1.1"); - } - [Fact] - public void diagnostic_object_is_reported_for_unrecognized_verb() - { - var result = Parse("OOOOPS https://example.com"); - - result.GetDiagnostics() - .Should().ContainSingle().Which.Message.Should().Be("Unrecognized HTTP verb OOOOPS"); - } - - [Fact] - public void request_node_without_method_node_created_correctly() - { - var result = Parse("https://example.com"); - - result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which - .MethodNode.Should().BeNull(); - } - - [Fact] - public void url_node_can_give_url() - { - var result = Parse( - """ - GET https://{{host}}/api/{{version}}comments/1 - """); - - var requestNode = result.SyntaxTree.RootNode.ChildNodes - .Should().ContainSingle().Which; - - var urlNode = requestNode.UrlNode; - urlNode.GetUri(x => x.Text switch - { - "host" => "example.com", - "version" => "123-", - _ => throw new NotImplementedException() - }).ToString().Should().Be("https://example.com/api/123-comments/1"); - } - /* - [Fact] - public void error_is_reported_for_incorrect_uri() - { - var result = Parse( - """ - GET https://{{host}}/api/{{version}}comments/1 - """); - - var requestNode = result.SyntaxTree.RootNode.ChildNodes - .Should().ContainSingle().Which; - - var urlNode = requestNode.UrlNode; - urlNode.TryGetUri(x => x.Text switch - { - "host" => "example.com" - }).Should().BeFalse(); - - }*/ - } - - public class Headers - { - [Fact] - public void header_with_body_is_parsed_correctly() - { - var result = Parse( - """ - POST https://example.com/comments HTTP/1.1 - Content-Type: application/xml - Authorization: token xxx - - - sample - - - """); - - var requestNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which; - - requestNode.HeadersNode.HeaderNodes.Count.Should().Be(2); - requestNode.BodyNode.Text.Should().Be( - """ - - sample - - - """); - } - - [Fact] - public void header_separator_is_present() - { - var result = Parse( - """ - POST https://example.com/comments HTTP/1.1 - Content-Type: application - """); - - var requestNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which; - - requestNode.HeadersNode.HeaderNodes.Single().SeparatorNode.Text.Should().Be(":"); - } - - [Fact] - public void headers_are_parsed_correctly() - { - var result = Parse( - """ - GET https://example.com HTTP/1.1 - Accept: */* - Accept-Encoding : gzip, deflate, br - Accept-Language : en-US,en;q=0.9 - ContentLength:7060 - Cookie: expor=;HSD=Ak_1ZasdqwASDASD;SSID=SASASSDFsdfsdf213123;APISID=WRQWRQWRQWRcc123123; - """); - - var headersNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .ChildNodes.Should().ContainSingle().Which; - - var headerNodes = headersNode.HeaderNodes.ToArray(); - headerNodes.Should().HaveCount(5); - - headerNodes[0].NameNode.Text.Should().Be("Accept"); - headerNodes[0].ValueNode.Text.Should().Be("*/*"); - - headerNodes[1].NameNode.Text.Should().Be("Accept-Encoding"); - headerNodes[1].ValueNode.Text.Should().Be("gzip, deflate, br"); - - headerNodes[2].NameNode.Text.Should().Be("Accept-Language"); - headerNodes[2].ValueNode.Text.Should().Be("en-US,en;q=0.9"); - - headerNodes[3].NameNode.Text.Should().Be("ContentLength"); - headerNodes[3].ValueNode.Text.Should().Be("7060"); - - headerNodes[4].NameNode.Text.Should().Be("Cookie"); - headerNodes[4].ValueNode.Text.Should().Be("expor=;HSD=Ak_1ZasdqwASDASD;SSID=SASASSDFsdfsdf213123;APISID=WRQWRQWRQWRcc123123;"); - } - } - - public class Body - { - [Fact] - public void body_separator_is_present() - { - var result = Parse( - """ - POST https://example.com/comments HTTP/1.1 - Content-Type: application/xml - Authorization: token xxx - - - sample - - - """); - - var requestNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which; - - requestNode.BodySeparatorNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); - } - - [Fact] - public void when_headers_are_not_present_there_should_be_no_header_nodes() - { - var result = Parse( - """ - POST https://example.com/comments HTTP/1.1 - - - sample - - - """); - - - var requestNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which; - - requestNode.HeadersNode.Should().BeNull(); - } - - [Fact] - public void body_is_parsed_correctly_when_headers_are_not_present() - { - var result = Parse( - """ - POST https://example.com/comments HTTP/1.1 - - - sample - - - """); - - - var requestNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which; - - requestNode.BodyNode.Text.Should().Be( - """ - - sample - - - """); - } - - [Fact] - public void multiple_new_lines_before_body_are_parsed_correctly() - { - var result = Parse( - """ - POST https://example.com/comments HTTP/1.1 - Content-Type: application/xml - Authorization: token xxx - - - - - - sample - - - """); - - - var requestNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which; - - requestNode.BodyNode.Text.Should().Be( - """ - - sample - - - """); - } - } - - public class Comment - { - [Fact] - public void comments_are_parsed_correctly() - { - - var result = Parse( - """ - # This is a comment - GET https://example.com HTTP/1.1" - """); - - - var methodNode = result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode; - - methodNode.ChildNodes.Should().ContainSingle().Which.Text.Should().Be( - "# This is a comment"); - - - } - } - - public class Tree - { - [Fact] - public void multiple_request_are_parsed_correctly() - { - var result = Parse( - """ - GET https://example.com - - ### - - GET https://example1.com - - ### - - GET https://example2.com - """); - - - var requestNodes = result.SyntaxTree.RootNode - .ChildNodes.OfType(); - - requestNodes.Select(r => r.Text).Should() - .BeEquivalentSequenceTo(new[] { "GET https://example.com", - "GET https://example1.com", "GET https://example2.com"}); - } + return result; } +} - public class Variables - { - [Fact] - public void expression_is_parsed_correctly() - { - var result = Parse( - """ - GET https://{{host}}/api/{{version}}comments/1 HTTP/1.1 - Authorization: {{token}} - """); - - var requestNode = result.SyntaxTree.RootNode.ChildNodes - .Should().ContainSingle().Which; - - requestNode.UrlNode.DescendantNodesAndTokens().OfType().Select(e => e.Text) - .Should().BeEquivalentSequenceTo(new[] { "host", "version" }); - requestNode.HeadersNode.DescendantNodesAndTokens().OfType() - .Should().ContainSingle().Which.Text.Should().Be("token"); - } - //TODO Test all parsers for expression - } -} // TODO: Test string with variable declarations but no requests /* @@ -702,4 +195,4 @@ public async Task HeaderMissingValue_ShouldBeTreatedAsBody_WithError() Assert.AreEqual(string.Format(Strings.HttpHeaderMissingValue, "foo:"), doc.Items[3].Errors[0].Message); } -}*/ +}*/ \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj b/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj index aa46df844d..63b71cf4bc 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj +++ b/src/Microsoft.DotNet.Interactive.HttpRequest/Microsoft.DotNet.Interactive.HttpRequest.csproj @@ -19,6 +19,10 @@ + + + + diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingDelegate.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingDelegate.cs new file mode 100644 index 0000000000..094dfef51f --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingDelegate.cs @@ -0,0 +1,7 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal delegate HttpBindingResult HttpBindingDelegate(HttpExpressionNode node); \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingResult.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingResult.cs new file mode 100644 index 0000000000..256d17bd28 --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBindingResult.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable enable +using System; +using System.Collections.Generic; + +namespace Microsoft.DotNet.Interactive.HttpRequest; + +internal class HttpBindingResult +{ + private HttpBindingResult() + { + } + + public List Diagnostics { get; } = new(); + public bool IsSuccessful { get; private set; } + + public T? Value { get; set; } + + public static HttpBindingResult Success(T value) => new() + { + IsSuccessful = true, + Value = value + }; + + public static HttpBindingResult Failure(params Diagnostic[] diagnostics) + { + if (diagnostics is null) + { + throw new ArgumentNullException(nameof(diagnostics)); + } + + if (diagnostics.Length == 0) + { + throw new ArgumentException("Value cannot be an empty collection.", nameof(diagnostics)); + } + + var result = new HttpBindingResult + { + IsSuccessful = false + }; + + result.Diagnostics.AddRange(diagnostics); + + return result; + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs index 6a987eab52..1cfc49892b 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpEmbeddedExpressionNode.cs @@ -5,8 +5,6 @@ using Microsoft.CodeAnalysis.Text; -using System.Collections.Generic; - namespace Microsoft.DotNet.Interactive.HttpRequest; internal class HttpEmbeddedExpressionNode : HttpSyntaxNode @@ -31,5 +29,4 @@ internal HttpEmbeddedExpressionNode( public HttpExpressionStartNode StartNode { get; } public HttpExpressionNode ExpressionNode { get; } public HttpExpressionEndNode EndNode { get; } - -} +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs index 968602d0f6..21b0c7a970 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpExpressionNode.cs @@ -12,4 +12,10 @@ internal class HttpExpressionNode : HttpSyntaxNode internal HttpExpressionNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) { } -} + + public HttpBindingResult CreateBindingFailure(string message) => + HttpBindingResult.Failure(CreateDiagnostic(message)); + + public HttpBindingResult CreateBindingSuccess(object? value) => + HttpBindingResult.Success(value); +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs index 65319ad557..bdb35a4068 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs @@ -3,6 +3,8 @@ #nullable enable +using System.Collections.Generic; +using System.Net.Http; using Microsoft.CodeAnalysis.Text; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -19,14 +21,12 @@ internal HttpRequestNode( HttpBodySeparatorNode? bodySeparatorNode = null, HttpBodyNode? bodyNode = null) : base(sourceText, syntaxTree) { - if (methodNode is not null) { MethodNode = methodNode; Add(MethodNode); } - UrlNode = urlNode; Add(UrlNode); @@ -40,9 +40,9 @@ internal HttpRequestNode( { HeadersNode = headersNode; Add(HeadersNode); - } + } - if(bodySeparatorNode is not null) + if (bodySeparatorNode is not null) { BodySeparatorNode = bodySeparatorNode; Add(bodySeparatorNode); @@ -66,4 +66,36 @@ internal HttpRequestNode( public HttpBodySeparatorNode? BodySeparatorNode { get; } public HttpBodyNode? BodyNode { get; } -} + + public HttpBindingResult TryGetHttpRequestMessage(HttpBindingDelegate bind) + { + var request = new HttpRequestMessage(); + var diagnostics = new List(); + var success = true; + + if (MethodNode is { Span.IsEmpty: false }) + { + request.Method = new HttpMethod(MethodNode.Text); + } + + var uriBindingResult = UrlNode.TryGetUri(bind); + if (uriBindingResult.IsSuccessful) + { + request.RequestUri = uriBindingResult.Value; + } + else + { + success = false; + diagnostics.AddRange(uriBindingResult.Diagnostics); + } + + if (success) + { + return HttpBindingResult.Success(request); + } + else + { + return HttpBindingResult.Failure(diagnostics.ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs index 045b2c43bb..5a4ef6712f 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs @@ -3,10 +3,8 @@ #nullable enable -using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -17,21 +15,8 @@ public HttpRequestParseResult(HttpSyntaxTree? syntaxTree) public HttpSyntaxTree? SyntaxTree { get; } - public IEnumerable GetHttpRequestMessages() - { - if (SyntaxTree?.RootNode is { } rootNode) - { - foreach (var requestNode in rootNode.ChildNodes.OfType()) - { - yield return new HttpRequestMessage( - new HttpMethod(requestNode.MethodNode?.Text ?? "GET"), - new Uri(@"https://something.we.wont.use.in.our.tests.com")); - } - } - } - public IEnumerable GetDiagnostics() { return SyntaxTree?.RootNode?.GetDiagnostics() ?? Enumerable.Empty(); } -} +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs index 332e88ca77..a14050bec3 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs @@ -5,7 +5,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using System; using System.Collections.Generic; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -95,7 +94,6 @@ private void ConsumeCurrentTokenInto(HttpSyntaxNode node) AdvanceToNextToken(); } - private T ParseLeadingTrivia(T node) where T : HttpSyntaxNode { while (MoreTokens()) @@ -212,14 +210,14 @@ private HttpRequestNode ParseRequest() } else { - var tokenSpan = _sourceText.GetSubText(CurrentToken.Span).Lines.GetLinePositionSpan(CurrentToken.Span); + var message = $"Unrecognized HTTP verb {CurrentToken.Text}"; + + var diagnostic = CurrentToken.CreateDiagnostic(message); - var diagnostic = new Diagnostic(LinePositionSpan.FromCodeAnalysisLinePositionSpan(tokenSpan), DiagnosticSeverity.Warning, CurrentToken.Text, $"Unrecognized HTTP verb {CurrentToken.Text}"); node.AddDiagnostic(diagnostic); + ConsumeCurrentTokenInto(node); } - - } return ParseTrailingTrivia(node); diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs index 6079f6c1e2..ce1dd2810a 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs @@ -4,20 +4,22 @@ #nullable enable using System.Collections.Generic; -using System.Linq; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace Microsoft.DotNet.Interactive.HttpRequest; internal abstract class HttpSyntaxNodeOrToken { + protected List? _diagnostics = null; + private protected HttpSyntaxNodeOrToken(SourceText sourceText, HttpSyntaxTree? syntaxTree) { SourceText = sourceText; SyntaxTree = syntaxTree; } - protected SourceText SourceText { get; } + public SourceText SourceText { get; } public HttpSyntaxNode? Parent { get; internal set; } @@ -25,8 +27,6 @@ private protected HttpSyntaxNodeOrToken(SourceText sourceText, HttpSyntaxTree? s public HttpSyntaxTree? SyntaxTree { get; } - protected List? _diagnostics = null; - /// /// Gets the significant text of the current node or token, without trivia. /// @@ -35,7 +35,7 @@ private protected HttpSyntaxNodeOrToken(SourceText sourceText, HttpSyntaxTree? s /// /// Gets the text of the current node or token, including trivia. /// - public string TextWithTrivia => SourceText.ToString(Span); + public string TextWithTrivia => SourceText.ToString(Span); public override string ToString() => $"{GetType().Name}: {Text}"; @@ -43,9 +43,18 @@ private protected HttpSyntaxNodeOrToken(SourceText sourceText, HttpSyntaxTree? s public void AddDiagnostic(Diagnostic d) { - _diagnostics ??= new List(); + _diagnostics ??= new List(); _diagnostics.Add(d); - } -} + public Diagnostic CreateDiagnostic(string message) + { + var lines = SourceText.Lines; + + var tokenSpan = lines.GetLinePositionSpan(Span); + + var diagnostic = new Diagnostic(LinePositionSpan.FromCodeAnalysisLinePositionSpan(tokenSpan), DiagnosticSeverity.Warning, Text, message); + + return diagnostic; + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs index 237e1962ce..cbcbf20a4d 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs @@ -3,9 +3,10 @@ #nullable enable -using Microsoft.CodeAnalysis.Text; using System; +using System.Collections.Generic; using System.Text; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -14,19 +15,47 @@ internal class HttpUrlNode : HttpSyntaxNode internal HttpUrlNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) { } - - internal Uri GetUri(Func value) + + internal HttpBindingResult TryGetUri(HttpBindingDelegate bind) { var urlText = new StringBuilder(); - foreach(var node in ChildNodesAndTokens) + var diagnostics = new List(); + var success = true; + + foreach (var node in ChildNodesAndTokens) { - urlText.Append(node switch + if (node is HttpEmbeddedExpressionNode n) + { + var innerResult = bind(n.ExpressionNode); + + if (innerResult.IsSuccessful) + { + var nodeText = innerResult.Value?.ToString(); + urlText.Append(nodeText); + } + else + { + success = false; + } + + diagnostics.AddRange(innerResult.Diagnostics); + } + else { - HttpEmbeddedExpressionNode n => value(n.ExpressionNode), - _ => node.Text - }); + urlText.Append(node.Text); + } + } + + if (success) + { + var uri = new Uri(urlText.ToString(), UriKind.Absolute); + + return HttpBindingResult.Success(uri); + } + else + { + return HttpBindingResult.Failure(diagnostics.ToArray()); } - return new Uri(urlText.ToString(), UriKind.Absolute); } -} +} \ No newline at end of file From 02f8dc8ee1ad3ce91ae58c869ed560deb400d700 Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Mon, 7 Aug 2023 13:11:10 +0100 Subject: [PATCH 07/13] change publishing script to use allow list --- eng/publish/PublishVSCodeExtension.ps1 | 42 +++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/eng/publish/PublishVSCodeExtension.ps1 b/eng/publish/PublishVSCodeExtension.ps1 index dc04aa2b0b..c27176f1f8 100644 --- a/eng/publish/PublishVSCodeExtension.ps1 +++ b/eng/publish/PublishVSCodeExtension.ps1 @@ -32,15 +32,43 @@ try { # publish nuget if ($vscodeTarget -eq "stable") { + $packagestoPublish = @( + "Microsoft.dotnet-interactive", + "Microsoft.DotNet.Interactive", + "Microsoft.DotNet.Interactive.AspNetCore", + "Microsoft.DotNet.Interactive.Browser", + "Microsoft.DotNet.Interactive.CSharp", + "Microsoft.DotNet.Interactive.Documents", + "Microsoft.DotNet.Interactive.ExtensionLab", + "Microsoft.DotNet.Interactive.Formatting", + "Microsoft.DotNet.Interactive.FSharp", + "Microsoft.DotNet.Interactive.Http,", + "Microsoft.DotNet.Interactive.HttpRequest", + "Microsoft.DotNet.Interactive.Journey", + "Microsoft.DotNet.Interactive.Kql", + "Microsoft.DotNet.Interactive.Mermaid", + "Microsoft.DotNet.Interactive.PackageManagement", + "Microsoft.DotNet.Interactive.PowerShell", + "Microsoft.DotNet.Interactive.SQLite", + "Microsoft.DotNet.Interactive.SqlServer" + ) + Get-ChildItem "$artifactsPath\packages\Shipping\Microsoft.DotNet*.nupkg" | ForEach-Object { $nugetPackagePath = $_.ToString() - # don't publish asp or netstandard packages - if (-Not ($nugetPackagePath -match "(CSharpProject|VisualStudio)")) { - Write-Host "Publishing $nugetPackagePath" - if (-Not $simulate) { - dotnet nuget push $nugetPackagePath --source https://api.nuget.org/v3/index.json --api-key $nugetToken --no-symbols - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + $nugetPacakgeName = $_.Name + if ($nugetPacakgeName -match '(?<=(?.+))\.(?((\d+\.\d+(\.\d+)?))(?(-.*)?))\.nupkg') + { + $packageId = $Matches.id + $packageVersion = $Matches.version + + # publish only listed packages + if ($packagestoPublish.Contains($packageId)) { + Write-Host "Publishing $nugetPackagePath" + if (-Not $simulate) { + dotnet nuget push $nugetPackagePath --source https://api.nuget.org/v3/index.json --api-key $nugetToken --no-symbols + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } } } } From cb40a33fb89d0f3ba584917812efc536ed451935 Mon Sep 17 00:00:00 2001 From: Osvaldo Calles <510598+ocallesp@users.noreply.github.com> Date: Wed, 9 Aug 2023 15:09:46 -0600 Subject: [PATCH 08/13] Refactor build and package scripts, and improve cleanup (#3129) Co-authored-by: Osvaldo Calles --- repack.ps1 | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/repack.ps1 b/repack.ps1 index 7b3edad2f4..5fc0cc9c71 100644 --- a/repack.ps1 +++ b/repack.ps1 @@ -8,8 +8,17 @@ dotnet build dotnet pack /p:PackageVersion=2.0.0 # copy the dotnet-interactive packages to the temp directory -Get-ChildItem -Recurse -Filter *.nupkg | Move-Item -Destination c:\temp\packages -Force +$destinationPath = "C:\temp\packages" +if (-not (Test-Path -Path $destinationPath -PathType Container)) { + New-Item -Path $destinationPath -ItemType Directory -Force +} +Get-ChildItem -Recurse -Filter *.nupkg | Move-Item -Destination $destinationPath -Force # delete the #r nuget caches -Remove-Item -Recurse -Force ~\.packagemanagement\nuget\Cache -Remove-Item -Recurse -Force ~\.packagemanagement\nuget\Projects +if (Test-Path -Path ~\.packagemanagement\nuget\Cache -PathType Container) { + Remove-Item -Recurse -Force ~\.packagemanagement\nuget\Cache +} + +if (Test-Path -Path ~\.packagemanagement\nuget\Projects -PathType Container) { + Remove-Item -Recurse -Force ~\.packagemanagement\nuget\Projects +} From 8aa65342a3a32e92e28501bbc7f1b578d3dd7a7a Mon Sep 17 00:00:00 2001 From: "dotnet-maestro[bot]" <42748379+dotnet-maestro[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:16:17 -0700 Subject: [PATCH 09/13] Update dependencies from https://github.com/dotnet/arcade build 20230808.3 (#3128) Microsoft.DotNet.Arcade.Sdk From Version 7.0.0-beta.23370.3 -> To Version 7.0.0-beta.23408.3 Co-authored-by: dotnet-maestro[bot] --- eng/Version.Details.xml | 4 ++-- global.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ca4f94a95c..d632459199 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -3,9 +3,9 @@ - + https://github.com/dotnet/arcade - c9c125ccc43361cd94433c2d7f7069d465ad11a5 + 3b8f3de4606c338f99e8ce85cfb6f960f6a428c8 diff --git a/global.json b/global.json index 1640b18381..c71ec846c0 100644 --- a/global.json +++ b/global.json @@ -1,14 +1,14 @@ { "sdk": { - "version": "7.0.109", + "version": "7.0.110", "allowPrerelease": true, "rollForward": "latestMinor" }, "tools": { - "dotnet": "7.0.109", + "dotnet": "7.0.110", "rollForward": "latestMinor" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.23370.3" + "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.23408.3" } } From a5548386d51df1464db44a7a5a17886d165a1f70 Mon Sep 17 00:00:00 2001 From: Jon Sequeira Date: Thu, 10 Aug 2023 07:12:26 -0700 Subject: [PATCH 10/13] update HttpRequestKernel to use new parser; support variable expressions in request body (#3122) * update kernel to use new parser; support expressions in req body * FullText and FullSpan; exclude comments from Span * pre-calculate IsSignificant and Span * prevent kernel from sending requests when errors are present * cleanup * remove ParsedHttpRequest type * make SyntaxTree.RootNode non-nullable * fix nullability warning * clean up tests to take kernel extension out of the loop * improve variable sharing, add support for RequestValueInfos --- ...ttpRequest_api_is_not_changed.approved.txt | 2 +- .../HttpRequestKernelTests.cs | 248 ++++++++------- .../ParserTests.Comments.cs | 11 +- .../ParserTests.Headers.cs | 4 +- .../ParserTests.Lexer.cs | 26 +- .../ParserTests.Method.cs | 40 ++- .../ParserTests.Request.cs | 1 - .../ParserTests.Trivia.cs | 10 +- .../ParserTests.cs | 2 +- .../Utility/AssertionExtensions.cs | 13 + .../HttpRequestKernel.cs | 290 ++++-------------- .../HttpBodyNode.cs | 44 ++- .../HttpCommentNode.cs | 3 +- .../HttpRequestNode.cs | 59 +++- .../HttpRequestParseResult.cs | 9 +- .../HttpRequestParser.cs | 47 +-- .../HttpSyntaxNode.cs | 69 +++-- .../HttpSyntaxNodeOrToken.cs | 19 +- .../HttpSyntaxToken.cs | 16 +- .../HttpSyntaxTree.cs | 10 +- .../HttpUrlNode.cs | 1 - .../Events/DiagnosticsProduced.cs | 3 +- 22 files changed, 464 insertions(+), 463 deletions(-) diff --git a/src/Microsoft.DotNet.Interactive.ApiCompatibility.Tests/ApiCompatibilityTests.httpRequest_api_is_not_changed.approved.txt b/src/Microsoft.DotNet.Interactive.ApiCompatibility.Tests/ApiCompatibilityTests.httpRequest_api_is_not_changed.approved.txt index ae2ebedfdf..285f3ca1a3 100644 --- a/src/Microsoft.DotNet.Interactive.ApiCompatibility.Tests/ApiCompatibilityTests.httpRequest_api_is_not_changed.approved.txt +++ b/src/Microsoft.DotNet.Interactive.ApiCompatibility.Tests/ApiCompatibilityTests.httpRequest_api_is_not_changed.approved.txt @@ -14,7 +14,7 @@ Microsoft.DotNet.Interactive.HttpRequest public System.String Method { get;} public System.String Uri { get;} public System.String Version { get;} - public class HttpRequestKernel : Microsoft.DotNet.Interactive.Kernel, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, System.IDisposable + public class HttpRequestKernel : Microsoft.DotNet.Interactive.Kernel, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, Microsoft.DotNet.Interactive.IKernelCommandHandler, System.IDisposable .ctor(System.String name = null, System.Net.Http.HttpClient client = null, System.Int32 responseDelayThresholdInMilliseconds = 1000, System.Int32 contentByteLengthThreshold = 500000) public class HttpRequestKernelExtension public static System.Void Load(Microsoft.DotNet.Interactive.Kernel kernel, System.Net.Http.HttpClient httpClient = null, System.Int32 responseDelayThresholdInMilliseconds = 1000, System.Int32 contentByteLengthThreshold = 500000) diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/HttpRequestKernelTests.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/HttpRequestKernelTests.cs index 5773bc28ae..a245bc640a 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/HttpRequestKernelTests.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/HttpRequestKernelTests.cs @@ -91,10 +91,12 @@ public async Task can_handle_multiple_request_in_a_single_submission() var client = new HttpClient(handler); using var kernel = new HttpRequestKernel(client: client); - var result = await kernel.SendAsync(new SubmitCode(@" -get https://location1.com:1200/endpoint - -put https://location2.com:1200/endpoint")); + var result = await kernel.SendAsync(new SubmitCode(""" + + get https://location1.com:1200/endpoint + ### + put https://location2.com:1200/endpoint + """)); using var _ = new AssertionScope(); @@ -116,9 +118,11 @@ public async Task can_set_request_headers() var client = new HttpClient(handler); using var kernel = new HttpRequestKernel(client: client); - var result = await kernel.SendAsync(new SubmitCode(@" -get https://location1.com:1200/endpoint -Authorization: Basic username password")); + var result = await kernel.SendAsync(new SubmitCode(""" + + get https://location1.com:1200/endpoint + Authorization: Basic username password + """)); using var _ = new AssertionScope(); @@ -147,7 +151,6 @@ public async Task can_set_body_from_single_line() Content-Type: application/json { "key" : "value", "list": [1, 2, 3] } - """)); using var _ = new AssertionScope(); @@ -196,13 +199,6 @@ public async Task can_set_body_from_multiline_text() """); } - [Fact(Skip = "Requires updates to HTTP parser")] - public void it_can_set_http_version() - { - // TODO (it_can_set_http_version) write test - throw new NotImplementedException(); - } - [Fact] public async Task can_set_contenttype_without_a_body() { @@ -216,11 +212,13 @@ public async Task can_set_contenttype_without_a_body() var client = new HttpClient(handler); using var kernel = new HttpRequestKernel(client: client); - var result = await kernel.SendAsync(new SubmitCode(@" -Get https://location1.com:1200/endpoint -Authorization: Basic username password -Content-Type: application/json -")); + var result = await kernel.SendAsync(new SubmitCode(""" + + Get https://location1.com:1200/endpoint + Authorization: Basic username password + Content-Type: application/json + + """)); using var _ = new AssertionScope(); result.Events.Should().NotContainErrors(); @@ -245,17 +243,18 @@ public async Task can_use_symbols_in_body() var result = await kernel.SendAsync(new SendValue("one", 1)); result.Events.Should().NotContainErrors(); - result = await kernel.SendAsync(new SubmitCode(@" -post https://location1.com:1200/endpoint -Authorization: Basic username password -Content-Type: application/json - -{ ""key"" : ""value"", ""list"": [{{one}}, 2, 3] } -")); + result = await kernel.SendAsync(new SubmitCode(""" + + post https://location1.com:1200/endpoint + Authorization: Basic username password + Content-Type: application/json + + { "key" : "value", "list": [{{one}}, 2, 3] } + """)); result.Events.Should().NotContainErrors(); var bodyAsString = await request.Content.ReadAsStringAsync(); - bodyAsString.Should().Be("{ \"key\" : \"value\", \"list\": [1, 2, 3] }"); + bodyAsString.Should().Be("""{ "key" : "value", "list": [1, 2, 3] }"""); } [Fact] @@ -276,9 +275,10 @@ public async Task comments_can_be_placed_before_a_variable_expanded_request() var result = await kernel.SendAsync(new SendValue("theHost", "example.com")); result.Events.Should().NotContainErrors(); - var code = @" -// something to ensure we're not on the first line -GET https://{{theHost}}"; + var code = """ + # something to ensure we're not on the first line + GET https://{{theHost}} + """; result = await kernel.SendAsync(new SubmitCode(code)); result.Events.Should().NotContainErrors(); @@ -299,19 +299,7 @@ public async Task diagnostic_positions_are_correct_for_unresolved_symbols_in_URL var diagnostics = result.Events.Should().ContainSingle().Which; - diagnostics.Diagnostics.First().Message.Should().Be(@"Cannot resolve symbol 'api_endpoint'"); - } - - [Fact(Skip = "Requires updates to HTTP parser")] - public void diagnostic_positions_are_correct_for_unresolved_symbols_in_request_body() - { - throw new NotImplementedException(); - } - - [Fact(Skip = "Requires updates to HTTP parser")] - public void diagnostic_positions_are_correct_for_unresolved_symbols_in_request_headers() - { - throw new NotImplementedException(); + diagnostics.Diagnostics.First().Message.Should().Be("Cannot resolve symbol 'api_endpoint'"); } [Fact] @@ -319,9 +307,11 @@ public async Task diagnostic_positions_are_correct_for_unresolved_symbols() { using var kernel = new HttpRequestKernel(); - var code = @" -// something to ensure we're not on the first line -GET https://example.com/{{unresolved_symbol}}"; + var code = """ + + // something to ensure we're not on the first line + GET https://example.com/{{unresolved_symbol}} + """; var result = await kernel.SendAsync(new RequestDiagnostics(code)); @@ -339,9 +329,11 @@ public async Task diagnostic_positions_are_correct_for_unresolved_symbols_after_ { using var kernel = new HttpRequestKernel(); - var code = @" -GET https://example.com/ -User-Agent: {{unresolved_symbol}}"; + var code = """ + + GET https://example.com/ + User-Agent: {{unresolved_symbol}} + """; var result = await kernel.SendAsync(new RequestDiagnostics(code)); @@ -359,9 +351,11 @@ public async Task multiple_diagnostics_are_returned_from_the_same_submission() { using var kernel = new HttpRequestKernel(); - var code = @" -GET {{missing_value_1}}/index.html -User-Agent: {{missing_value_2}}"; + var code = """ + + GET {{missing_value_1}}/index.html + User-Agent: {{missing_value_2}} + """; var result = await kernel.SendAsync(new RequestDiagnostics(code)); @@ -374,22 +368,36 @@ public async Task multiple_diagnostics_are_returned_from_the_same_submission() diagnostics.Diagnostics.Should().HaveCount(2); } + [Fact] + public async Task when_error_diagnostics_are_present_then_request_is_not_sent() + { + var messageWasSent = false; + var handler = new InterceptingHttpMessageHandler((_, _) => + { + messageWasSent = true; + throw new Exception(); + }); + var client = new HttpClient(handler); + + using var kernel = new HttpRequestKernel(client:client); + + await kernel.SendAsync(new SubmitCode("OOPS http://testuri.ninja")); + + messageWasSent.Should().BeFalse(); + } + [Fact] public async Task produces_html_formatted_display_value() { - HttpRequestMessage request = null; var handler = new InterceptingHttpMessageHandler((message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; return Task.FromResult(response); }); var client = new HttpClient(handler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel(client: client); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -401,19 +409,15 @@ public async Task produces_html_formatted_display_value() [Fact] public async Task produces_json_formatted_return_value() { - HttpRequestMessage request = null; var handler = new InterceptingHttpMessageHandler((message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; return Task.FromResult(response); }); var client = new HttpClient(handler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -425,21 +429,17 @@ public async Task produces_json_formatted_return_value() [Fact] public async Task display_should_be_suppressed_for_return_value() { - HttpRequestMessage request = null; var handler = new InterceptingHttpMessageHandler((message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; return Task.FromResult(response); }); var client = new HttpClient(handler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client); - var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); + var result = await kernel.SendAsync(new SubmitCode("GET http://testuri.ninja")); result.Events.Should().ContainSingle().Which .FormattedValues.Should().ContainSingle().Which @@ -450,10 +450,8 @@ public async Task display_should_be_suppressed_for_return_value() public async Task produces_initial_displayed_value_that_is_updated_when_response_is_slow() { const int ResponseDelayThresholdInMilliseconds = 5; - HttpRequestMessage request = null; var slowResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; await Task.Delay(2 * ResponseDelayThresholdInMilliseconds); @@ -461,9 +459,7 @@ public async Task produces_initial_displayed_value_that_is_updated_when_response }); var client = new HttpClient(slowResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -482,10 +478,8 @@ public async Task produces_initial_displayed_value_that_is_updated_when_response public async Task when_response_is_slow_initial_displayed_value_conveys_that_it_is_awaiting_response() { const int ResponseDelayThresholdInMilliseconds = 5; - HttpRequestMessage request = null; var slowResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; await Task.Delay(2 * ResponseDelayThresholdInMilliseconds); @@ -493,9 +487,7 @@ public async Task when_response_is_slow_initial_displayed_value_conveys_that_it_ }); var client = new HttpClient(slowResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -518,9 +510,7 @@ public async Task when_response_is_slow_final_displayed_value_includes_response_ }); var client = new HttpClient(slowResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -532,10 +522,8 @@ public async Task when_response_is_slow_final_displayed_value_includes_response_ public async Task when_response_is_slow_and_an_error_happens_the_awaiting_response_displayed_value_is_cleared() { const int ResponseDelayThresholdInMilliseconds = 5; - HttpRequestMessage request = null; var throwingResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; await Task.Delay(2 * ResponseDelayThresholdInMilliseconds); @@ -543,11 +531,9 @@ public async Task when_response_is_slow_and_an_error_happens_the_awaiting_respon }); var client = new HttpClient(throwingResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds); - var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); + var result = await kernel.SendAsync(new SubmitCode("GET http://testuri.ninja")); var displayedValueUpdated = result.Events.OfType().First(); using var _ = new AssertionScope(); @@ -561,10 +547,8 @@ public async Task produces_initial_displayed_value_that_is_updated_when_response { const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var largeResponseHandler = new InterceptingHttpMessageHandler((message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -577,10 +561,7 @@ public async Task produces_initial_displayed_value_that_is_updated_when_response }); var client = new HttpClient(largeResponseHandler); - - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, contentByteLengthThreshold: ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, contentByteLengthThreshold: ContentByteLengthThreshold); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -600,10 +581,8 @@ public async Task when_response_is_large_initial_displayed_value_conveys_that_it { const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var largeResponseHandler = new InterceptingHttpMessageHandler((message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -617,9 +596,7 @@ public async Task when_response_is_large_initial_displayed_value_conveys_that_it var client = new HttpClient(largeResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, contentByteLengthThreshold: ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, contentByteLengthThreshold: ContentByteLengthThreshold); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -632,10 +609,8 @@ public async Task when_response_is_large_final_displayed_value_includes_response { const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var largeResponseHandler = new InterceptingHttpMessageHandler((message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -649,9 +624,7 @@ public async Task when_response_is_large_final_displayed_value_includes_response var client = new HttpClient(largeResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, contentByteLengthThreshold: ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, contentByteLengthThreshold: ContentByteLengthThreshold); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -665,10 +638,8 @@ public async Task produces_initial_displayed_value_that_is_updated_twice_when_re const int ResponseDelayThresholdInMilliseconds = 5; const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var slowAndLargeResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -683,9 +654,7 @@ public async Task produces_initial_displayed_value_that_is_updated_twice_when_re var client = new HttpClient(slowAndLargeResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -707,10 +676,8 @@ public async Task when_response_is_slow_and_large_first_displayed_value_conveys_ const int ResponseDelayThresholdInMilliseconds = 5; const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var slowAndLargeResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -725,9 +692,7 @@ public async Task when_response_is_slow_and_large_first_displayed_value_conveys_ var client = new HttpClient(slowAndLargeResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -741,10 +706,8 @@ public async Task when_response_is_slow_and_large_second_displayed_value_conveys const int ResponseDelayThresholdInMilliseconds = 5; const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var slowAndLargeResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -759,9 +722,7 @@ public async Task when_response_is_slow_and_large_second_displayed_value_conveys var client = new HttpClient(slowAndLargeResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); @@ -775,10 +736,8 @@ public async Task when_response_is_slow_and_large_final_displayed_value_includes const int ResponseDelayThresholdInMilliseconds = 5; const int ContentByteLengthThreshold = 100; - HttpRequestMessage request = null; var slowAndLargeResponseHandler = new InterceptingHttpMessageHandler(async (message, _) => { - request = message; var response = new HttpResponseMessage(HttpStatusCode.OK); response.RequestMessage = message; var builder = new StringBuilder(); @@ -793,11 +752,9 @@ public async Task when_response_is_slow_and_large_final_displayed_value_includes var client = new HttpClient(slowAndLargeResponseHandler); - using var root = new CompositeKernel(); - HttpRequestKernelExtension.Load(root, client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); - var kernel = root.FindKernels(k => k is HttpRequestKernel).Single(); + using var kernel = new HttpRequestKernel("http", client, ResponseDelayThresholdInMilliseconds, ContentByteLengthThreshold); - var result = await kernel.SendAsync(new SubmitCode($"GET http://testuri.ninja")); + var result = await kernel.SendAsync(new SubmitCode("GET http://testuri.ninja")); result.Events.OfType().Skip(2).First() .FormattedValues.Single().Value.Should().ContainAll("Response", "Request", "Headers"); @@ -843,4 +800,45 @@ public void JSONPath_can_be_used_to_access_response_properties() // TODO (dot_notation_can_be_used_to_access_response_properties) write test throw new NotImplementedException(); } + + [Fact] + public async Task It_supports_RequestValueInfos() + { + using var kernel = new HttpRequestKernel(); + + var sendValueResult = await kernel.SendAsync(new SendValue("theValue", 123, FormattedValue.CreateSingleFromObject(123, JsonFormatter.MimeType))); + + sendValueResult.Events.Should().NotContainErrors(); + + var result = await kernel.SendAsync(new RequestValueInfos()); + + using var _ = new AssertionScope(); + result.Events.Should().NotContainErrors(); + var valueInfo = result.Events.Should().ContainSingle() + .Which + .ValueInfos.Should().ContainSingle() + .Which; + valueInfo.Name.Should().Be("theValue"); + valueInfo.FormattedValue.Should().BeEquivalentTo(new FormattedValue(PlainTextSummaryFormatter.MimeType, "123")); + } + + [Fact] + public async Task It_supports_RequestValue() + { + using var kernel = new HttpRequestKernel(); + + var sendValueResult = await kernel.SendAsync(new SendValue("theValue", 123, FormattedValue.CreateSingleFromObject(123, JsonFormatter.MimeType))); + + sendValueResult.Events.Should().NotContainErrors(); + + var result = await kernel.SendAsync(new RequestValue("theValue", JsonFormatter.MimeType)); + + using var _ = new AssertionScope(); + var valueProduced = result.Events.Should().ContainSingle() + .Which; + valueProduced.Name.Should().Be("theValue"); + valueProduced + .FormattedValue.Should() + .BeEquivalentTo(new FormattedValue(JsonFormatter.MimeType, "123")); + } } diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs index 2624af4194..2f8273993e 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Comments.cs @@ -12,13 +12,14 @@ public partial class ParserTests public class Comments { [Fact] - public void comments_are_parsed_correctly() + public void line_comment_before_method_and_url_is_parsed_correctly() { - var result = Parse( - """ + var code = """ # This is a comment - GET https://example.com HTTP/1.1" - """); + GET https://example.com + """; + + var result = Parse(code); var methodNode = result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs index d9cbb290a4..1925050d82 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Headers.cs @@ -17,7 +17,7 @@ public void header_with_body_is_parsed_correctly() { var result = Parse( """ - POST https://example.com/comments HTTP/1.1 + POST https://example.com/comments Content-Type: application/xml Authorization: token xxx @@ -41,7 +41,7 @@ public void header_with_body_is_parsed_correctly() } [Fact] - public void header_separator_is_present() + public void header_separator_is_parsed() { var result = Parse( """ diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs index 63a9ff0ed6..b5604ec3fc 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Lexer.cs @@ -24,7 +24,7 @@ public void multiple_whitespaces_are_treated_as_a_single_token() result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.Single().TextWithTrivia.Should().Be(" \t "); + .MethodNode.ChildTokens.Single().Text.Should().Be(" \t "); } [Fact] @@ -33,11 +33,11 @@ public void multiple_newlines_are_parsed_into_different_tokens() var result = Parse("\n\v\r\n\n"); result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( - new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\v", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\r\n", Kind = HttpTokenKind.NewLine }, - new { TextWithTrivia = "\n", Kind = HttpTokenKind.NewLine }); + .MethodNode.ChildTokens.Select(t => new { t.Text, t.Kind }).Should().BeEquivalentSequenceTo( + new { Text = "\n", Kind = HttpTokenKind.NewLine }, + new { Text = "\v", Kind = HttpTokenKind.NewLine }, + new { Text = "\r\n", Kind = HttpTokenKind.NewLine }, + new { Text = "\n", Kind = HttpTokenKind.NewLine }); } [Fact] @@ -45,13 +45,13 @@ public void multiple_punctuations_are_parsed_into_different_tokens() { var result = Parse(".!?.:/"); result.SyntaxTree.RootNode.ChildNodes.Should().ContainSingle().Which - .UrlNode.ChildTokens.Select(t => new { t.TextWithTrivia, t.Kind }).Should().BeEquivalentSequenceTo( - new { TextWithTrivia = ".", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = "!", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = "?", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = ".", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = ":", Kind = HttpTokenKind.Punctuation }, - new { TextWithTrivia = "/", Kind = HttpTokenKind.Punctuation }); + .UrlNode.ChildTokens.Select(t => new { t.Text, t.Kind }).Should().BeEquivalentSequenceTo( + new { Text = ".", Kind = HttpTokenKind.Punctuation }, + new { Text = "!", Kind = HttpTokenKind.Punctuation }, + new { Text = "?", Kind = HttpTokenKind.Punctuation }, + new { Text = ".", Kind = HttpTokenKind.Punctuation }, + new { Text = ":", Kind = HttpTokenKind.Punctuation }, + new { Text = "/", Kind = HttpTokenKind.Punctuation }); } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs index e1b397e26e..90bb47f8c5 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Method.cs @@ -32,9 +32,35 @@ public void newline_is_legal_at_the_beginning_of_a_request() GET https://example.com """); - result.SyntaxTree.RootNode - .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); + var methodNode = result.SyntaxTree + .RootNode + .ChildNodes + .Should() + .ContainSingle().Which + .MethodNode; + + methodNode.ChildTokens.First().Kind.Should().Be(HttpTokenKind.NewLine); + methodNode.Text.Should().Be("GET"); + } + + [Fact] + public void comment_is_legal_at_the_beginning_of_a_request() + { + var result = Parse( + """ + # this is a comment + GET https://example.com + """); + + var methodNode = result.SyntaxTree + .RootNode + .ChildNodes + .Should() + .ContainSingle().Which + .MethodNode; + + methodNode.ChildNodes.First().Should().BeOfType(); + methodNode.Text.Should().Be("GET"); } [Theory] @@ -52,10 +78,10 @@ public void common_verbs_are_parsed_correctly(string line, string method) } [Theory] - [InlineData(@"GET https://example.com", "GET")] - [InlineData(@"Get https://example.com", "Get")] - [InlineData(@"OPTIONS https://example.com", "OPTIONS")] - [InlineData(@"options https://example.com", "options")] + [InlineData("GET https://example.com", "GET")] + [InlineData("Get https://example.com", "Get")] + [InlineData("OPTIONS https://example.com", "OPTIONS")] + [InlineData("options https://example.com", "options")] public void it_can_parse_verbs_regardless_of_their_casing(string line, string method) { var result = Parse(line); diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs index 77cbfab580..e649896713 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Request.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System; using System.Linq; using System.Net.Http; using FluentAssertions; diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs index c406ffb66e..46b8cf51f6 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.Trivia.cs @@ -16,9 +16,7 @@ public class Trivia public void it_can_parse_an_empty_string() { var result = Parse(""); - result.SyntaxTree.Should().BeNull(); - - // TODO: Test error reporting. + result.SyntaxTree.Should().NotBeNull(); } [Fact] @@ -28,9 +26,7 @@ public void it_can_parse_a_string_with_only_whitespace() result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which - .MethodNode.ChildTokens.First().TextWithTrivia.Should().Be(" \t "); - - // TODO: Test error reporting. + .MethodNode.ChildTokens.First().Text.Should().Be(" \t "); } [Fact] @@ -40,7 +36,7 @@ public void it_can_parse_a_string_with_only_newlines() result.SyntaxTree.RootNode .ChildNodes.Should().ContainSingle().Which - .MethodNode.TextWithTrivia.Should().Be("\r\n\n\r\n"); + .MethodNode.FullText.Should().Be("\r\n\n\r\n"); } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs index 8a0a082781..d4ed72b5b3 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/ParserTests.cs @@ -26,7 +26,7 @@ private static HttpRequestParseResult Parse(string code) var result = HttpRequestParser.Parse(code); if (result.SyntaxTree?.RootNode is not null) { - result.SyntaxTree.RootNode.TextWithTrivia.Should().Be(code); + result.SyntaxTree.RootNode.FullText.Should().Be(code); } return result; diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Utility/AssertionExtensions.cs b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Utility/AssertionExtensions.cs index 0cfe0995e9..f3ee2c310d 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Utility/AssertionExtensions.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest.Tests/Utility/AssertionExtensions.cs @@ -22,4 +22,17 @@ public static AndWhichConstraint ContainSingle( return new AndWhichConstraint(subject.Should(), subject); } + + public static AndWhichConstraint ContainSingle( + this GenericCollectionAssertions should) + where T : HttpSyntaxNode + { + should.ContainSingle(e => e is T); + + var subject = should.Subject + .OfType() + .Single(); + + return new AndWhichConstraint(subject.Should(), subject); + } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequest/HttpRequestKernel.cs b/src/Microsoft.DotNet.Interactive.HttpRequest/HttpRequestKernel.cs index d14f4300da..2606905dbd 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequest/HttpRequestKernel.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequest/HttpRequestKernel.cs @@ -3,29 +3,26 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Formatting; +using Microsoft.DotNet.Interactive.ValueSharing; namespace Microsoft.DotNet.Interactive.HttpRequest; public class HttpRequestKernel : - Kernel, - IKernelCommandHandler, - IKernelCommandHandler, - IKernelCommandHandler, - IKernelCommandHandler + Kernel, + IKernelCommandHandler, + IKernelCommandHandler, + IKernelCommandHandler, + IKernelCommandHandler, + IKernelCommandHandler { internal const int DefaultResponseDelayThresholdInMilliseconds = 1000; internal const int DefaultContentByteLengthThreshold = 500_000; @@ -34,22 +31,7 @@ public class HttpRequestKernel : private readonly int _responseDelayThresholdInMilliseconds; private readonly long _contentByteLengthThreshold; - private readonly Dictionary _variables = new(StringComparer.InvariantCultureIgnoreCase); - private static readonly Regex IsRequest; - private static readonly Regex IsHeader; - - private const string InterpolationStartMarker = "{{"; - private const string InterpolationEndMarker = "}}"; - - static HttpRequestKernel() - { - var verbs = string.Join("|", - typeof(HttpMethod).GetProperties(BindingFlags.Static | BindingFlags.Public).Select(p => p.GetValue(null)!.ToString())); - - IsRequest = new Regex(@"^\s*(" + verbs + ")", RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.IgnoreCase); - - IsHeader = new Regex(@"^\s*(?[\w-]+):\s*(?.*)", RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.IgnoreCase); - } + private readonly Dictionary _variables = new(StringComparer.InvariantCultureIgnoreCase); public HttpRequestKernel( string? name = null, @@ -74,27 +56,46 @@ Task IKernelCommandHandler.HandleAsync(RequestValue command, Kerne var valueProduced = new ValueProduced( value, command.Name, - FormattedValue.CreateSingleFromObject(value), + FormattedValue.CreateSingleFromObject(value, JsonFormatter.MimeType), command); context.Publish(valueProduced); } + else + { + context.Fail(command, message: $"Value not found: {command.Name}"); + } return Task.CompletedTask; } - Task IKernelCommandHandler.HandleAsync(SendValue command, KernelInvocationContext context) + Task IKernelCommandHandler.HandleAsync(RequestValueInfos command, KernelInvocationContext context) { - SetValue(command.Name, command.FormattedValue.Value.Trim('"')); + var valueInfos = _variables.Select(v => new KernelValueInfo(v.Key, FormattedValue.CreateSingleFromObject(v.Value, PlainTextSummaryFormatter.MimeType))).ToArray(); + + context.Publish(new ValueInfosProduced(valueInfos, command)); + return Task.CompletedTask; } - private void SetValue(string valueName, string value) - => _variables[valueName] = value; + async Task IKernelCommandHandler.HandleAsync(SendValue command, KernelInvocationContext context) + { + await SetValueAsync(command, context, SetValueAsync); + } + + private Task SetValueAsync(string valueName, object value, Type? declaredType = null) + { + _variables[valueName] = value; + return Task.CompletedTask; + } async Task IKernelCommandHandler.HandleAsync(SubmitCode command, KernelInvocationContext context) { - var parsedRequests = ParseRequests(command.Code).ToArray(); - var diagnostics = parsedRequests.SelectMany(r => r.Diagnostics).ToArray(); + var parseResult = HttpRequestParser.Parse(command.Code); + + var httpRequestResults = parseResult.SyntaxTree.RootNode.ChildNodes.OfType().Select(n => n.TryGetHttpRequestMessage(BindExpressionValues)).ToArray(); + + var requestMessages = httpRequestResults.Select(r => r.Value).ToArray(); + var diagnostics = httpRequestResults.SelectMany(r => r.Diagnostics).ToArray(); PublishDiagnostics(context, command, diagnostics); @@ -106,25 +107,23 @@ async Task IKernelCommandHandler.HandleAsync(SubmitCode command, Ker try { - foreach (var parsedRequest in parsedRequests) + foreach (var requestMessage in requestMessages) { - await HandleRequestAsync(parsedRequest, command, context); + if (requestMessage is not null) + { + await SendRequestAsync(requestMessage, command, context); + } } } - catch (Exception ex) when (ex is OperationCanceledException || ex is HttpRequestException) + catch (Exception ex) when (ex is OperationCanceledException or HttpRequestException) { context.Fail(command, message: ex.Message); } } - private async Task HandleRequestAsync( - ParsedHttpRequest parsedRequest, - KernelCommand command, - KernelInvocationContext context) + private async Task SendRequestAsync(HttpRequestMessage requestMessage, KernelCommand command, KernelInvocationContext context) { var cancellationToken = context.CancellationToken; - var requestMessage = GetRequestMessage(parsedRequest); - var isResponseAvailable = false; var semaphore = new SemaphoreSlim(1); string? valueId = null; @@ -214,40 +213,6 @@ void ClearDisplayedValue() } } - private static HttpRequestMessage GetRequestMessage(ParsedHttpRequest parsedRequest) - { - var requestMessage = new HttpRequestMessage(new HttpMethod(parsedRequest.Verb), parsedRequest.Address); - if (!string.IsNullOrWhiteSpace(parsedRequest.Body)) - { - requestMessage.Content = new StringContent(parsedRequest.Body); - } - - foreach (var kvp in parsedRequest.Headers) - { - switch (kvp.Key.ToLowerInvariant()) - { - case "content-type": - if (requestMessage.Content is null) - { - requestMessage.Content = new StringContent(parsedRequest.Body); - } - requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(kvp.Value); - break; - case "accept": - requestMessage.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(kvp.Value)); - break; - case "user-agent": - requestMessage.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(kvp.Value)); - break; - default: - requestMessage.Headers.Add(kvp.Key, kvp.Value); - break; - } - } - - return requestMessage; - } - private async Task GetResponseWithTimingAsync( HttpRequestMessage requestMessage, CancellationToken cancellationToken) @@ -269,7 +234,7 @@ private async Task GetResponseWithTimingAsync( return response; } - private void PublishDiagnostics(KernelInvocationContext context, KernelCommand command, IEnumerable diagnostics) + private void PublishDiagnostics(KernelInvocationContext context, KernelCommand command, IReadOnlyCollection diagnostics) { if (diagnostics.Any()) { @@ -277,7 +242,7 @@ private void PublishDiagnostics(KernelInvocationContext context, KernelCommand c diagnostics .Select(d => d.ToString()) .Select(text => new FormattedValue(PlainTextFormatter.MimeType, text)) - .ToImmutableArray(); + .ToArray(); context.Publish(new DiagnosticsProduced(diagnostics, command, formattedDiagnostics)); } @@ -285,176 +250,45 @@ private void PublishDiagnostics(KernelInvocationContext context, KernelCommand c Task IKernelCommandHandler.HandleAsync(RequestDiagnostics command, KernelInvocationContext context) { - var requestsAndDiagnostics = InterpolateAndGetDiagnostics(command.Code); - var diagnostics = requestsAndDiagnostics.SelectMany(r => r.Diagnostics); + var parseResult = HttpRequestParser.Parse(command.Code); + var requestsAndDiagnostics = GetAllDiagnostics(parseResult); + var diagnostics = requestsAndDiagnostics; PublishDiagnostics(context, command, diagnostics); return Task.CompletedTask; } - private IEnumerable<(string Request, List Diagnostics)> InterpolateAndGetDiagnostics(string code) + private List GetAllDiagnostics(HttpRequestParseResult parseResult) { - var lines = code.Split('\n'); - - var result = new List<(string Request, List)>(); - var currentLines = new List(); - var currentDiagnostics = new List(); - - for (var line = 0; line < lines.Length; line++) - { - var lineText = lines[line]; - if (IsRequest.IsMatch(lineText)) - { - if (MightContainRequest(currentLines)) - { - var requestCode = string.Join('\n', currentLines); - result.Add((requestCode, currentDiagnostics)); - } - - currentLines = new List(); - currentDiagnostics = new List(); - } - - var rebuiltLine = new StringBuilder(); - var lastStart = 0; - var interpolationStart = lineText.IndexOf(InterpolationStartMarker); - while (interpolationStart >= 0) - { - rebuiltLine.Append(lineText[lastStart..interpolationStart]); - var interpolationEnd = lineText.IndexOf(InterpolationEndMarker, interpolationStart + InterpolationStartMarker.Length); - if (interpolationEnd < 0) - { - // no end marker - // TODO: error? - } - - var variableName = lineText[(interpolationStart + InterpolationStartMarker.Length)..interpolationEnd]; - if (_variables.TryGetValue(variableName, out var value)) - { - rebuiltLine.Append(value); - } - else - { - // no variable found; keep old code and report diagnostic - rebuiltLine.Append(InterpolationStartMarker); - rebuiltLine.Append(variableName); - rebuiltLine.Append(InterpolationEndMarker); - var position = new LinePositionSpan( - new LinePosition(line, interpolationStart + InterpolationStartMarker.Length), - new LinePosition(line, interpolationEnd)); - currentDiagnostics.Add(new Diagnostic(position, DiagnosticSeverity.Error, "HTTP404", $"Cannot resolve symbol '{variableName}'")); - } + var diagnostics = new List(); - lastStart = interpolationEnd + InterpolationEndMarker.Length; - interpolationStart = lineText.IndexOf(InterpolationStartMarker, lastStart); - } - - rebuiltLine.Append(lineText[lastStart..]); - currentLines.Add(rebuiltLine.ToString()); - } - - if (MightContainRequest(currentLines)) + foreach (var diagnostic in parseResult.GetDiagnostics()) { - var requestCode = string.Join('\n', currentLines); - result.Add((requestCode, currentDiagnostics)); + diagnostics.Add(diagnostic); } - return result; - } - - private static bool MightContainRequest(IEnumerable lines) - { - return lines.Any(line => IsRequest.IsMatch(line)); - //return lines.Any() && lines.Any(line => !string.IsNullOrWhiteSpace(line)); - } - - private IEnumerable ParseRequests(string requests) - { - var parsedRequests = new List(); - - foreach (var (request, diagnostics) in InterpolateAndGetDiagnostics(requests)) + foreach (var expressionNode in parseResult.SyntaxTree.RootNode.DescendantNodesAndTokensAndSelf().OfType()) { - var body = new StringBuilder(); - string? verb = null; - string? address = null; - var headerValues = new Dictionary(); - var lines = request.Split(new[] { '\n' }); - for (var index = 0; index < lines.Length; index++) - { - var line = lines[index]; - if (verb == null) - { - if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) - { - continue; - } - - var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - verb = parts[0].Trim(); - address = parts[1].Trim(); - } - else if (!string.IsNullOrWhiteSpace(line) && IsHeader.Matches(line) is { } matches && matches.Count != 0) - { - foreach (Match match in matches) - { - var key = match.Groups["key"].Value; - var value = match.Groups["value"].Value.Trim(); - headerValues[key] = value; - } - } - else - { - for (; index < lines.Length; index++) - { - body.AppendLine(lines[index]); - } - } - } - - if (string.IsNullOrWhiteSpace(verb)) + if (BindExpressionValues(expressionNode) is { IsSuccessful: false } bindResult) { - throw new InvalidOperationException("Cannot perform HttpRequest without a valid verb."); + diagnostics.AddRange(bindResult.Diagnostics); } - - var uri = GetAbsoluteUriString(address); - var bodyText = body.ToString().Trim(); - parsedRequests.Add(new ParsedHttpRequest(verb, uri, bodyText, headerValues, diagnostics)); } - return parsedRequests; + return diagnostics; } - private string GetAbsoluteUriString(string? address) + private HttpBindingResult BindExpressionValues(HttpExpressionNode node) { - if (string.IsNullOrWhiteSpace(address)) - { - throw new InvalidOperationException("Cannot perform HttpRequest without a valid uri."); - } + var variableName = node.Text; + var expression = variableName; - var uri = new Uri(address, UriKind.RelativeOrAbsolute); - - if (!uri.IsAbsoluteUri) + if (_variables.TryGetValue(expression, out var value)) { - throw new InvalidOperationException($"Cannot use relative path {uri} without a base address."); + return HttpBindingResult.Success(value); } - return uri.AbsoluteUri; - } - - private class ParsedHttpRequest - { - public ParsedHttpRequest(string verb, string address, string body, IEnumerable> headers, IEnumerable diagnostics) - { - Verb = verb; - Address = address; - Body = body; - Headers = headers; - Diagnostics = diagnostics; - } + var diagnostic = node.CreateDiagnostic($"Cannot resolve symbol '{variableName}'"); - public string Verb { get; } - public string Address { get; } - public string Body { get; } - public IEnumerable> Headers { get; } - public IEnumerable Diagnostics { get; } + return HttpBindingResult.Failure(diagnostic); } -} +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBodyNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBodyNode.cs index 1950a1554d..2a6fb6e7fd 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBodyNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpBodyNode.cs @@ -3,6 +3,8 @@ #nullable enable +using System.Collections.Generic; +using System.Text; using Microsoft.CodeAnalysis.Text; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -12,4 +14,44 @@ internal class HttpBodyNode : HttpSyntaxNode internal HttpBodyNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) { } -} + + public HttpBindingResult TryGetBody(HttpBindingDelegate bind) + { + var bodyText = new StringBuilder(); + var diagnostics = new List(); + var success = true; + + foreach (var node in ChildNodesAndTokens) + { + if (node is HttpEmbeddedExpressionNode n) + { + var innerResult = bind(n.ExpressionNode); + + if (innerResult.IsSuccessful) + { + var nodeText = innerResult.Value?.ToString(); + bodyText.Append(nodeText); + } + else + { + success = false; + } + + diagnostics.AddRange(innerResult.Diagnostics); + } + else + { + bodyText.Append(node.Text); + } + } + + if (success) + { + return HttpBindingResult.Success(bodyText.ToString()); + } + else + { + return HttpBindingResult.Failure(diagnostics.ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpCommentNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpCommentNode.cs index 8376ac42a9..51e86461a6 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpCommentNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpCommentNode.cs @@ -30,5 +30,6 @@ internal HttpCommentNode( public HttpCommentStartNode CommentStartNode { get; } public HttpCommentBodyNode? CommentBodyNode { get; } - + + public override bool IsSignificant => false; } diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs index bdb35a4068..33399ec93e 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestNode.cs @@ -3,8 +3,12 @@ #nullable enable +using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; namespace Microsoft.DotNet.Interactive.HttpRequest; @@ -70,10 +74,9 @@ internal HttpRequestNode( public HttpBindingResult TryGetHttpRequestMessage(HttpBindingDelegate bind) { var request = new HttpRequestMessage(); - var diagnostics = new List(); - var success = true; + var diagnostics = new List(base.GetDiagnostics()); - if (MethodNode is { Span.IsEmpty: false }) + if (MethodNode is { FullSpan.IsEmpty: false }) { request.Method = new HttpMethod(MethodNode.Text); } @@ -85,11 +88,57 @@ public HttpBindingResult TryGetHttpRequestMessage(HttpBindin } else { - success = false; diagnostics.AddRange(uriBindingResult.Diagnostics); } - if (success) + var headers = + HeadersNode?.HeaderNodes.Select(h => new KeyValuePair(h.NameNode.Text, h.ValueNode.Text)).ToArray() + ?? + Array.Empty>(); + + foreach (var kvp in headers) + { + switch (kvp.Key.ToLowerInvariant()) + { + case "content-type": + if (request.Content is null) + { + request.Content = new StringContent(""); + } + + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(kvp.Value); + break; + case "accept": + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(kvp.Value)); + break; + case "user-agent": + request.Headers.UserAgent.Add(ProductInfoHeaderValue.Parse(kvp.Value)); + break; + default: + request.Headers.Add(kvp.Key, kvp.Value); + break; + } + } + + var bodyResult = BodyNode?.TryGetBody(bind); + string? body = null; + + if (bodyResult is not null) + { + if (bodyResult.IsSuccessful) + { + body = bodyResult.Value ?? ""; + } + + diagnostics.AddRange(bodyResult.Diagnostics); + } + + if (!string.IsNullOrWhiteSpace(body)) + { + request.Content = new StringContent(body); + } + + if (diagnostics.All(d => d.Severity != DiagnosticSeverity.Error)) { return HttpBindingResult.Success(request); } diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs index 5a4ef6712f..31ac4eef71 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParseResult.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections.Generic; using System.Linq; @@ -10,13 +11,13 @@ namespace Microsoft.DotNet.Interactive.HttpRequest; internal class HttpRequestParseResult { - public HttpRequestParseResult(HttpSyntaxTree? syntaxTree) - => SyntaxTree = syntaxTree; + public HttpRequestParseResult(HttpSyntaxTree syntaxTree) + => SyntaxTree = syntaxTree ?? throw new ArgumentNullException(nameof(syntaxTree)); - public HttpSyntaxTree? SyntaxTree { get; } + public HttpSyntaxTree SyntaxTree { get; } public IEnumerable GetDiagnostics() { - return SyntaxTree?.RootNode?.GetDiagnostics() ?? Enumerable.Empty(); + return SyntaxTree.RootNode?.GetDiagnostics() ?? Enumerable.Empty(); } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs index a14050bec3..774b32d311 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpRequestParser.cs @@ -3,7 +3,6 @@ #nullable enable -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System.Collections.Generic; @@ -33,19 +32,9 @@ public HttpSyntaxParser(SourceText sourceText) _syntaxTree = new HttpSyntaxTree(_sourceText); } - public HttpSyntaxTree? Parse() + public HttpSyntaxTree Parse() { _tokens = new HttpLexer(_sourceText, _syntaxTree).Lex(); - if (_tokens.Count == 0) - { - return null; - } - - var rootNode = new HttpRootSyntaxNode( - _sourceText, - _syntaxTree); - - _syntaxTree.RootNode = rootNode; while (MoreTokens()) { @@ -55,7 +44,8 @@ public HttpSyntaxParser(SourceText sourceText) { _syntaxTree.RootNode.Add(requestNode); } - if(ParseRequestSeparator() is { } separatorNode) + + if (ParseRequestSeparator() is { } separatorNode) { _syntaxTree.RootNode.Add(separatorNode); } @@ -204,7 +194,7 @@ private HttpRequestNode ParseRequest() if (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Word) { - if (CurrentToken.Text.ToLower() is ("get" or "post" or "patch" or "put" or "delete" or "head" or "options" or "trace")) + if (CurrentToken.Text.ToLower() is "get" or "post" or "patch" or "put" or "delete" or "head" or "options" or "trace") { ConsumeCurrentTokenInto(node); } @@ -231,8 +221,7 @@ private HttpUrlNode ParseUrl() while (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Word or HttpTokenKind.Punctuation) { - if (CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" } && - NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" }) + if (IsAtStartOfEmbeddedExpression()) { node.Add(ParseEmbeddedExpression()); } @@ -245,6 +234,12 @@ private HttpUrlNode ParseUrl() return ParseTrailingTrivia(node, stopAfterNewLine: true); } + private bool IsAtStartOfEmbeddedExpression() + { + return CurrentToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" } && + NextToken is { Kind: HttpTokenKind.Punctuation } and { Text: "{" }; + } + private HttpEmbeddedExpressionNode ParseEmbeddedExpression() { var startNode = ParseExpressionStart(); @@ -298,7 +293,9 @@ private HttpExpressionEndNode ParseExpressionEnd() ParseLeadingTrivia(node); - if (MoreTokens() && CurrentToken.Kind is HttpTokenKind.Word) + if (MoreTokens() && + CurrentToken.Kind is HttpTokenKind.Word && + CurrentToken.Text.ToLowerInvariant() == "http") { ConsumeCurrentTokenInto(node); @@ -402,7 +399,6 @@ private HttpHeaderValueNode ParseHeaderValue() { ConsumeCurrentTokenInto(node); } - } return ParseTrailingTrivia(node, stopAfterNewLine: true); @@ -445,15 +441,22 @@ private HttpHeaderValueNode ParseHeaderValue() ParseLeadingTrivia(node); - if (MoreTokens() && CurrentToken.Kind is not (HttpTokenKind.Whitespace or HttpTokenKind.NewLine) && - !IsRequestSeparator()) + if (MoreTokens() && + CurrentToken.Kind is not (HttpTokenKind.Whitespace or HttpTokenKind.NewLine) && + !IsRequestSeparator()) { ConsumeCurrentTokenInto(node); while (MoreTokens() && !IsRequestSeparator()) { - - ConsumeCurrentTokenInto(node); + if (!IsAtStartOfEmbeddedExpression()) + { + ConsumeCurrentTokenInto(node); + } + else + { + node.Add(ParseEmbeddedExpression()); + } } } diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs index 169c0ff921..ed1bfcd705 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNode.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #nullable enable @@ -12,8 +12,10 @@ namespace Microsoft.DotNet.Interactive.HttpRequest; internal abstract class HttpSyntaxNode : HttpSyntaxNodeOrToken { - private TextSpan _span; + private TextSpan _fullSpan; private readonly List _childNodesAndTokens = new(); + private TextSpan _span; + private bool _isSignificant = false; private protected HttpSyntaxNode( SourceText sourceText, @@ -21,22 +23,31 @@ private protected HttpSyntaxNode( { } + public override TextSpan FullSpan => _fullSpan; + + public override bool IsSignificant => _isSignificant; + public override TextSpan Span => _span; + /// + /// Gets the text of the current node, including trivia. + /// + public string FullText => SourceText.ToString(FullSpan); + public bool Contains(HttpSyntaxNode node) => false; public HttpSyntaxNode? FindNode(TextSpan span) => DescendantNodesAndTokensAndSelf() .OfType() .Reverse() - .FirstOrDefault(n => n.Span.Contains(span)); + .FirstOrDefault(n => n.FullSpan.Contains(span)); public HttpSyntaxNode? FindNode(int position) => FindToken(position)?.Parent; public HttpSyntaxToken? FindToken(int position) { - var candidate = _childNodesAndTokens.FirstOrDefault(n => n.Span.Contains(position)); + var candidate = _childNodesAndTokens.FirstOrDefault(n => n.FullSpan.Contains(position)); return candidate switch { @@ -48,15 +59,34 @@ private protected HttpSyntaxNode( private void GrowSpan(HttpSyntaxNodeOrToken child) { - if (_span == default) + if (_fullSpan == default) { + _fullSpan = child.FullSpan; _span = child.Span; } else { - var _spanStart = Math.Min(_span.Start, child.Span.Start); - var _spanEnd = Math.Max(_span.End, child.Span.End); - _span = new TextSpan(_spanStart, _spanEnd - _span.Start); + var fullSpanStart = Math.Min(_fullSpan.Start, child.FullSpan.Start); + var fullSpanEnd = Math.Max(_fullSpan.End, child.FullSpan.End); + _fullSpan = new TextSpan(fullSpanStart, fullSpanEnd - _fullSpan.Start); + + var firstSignificantNodeOrToken = ChildNodesAndTokens + .FirstOrDefault(n => n.IsSignificant); + + var lastSignificantNodeOrToken = ChildNodesAndTokens + .LastOrDefault(n => n.IsSignificant); + + var startOfSignificantText = + firstSignificantNodeOrToken?.Span.Start ?? + FullSpan.Start; + + var endOfSignificantText = + lastSignificantNodeOrToken?.Span.End ?? + FullSpan.End; + + _span = TextSpan.FromBounds( + startOfSignificantText, + endOfSignificantText); } } @@ -69,9 +99,14 @@ internal void Add(HttpSyntaxNodeOrToken child) child.Parent = this; - GrowSpan(child); + if (child.IsSignificant) + { + _isSignificant = true; + } _childNodesAndTokens.Add(child); + + GrowSpan(child); } public IEnumerable ChildNodes => @@ -83,13 +118,12 @@ internal void Add(HttpSyntaxNodeOrToken child) public override IEnumerable GetDiagnostics() { - foreach(var child in ChildNodesAndTokens) + foreach (var child in ChildNodesAndTokens) { - foreach(var diagnostic in child.GetDiagnostics()) + foreach (var diagnostic in child.GetDiagnostics()) { yield return diagnostic; } - } if (_diagnostics is not null) @@ -99,16 +133,15 @@ public override IEnumerable GetDiagnostics() yield return diagnostic; } } - } public IEnumerable DescendantNodesAndTokensAndSelf() { yield return this; - foreach (var syntaxNodeOrToken1 in DescendantNodesAndTokens()) + foreach (var node in DescendantNodesAndTokens()) { - yield return syntaxNodeOrToken1; + yield return node; } } @@ -120,8 +153,8 @@ public IEnumerable DescendantNodesAndTokens() => }); private static IEnumerable FlattenBreadthFirst( - IEnumerable source, - Func> children) + IEnumerable source, + Func> children) { var queue = new Queue(); @@ -142,4 +175,4 @@ private static IEnumerable FlattenBreadthFirst( yield return current; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs index ce1dd2810a..9927b1a494 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxNodeOrToken.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #nullable enable @@ -23,6 +23,10 @@ private protected HttpSyntaxNodeOrToken(SourceText sourceText, HttpSyntaxTree? s public HttpSyntaxNode? Parent { get; internal set; } + public abstract bool IsSignificant { get; } + + public abstract TextSpan FullSpan { get; } + public abstract TextSpan Span { get; } public HttpSyntaxTree? SyntaxTree { get; } @@ -30,12 +34,7 @@ private protected HttpSyntaxNodeOrToken(SourceText sourceText, HttpSyntaxTree? s /// /// Gets the significant text of the current node or token, without trivia. /// - public string Text => TextWithTrivia.Trim(); - - /// - /// Gets the text of the current node or token, including trivia. - /// - public string TextWithTrivia => SourceText.ToString(Span); + public string Text => SourceText.ToString(Span); public override string ToString() => $"{GetType().Name}: {Text}"; @@ -53,7 +52,11 @@ public Diagnostic CreateDiagnostic(string message) var tokenSpan = lines.GetLinePositionSpan(Span); - var diagnostic = new Diagnostic(LinePositionSpan.FromCodeAnalysisLinePositionSpan(tokenSpan), DiagnosticSeverity.Warning, Text, message); + var diagnostic = new Diagnostic( + LinePositionSpan.FromCodeAnalysisLinePositionSpan(tokenSpan), + DiagnosticSeverity.Error, + Text, + message); return diagnostic; } diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxToken.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxToken.cs index 053b181c8f..2994cc6625 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxToken.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxToken.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #nullable enable @@ -13,14 +13,18 @@ internal sealed class HttpSyntaxToken : HttpSyntaxNodeOrToken internal HttpSyntaxToken( HttpTokenKind kind, SourceText sourceText, - TextSpan span, + TextSpan fullSpan, HttpSyntaxTree? syntaxTree) : base(sourceText, syntaxTree) { Kind = kind; - Span = span; + FullSpan = fullSpan; } - public override TextSpan Span { get; } + public override TextSpan FullSpan { get; } + + public override bool IsSignificant => this is not { Kind: HttpTokenKind.Whitespace or HttpTokenKind.NewLine }; + + public override TextSpan Span => FullSpan; public HttpTokenKind Kind { get; set; } @@ -28,7 +32,6 @@ internal HttpSyntaxToken( public override IEnumerable GetDiagnostics() { - if (_diagnostics is not null) { foreach (var diagnostic in _diagnostics) @@ -36,6 +39,5 @@ public override IEnumerable GetDiagnostics() yield return diagnostic; } } - } -} +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxTree.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxTree.cs index 748ded400b..4e36aef9ac 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxTree.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpSyntaxTree.cs @@ -9,10 +9,10 @@ namespace Microsoft.DotNet.Interactive.HttpRequest; internal class HttpSyntaxTree { - private readonly SourceText _sourceText; - public HttpSyntaxTree(SourceText sourceText) - => _sourceText = sourceText; + { + RootNode = new HttpRootSyntaxNode(sourceText, this); + } - public HttpRootSyntaxNode? RootNode { get; set; } -} + public HttpRootSyntaxNode RootNode { get; } +} \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs index cbcbf20a4d..049181ddf9 100644 --- a/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs +++ b/src/Microsoft.DotNet.Interactive.HttpRequestParser/HttpUrlNode.cs @@ -19,7 +19,6 @@ internal HttpUrlNode(SourceText sourceText, HttpSyntaxTree? syntaxTree) : base(s internal HttpBindingResult TryGetUri(HttpBindingDelegate bind) { var urlText = new StringBuilder(); - var diagnostics = new List(); var success = true; diff --git a/src/Microsoft.DotNet.Interactive/Events/DiagnosticsProduced.cs b/src/Microsoft.DotNet.Interactive/Events/DiagnosticsProduced.cs index 27fadfec21..652af91209 100644 --- a/src/Microsoft.DotNet.Interactive/Events/DiagnosticsProduced.cs +++ b/src/Microsoft.DotNet.Interactive/Events/DiagnosticsProduced.cs @@ -21,7 +21,8 @@ public DiagnosticsProduced(IEnumerable diagnostics, { throw new ArgumentNullException(nameof(diagnostics)); } - else if (!diagnostics.Any()) + + if (!diagnostics.Any()) { throw new ArgumentException("At least one diagnostic required.", nameof(diagnostics)); } From 10e97f289be3c727f3057a6d294a98b879ae05d8 Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Wed, 9 Aug 2023 14:45:16 +0100 Subject: [PATCH 11/13] upgrade roslyn --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 7bc47c0e7d..878e2c8fc3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,7 +17,7 @@ 17.4.0 13.0.2 - 4.5.0 + 4.6.0 7.0.0 6.0.0 7.0.0 From f35b933037e6a2b8caf15833830bd96d4773caee Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Wed, 9 Aug 2023 14:48:03 +0100 Subject: [PATCH 12/13] upgrade System.Reflection.Metadata --- .../Microsoft.DotNet.Interactive.VisualStudio.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.DotNet.Interactive.VisualStudio/Microsoft.DotNet.Interactive.VisualStudio.csproj b/src/Microsoft.DotNet.Interactive.VisualStudio/Microsoft.DotNet.Interactive.VisualStudio.csproj index ee65e90ec0..d91c9be4f7 100644 --- a/src/Microsoft.DotNet.Interactive.VisualStudio/Microsoft.DotNet.Interactive.VisualStudio.csproj +++ b/src/Microsoft.DotNet.Interactive.VisualStudio/Microsoft.DotNet.Interactive.VisualStudio.csproj @@ -43,7 +43,7 @@ - + From d1effcc4b0c3772eeadfb3cb8b7849a609548864 Mon Sep 17 00:00:00 2001 From: Diego Colombo Date: Thu, 10 Aug 2023 15:30:54 +0100 Subject: [PATCH 13/13] update markdig upgrade f# Revert "upgrade f#" This reverts commit 4ce3dcb62fa9e920aa30972c38dd27a0a142ad90. --- .../Microsoft.DotNet.Interactive.CSharpProject.csproj | 4 ++-- .../Microsoft.DotNet.Interactive.Jupyter.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.DotNet.Interactive.CSharpProject/Microsoft.DotNet.Interactive.CSharpProject.csproj b/src/Microsoft.DotNet.Interactive.CSharpProject/Microsoft.DotNet.Interactive.CSharpProject.csproj index b41ca7a86f..3fa2ec6706 100644 --- a/src/Microsoft.DotNet.Interactive.CSharpProject/Microsoft.DotNet.Interactive.CSharpProject.csproj +++ b/src/Microsoft.DotNet.Interactive.CSharpProject/Microsoft.DotNet.Interactive.CSharpProject.csproj @@ -41,7 +41,7 @@ - + @@ -55,7 +55,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Microsoft.DotNet.Interactive.Jupyter/Microsoft.DotNet.Interactive.Jupyter.csproj b/src/Microsoft.DotNet.Interactive.Jupyter/Microsoft.DotNet.Interactive.Jupyter.csproj index a0b7638d2d..7688b00c95 100644 --- a/src/Microsoft.DotNet.Interactive.Jupyter/Microsoft.DotNet.Interactive.Jupyter.csproj +++ b/src/Microsoft.DotNet.Interactive.Jupyter/Microsoft.DotNet.Interactive.Jupyter.csproj @@ -19,7 +19,7 @@ - +