diff --git a/src/CHttpExecutor/CHttpExecutor.csproj b/src/CHttpExecutor/CHttpExecutor.csproj
index f6b6cb6..77c667a 100644
--- a/src/CHttpExecutor/CHttpExecutor.csproj
+++ b/src/CHttpExecutor/CHttpExecutor.csproj
@@ -22,7 +22,11 @@
-
+
+
+
+
+
diff --git a/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs b/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs
index 53672f6..8c095a5 100644
--- a/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs
+++ b/src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs
@@ -33,14 +33,14 @@ public VariablePostProcessingWriterStrategy(bool enabled)
public async Task CompleteAsync(CancellationToken token)
{
- await _pipe.Reader.CopyToAsync(Content, token);
+ if (Enabled)
+ await _pipe.Reader.CopyToAsync(Content, token);
IsCompleted = true;
Content.Seek(0, SeekOrigin.Begin);
}
public ValueTask DisposeAsync()
{
- Enabled = true;
return ValueTask.CompletedTask;
}
diff --git a/src/CHttpExecutor/VariablePreprocessor.cs b/src/CHttpExecutor/VariablePreprocessor.cs
index 6e86334..0e90990 100644
--- a/src/CHttpExecutor/VariablePreprocessor.cs
+++ b/src/CHttpExecutor/VariablePreprocessor.cs
@@ -1,7 +1,9 @@
using System.Buffers;
-using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Text.Json.Nodes;
using CHttp.Writers;
+using Json.More;
+using Json.Path;
namespace CHttpExecutor;
@@ -118,20 +120,24 @@ private static bool TryGetPathValue(ReadOnlySpan jsonPath, VariablePostPro
return ParseHeaderValue(jsonPath.Slice(headersPart.Length), responseCtx, ref result);
if (jsonPath.StartsWith(bodyPart, StringComparison.OrdinalIgnoreCase))
- return ParseBody(jsonPath.Slice(bodyPart.Length), responseCtx, ref result);
+ return ParseBody(jsonPath.Slice(bodyPart.Length - 1), responseCtx, ref result);
return false;
}
- private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result)
+ private static bool ParseBody_Custom(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result)
{
+ // VSCE does not require $.
+ if (jsonPath.StartsWith(".$"))
+ jsonPath = jsonPath.Slice(2);
+
responseCtx.Content.Seek(0, SeekOrigin.Begin);
var jsonDoc = JsonDocument.Parse(responseCtx.Content);
var currentElement = jsonDoc.RootElement;
while (jsonPath.Length > 0)
{
var segment = jsonPath;
- var separatorIndex = jsonPath.IndexOf('.');
+ var separatorIndex = jsonPath.Slice(1).IndexOfAny(".[");
if (separatorIndex == -1)
{
segment = jsonPath;
@@ -139,16 +145,17 @@ private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessin
}
else
{
- segment = jsonPath[..separatorIndex];
+ segment = jsonPath[..(separatorIndex + 1)];
jsonPath = jsonPath.Slice(separatorIndex + 1);
}
- if (currentElement.ValueKind == JsonValueKind.Object && currentElement.TryGetProperty(segment, out var element))
+ if (currentElement.ValueKind == JsonValueKind.Object && currentElement.TryGetProperty(segment[1..], out var element))
{
currentElement = element;
}
else if (currentElement.ValueKind == JsonValueKind.Array
- && int.TryParse(segment, out var arrayIndex)
+ && segment.Length > 2 && segment.StartsWith("[") && segment.EndsWith("]")
+ && int.TryParse(segment[1..^1].Trim(), out var arrayIndex)
&& currentElement.GetArrayLength() > arrayIndex)
{
currentElement = currentElement.EnumerateArray().ElementAt(arrayIndex);
@@ -162,6 +169,36 @@ private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessin
return true;
}
+ private static bool ParseBody(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result)
+ {
+ // VSCE does not require $.
+ if (jsonPath.StartsWith(".$"))
+ jsonPath = jsonPath.Slice(1);
+ if (jsonPath.StartsWith("."))
+ jsonPath = $"${jsonPath}";
+
+ var path = JsonPath.Parse(jsonPath.ToString(), new PathParsingOptions() { AllowMathOperations = false, AllowRelativePathStart = true, AllowJsonConstructs = false, AllowInOperator = false, TolerateExtraWhitespace = true });
+ responseCtx.Content.Seek(0, SeekOrigin.Begin);
+ var instance = JsonNode.Parse(responseCtx.Content);
+ var matches = path.Evaluate(instance);
+ if (matches?.Matches == null || matches.Matches.Count == 0)
+ return false;
+
+ if (matches.Matches.Count != 1)
+ {
+ return false;
+ }
+ var matchedValue = matches.Matches.First().Value;
+ if (matchedValue == null)
+ return false;
+
+ if (matchedValue.GetValueKind() == JsonValueKind.String)
+ result = matchedValue.ToString();
+ else
+ result = matches.Matches.First().Value?.ToJsonString(new JsonSerializerOptions() { WriteIndented = false }) ?? string.Empty;
+ return true;
+ }
+
private static bool ParseHeaderValue(ReadOnlySpan jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result)
{
var headerName = jsonPath.ToString();
diff --git a/tests/CHttpExecutor.Tests/IntegrationTests.cs b/tests/CHttpExecutor.Tests/IntegrationTests.cs
index 02a7fdf..bf06a35 100644
--- a/tests/CHttpExecutor.Tests/IntegrationTests.cs
+++ b/tests/CHttpExecutor.Tests/IntegrationTests.cs
@@ -1,8 +1,6 @@
-using System.Net.Http.Json;
+using System.Text.Json;
using CHttp.Tests;
-using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Server.Kestrel.Core;
namespace CHttpExecutor.Tests;
@@ -46,34 +44,6 @@ public class IntegrationTests
GET https://localhost:5020/ HTTP/2
my: {{myheader}}"u8.ToArray();
- private byte[] _postProcessingContentHeaderRequest = @"###
-# @no-cert-validation
-# @name firstRequest
-GET https://localhost:5020/ HTTP/2
-###
-@myheader = {{firstRequest.response.headers.content-type}}
-###
-
-# @no-cert-validation
-GET https://localhost:5020/ HTTP/2
-my: {{myheader}}"u8.ToArray();
-
- private byte[] _postProcessingBodyRequest = @"###
-# @no-cert-validation
-# @name firstRequest
-GET https://localhost:5020/ HTTP/2
-###
-@myvalue = {{firstRequest.response.body.data.1.stringValue}}
-@mydate = {{firstRequest.response.body.data.1.dateValue}}
-@mynumber = {{firstRequest.response.body.data.1.numberValue}}
-###
-
-# @no-cert-validation
-GET https://localhost:5020/ HTTP/2
-myvalue: {{myvalue}}
-mydate: {{mydate}}
-mynumber: {{mynumber}}"u8.ToArray();
-
[Fact]
public async Task SingleRequestInvokesEndpoint()
{
@@ -167,6 +137,18 @@ public async Task PostProcessingTrailersVariables()
await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
+ private byte[] _postProcessingContentHeaderRequest = @"###
+# @no-cert-validation
+# @name firstRequest
+GET https://localhost:5020/ HTTP/2
+###
+@myheader = {{firstRequest.response.headers.content-type}}
+###
+
+# @no-cert-validation
+GET https://localhost:5020/ HTTP/2
+my: {{myheader}}"u8.ToArray();
+
[Fact]
public async Task PostProcessingContentHeadersVariables()
{
@@ -191,6 +173,22 @@ public async Task PostProcessingContentHeadersVariables()
await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
+ private byte[] _postProcessingBodyRequest = @"###
+# @no-cert-validation
+# @name firstRequest
+GET https://localhost:5020/ HTTP/2
+###
+@myvalue = {{firstRequest.response.body.data[1].stringValue}}
+@mydate = {{firstRequest.response.body.$.data[1].dateValue}}
+@mynumber = {{firstRequest.response.body.data[1].numberValue}}
+###
+
+# @no-cert-validation
+GET https://localhost:5020/ HTTP/2
+myvalue: {{myvalue}}
+mydate: {{mydate}}
+mynumber: {{mynumber}}"u8.ToArray();
+
[Fact]
public async Task PostProcessingBodyVariables()
{
@@ -215,7 +213,79 @@ public async Task PostProcessingBodyVariables()
await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
+ private byte[] _postProcessingBodyArrayRequest = @"###
+# @no-cert-validation
+# @name firstRequest
+GET https://localhost:5020/ HTTP/2
+###
+@myvalue = {{firstRequest.response.body.data[1]}}
+###
+
+# @no-cert-validation
+GET https://localhost:5020/ HTTP/2
+myvalue: {{myvalue}}"u8.ToArray();
+
+ [Fact]
+ public async Task PostProcessingBodyArrayVariables()
+ {
+ string testValue = "roundtripped header value";
+ var testDate = new DateTime(2024, 06, 02);
+ TaskCompletionSource requestReceived = new();
+ using var host = HttpServer.CreateHostBuilder(async context =>
+ {
+ if (context.Request.Headers["myvalue"] == """{"stringValue":"roundtripped header value","dateValue":"2024-06-02T00:00:00","numberValue":2}""")
+ requestReceived.TrySetResult();
+ await context.Response.WriteAsJsonAsync(new Root([new(testValue, testDate, 1), new(testValue, testDate, 2)]), new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+ }, HttpProtocols.Http2, port: Port);
+ await host.StartAsync();
+
+ var stream = new MemoryStream(_postProcessingBodyArrayRequest);
+
+ var reader = new InputReader(new ExecutionPlanBuilder());
+ var plan = await reader.ReadStreamAsync(stream);
+ var executor = new Executor(plan);
+ await executor.ExecuteAsync();
+
+ await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+
+ private byte[] _postProcessingBodyArrayIntegersRequest = @"###
+# @no-cert-validation
+# @name firstRequest
+GET https://localhost:5020/ HTTP/2
+###
+@myvalue = {{firstRequest.response.body.data}}
+###
+
+# @no-cert-validation
+GET https://localhost:5020/ HTTP/2
+myvalue: {{myvalue}}"u8.ToArray();
+
+ [Fact]
+ public async Task PostProcessingBodyArrayIntegersVariables()
+ {
+ TaskCompletionSource requestReceived = new();
+ using var host = HttpServer.CreateHostBuilder(async context =>
+ {
+ if (context.Request.Headers["myvalue"] == """[1,2,3]""")
+ requestReceived.TrySetResult();
+ await context.Response.WriteAsJsonAsync(new RootInts([1, 2, 3]));
+ }, HttpProtocols.Http2, port: Port);
+ await host.StartAsync();
+
+ var stream = new MemoryStream(_postProcessingBodyArrayIntegersRequest);
+
+ var reader = new InputReader(new ExecutionPlanBuilder());
+ var plan = await reader.ReadStreamAsync(stream);
+ var executor = new Executor(plan);
+ await executor.ExecuteAsync();
+
+ await requestReceived.Task.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+
private record class Root(IEnumerable Data);
private record class Data(string StringValue, DateTime DateValue, int NumberValue);
+
+ private record class RootInts(IEnumerable Data);
}
\ No newline at end of file
diff --git a/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs b/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs
index ccf554e..dcf5d32 100644
--- a/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs
+++ b/tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs
@@ -1,4 +1,6 @@
-namespace CHttpExecutor.Tests;
+using System.Buffers;
+
+namespace CHttpExecutor.Tests;
public class VariablePreprocessorTests
{
@@ -90,4 +92,19 @@ public void InlinedReplacementFromValues()
var result = VariablePreprocessor.Evaluate("https://{{host}}/", variables, new Dictionary());
Assert.Equal("https://localhost/", result);
}
+
+ [Fact]
+ public async Task BodyParse()
+ {
+ var responseWriter = new VariablePostProcessingWriterStrategy(true);
+ responseWriter.Buffer.Write("""{"Data":"hello"}"""u8);
+ await responseWriter.Buffer.CompleteAsync();
+ await responseWriter.CompleteAsync(CancellationToken.None);
+ var responses = new Dictionary()
+ {
+ { "first", responseWriter }
+ };
+ var result = VariablePreprocessor.Evaluate("https://{{first.response.body.$.Data}}/", new Dictionary(), responses);
+ Assert.Equal("https://hello/", result);
+ }
}