Skip to content

Commit

Permalink
Using JsonPath
Browse files Browse the repository at this point in the history
  • Loading branch information
ladeak committed Jun 2, 2024
1 parent 99d658d commit 92e8b52
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 42 deletions.
6 changes: 5 additions & 1 deletion src/CHttpExecutor/CHttpExecutor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="CHttpExecutor.Tests"/>
<InternalsVisibleTo Include="CHttpExecutor.Tests" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="JsonPath.Net" Version="1.1.0" />
</ItemGroup>

</Project>
4 changes: 2 additions & 2 deletions src/CHttpExecutor/VariablePostProcessingWriterStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
51 changes: 44 additions & 7 deletions src/CHttpExecutor/VariablePreprocessor.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -118,37 +120,42 @@ private static bool TryGetPathValue(ReadOnlySpan<char> 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<char> jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result)
private static bool ParseBody_Custom(ReadOnlySpan<char> 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;
jsonPath = ReadOnlySpan<char>.Empty;
}
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);
Expand All @@ -162,6 +169,36 @@ private static bool ParseBody(ReadOnlySpan<char> jsonPath, VariablePostProcessin
return true;
}

private static bool ParseBody(ReadOnlySpan<char> 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<char> jsonPath, VariablePostProcessingWriterStrategy responseCtx, ref string result)
{
var headerName = jsonPath.ToString();
Expand Down
132 changes: 101 additions & 31 deletions tests/CHttpExecutor.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand All @@ -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()
{
Expand All @@ -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> Data);

private record class Data(string StringValue, DateTime DateValue, int NumberValue);

private record class RootInts(IEnumerable<int> Data);
}
19 changes: 18 additions & 1 deletion tests/CHttpExecutor.Tests/VariablePreprocessorTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace CHttpExecutor.Tests;
using System.Buffers;

namespace CHttpExecutor.Tests;

public class VariablePreprocessorTests
{
Expand Down Expand Up @@ -90,4 +92,19 @@ public void InlinedReplacementFromValues()
var result = VariablePreprocessor.Evaluate("https://{{host}}/", variables, new Dictionary<string, VariablePostProcessingWriterStrategy>());
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<string, VariablePostProcessingWriterStrategy>()
{
{ "first", responseWriter }
};
var result = VariablePreprocessor.Evaluate("https://{{first.response.body.$.Data}}/", new Dictionary<string, string>(), responses);
Assert.Equal("https://hello/", result);
}
}

0 comments on commit 92e8b52

Please sign in to comment.