Skip to content

Commit f6b0ff6

Browse files
authored
Merge pull request #37 from WeihanLi/dev
0.4.0
2 parents 4ed2dfa + d137a38 commit f6b0ff6

38 files changed

+1285
-99
lines changed

.husky/pre-commit

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
4+
dotnet build
5+
6+
## run all tasks
7+
# husky run

.husky/task-runner.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"tasks": [
3+
{
4+
"name": "dotnet-build",
5+
"command": "dotnet",
6+
"args": [ "build" ]
7+
},
8+
{
9+
"name": "dotnet-format",
10+
"command": "dotnet",
11+
"args": [ "format", "--include", "${staged}" ],
12+
"include": [ "**/*.cs", "**/*.vb" ]
13+
}
14+
]
15+
}

Directory.Build.props

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
<Authors>WeihanLi</Authors>
1414
<Copyright>Copyright 2021-$([System.DateTime]::Now.Year) (c) WeihanLi</Copyright>
1515
</PropertyGroup>
16-
<ItemGroup>
17-
<None Include="$(MSBuildThisFileDirectory)icon.png" Pack="true" Visible="false" PackagePath=""/>
18-
</ItemGroup>
1916
<ItemGroup>
2017
<Using Include="WeihanLi.Common"/>
2118
<Using Include="WeihanLi.Common.Helpers"/>

build/version.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<Project>
22
<PropertyGroup>
33
<VersionMajor>0</VersionMajor>
4-
<VersionMinor>3</VersionMinor>
5-
<VersionPatch>4</VersionPatch>
4+
<VersionMinor>4</VersionMinor>
5+
<VersionPatch>0</VersionPatch>
66
<VersionPrefix>$(VersionMajor).$(VersionMinor).$(VersionPatch)</VersionPrefix>
77
<InformationalVersion>$(PackageVersion)</InformationalVersion>
88
</PropertyGroup>

docs/ReleaseNotes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# dotnet-httpie release notes
22

3+
## [0.4.0](https://nuget.org/packages/dotnet-httpie/0.4.0)
4+
5+
- Add support for download file
6+
- Add support for response json schema validation
7+
- Add `RequestCacheMiddleware`
8+
- Add load test exporter
9+
310
## [0.3.0](https://nuget.org/packages/dotnet-httpie/0.3.0)
411

512
- Add support for load test

src/Directory.Build.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
<PackageReleaseNotes>https://github.com/WeihanLi/dotnet-httpie/tree/main/docs/ReleaseNotes.md</PackageReleaseNotes>
99
<ContinuousIntegrationBuild Condition="'$(TF_BUILD)' == 'true' or '$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
1010
</PropertyGroup>
11+
<ItemGroup>
12+
<None Include="$(MSBuildThisFileDirectory)../icon.png" Pack="true" Visible="false" PackagePath=""/>
13+
</ItemGroup>
1114
</Project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Weihan Li. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using HTTPie.Models;
5+
6+
namespace HTTPie.Abstractions;
7+
8+
public interface ILoadTestExporter : IPlugin
9+
{
10+
string Type { get; }
11+
12+
ValueTask Export(HttpContext context, HttpResponseModel[] responseList);
13+
}
14+
15+
public interface ILoadTestExporterSelector : IPlugin
16+
{
17+
ILoadTestExporter? Select();
18+
}

src/HTTPie/Abstractions/IOutputFormatter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ namespace HTTPie.Abstractions;
77

88
public interface IOutputFormatter : IPlugin
99
{
10-
string GetOutput(HttpContext httpContext);
10+
Task<string> GetOutput(HttpContext httpContext);
1111
}

src/HTTPie/HTTPie.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Using Include="System.CommandLine.Parsing" />
1616
</ItemGroup>
1717
<ItemGroup>
18+
<PackageReference Include="JsonSchema.Net" Version="2.2.1" />
1819
<PackageReference Include="MathNet.Numerics.Signed" Version="5.0.0-beta01" />
1920
<PackageReference Include="System.CommandLine" Version="2.0.0-beta3.22114.1" />
2021
<PackageReference Include="WeihanLi.Common" Version="1.0.50" />
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Weihan Li. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using HTTPie.Abstractions;
5+
using HTTPie.Models;
6+
using System.Text.Json;
7+
8+
namespace HTTPie.Implement;
9+
10+
public sealed class JsonLoadTestExporter : ILoadTestExporter
11+
{
12+
private static readonly Option<string> OutputJsonPathOption = new("--export-json-path", "Expected export json file path");
13+
14+
public ICollection<Option> SupportedOptions()
15+
{
16+
return new[] { OutputJsonPathOption };
17+
}
18+
19+
public string Type => "Json";
20+
21+
public async ValueTask Export(HttpContext context, HttpResponseModel[] responseList)
22+
{
23+
var jsonPath = context.Request.ParseResult.GetValueForOption(OutputJsonPathOption);
24+
if (string.IsNullOrEmpty(jsonPath))
25+
{
26+
return;
27+
}
28+
var result = new
29+
{
30+
context.Response.Elapsed,
31+
context.Request,
32+
ResponseList = responseList
33+
};
34+
await using var fs = File.Create(jsonPath);
35+
await JsonSerializer.SerializeAsync(fs, result);
36+
await fs.FlushAsync();
37+
}
38+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Weihan Li. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using HTTPie.Abstractions;
5+
using HTTPie.Models;
6+
7+
namespace HTTPie.Implement;
8+
9+
public sealed class LoadTestExporterSelector : ILoadTestExporterSelector
10+
{
11+
private readonly HttpContext _context;
12+
private readonly Dictionary<string, ILoadTestExporter> _exporters;
13+
private static readonly Option<string> ExporterTypeOption = new("--exporter-type", "Load test result exporter type");
14+
15+
public ICollection<Option> SupportedOptions()
16+
{
17+
return new[] { ExporterTypeOption };
18+
}
19+
20+
public LoadTestExporterSelector(HttpContext context, IEnumerable<ILoadTestExporter> exporters)
21+
{
22+
_context = context;
23+
_exporters = exporters.ToDictionary(x => x.Type, x => x, StringComparer.OrdinalIgnoreCase);
24+
ExporterTypeOption.AddCompletions(_exporters.Keys.ToArray());
25+
}
26+
27+
public ILoadTestExporter? Select()
28+
{
29+
var exporterType = _context.Request.ParseResult.GetValueForOption(ExporterTypeOption) ?? string.Empty;
30+
_exporters.TryGetValue(exporterType, out var exporter);
31+
return exporter;
32+
}
33+
}

src/HTTPie/Implement/OutputFormatter.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
using HTTPie.Models;
66
using HTTPie.Utilities;
77
using MathNet.Numerics.Statistics;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Logging;
810
using Microsoft.Extensions.Primitives;
911
using System.Text;
1012
using System.Text.Json.Nodes;
13+
using WeihanLi.Common.Extensions;
1114

1215
namespace HTTPie.Implement;
1316

@@ -20,16 +23,25 @@ public enum PrettyOptions
2023
All = 3
2124
}
2225

23-
public class OutputFormatter : IOutputFormatter
26+
public sealed class OutputFormatter : IOutputFormatter
2427
{
25-
public static readonly Option<PrettyOptions> PrettyOption = new("--pretty", () => PrettyOptions.All, "pretty output");
28+
private readonly IServiceProvider _serviceProvider;
29+
private readonly ILogger<OutputFormatter> _logger;
30+
private static readonly Option<PrettyOptions> PrettyOption = new("--pretty", () => PrettyOptions.All, "pretty output");
2631

27-
public static readonly Option QuietOption = new(new[] { "--quiet", "-q" }, "quiet mode, output nothing");
32+
private static readonly Option QuietOption = new(new[] { "--quiet", "-q" }, "quiet mode, output nothing");
2833
public static readonly Option OfflineOption = new("--offline", "offline mode, would not send the request, just print request info");
29-
public static readonly Option OutputHeadersOption = new(new[] { "-h", "--headers" }, "output response headers only");
30-
public static readonly Option OutputBodyOption = new(new[] { "-b", "--body" }, "output response headers and response body only");
31-
public static readonly Option OutputVerboseOption = new(new[] { "-v", "--verbose" }, "output request/response, response headers and response body");
32-
public static readonly Option<string> OutputPrintModeOption = new(new[] { "-p", "--print" }, "print mode, output specific info,H:request headers,B:request body,h:response headers,b:response body");
34+
private static readonly Option OutputHeadersOption = new(new[] { "-h", "--headers" }, "output response headers only");
35+
private static readonly Option OutputBodyOption = new(new[] { "-b", "--body" }, "output response headers and response body only");
36+
private static readonly Option OutputVerboseOption = new(new[] { "-v", "--verbose" }, "output request/response, response headers and response body");
37+
private static readonly Option<string> OutputPrintModeOption = new(new[] { "-p", "--print" }, "print mode, output specific info,H:request headers,B:request body,h:response headers,b:response body");
38+
39+
40+
public OutputFormatter(IServiceProvider serviceProvider, ILogger<OutputFormatter> logger)
41+
{
42+
_serviceProvider = serviceProvider;
43+
_logger = logger;
44+
}
3345

3446
public ICollection<Option> SupportedOptions() => new HashSet<Option>()
3547
{
@@ -91,12 +103,12 @@ public static OutputFormat GetOutputFormat(HttpContext httpContext)
91103
return outputFormat;
92104
}
93105

94-
public string GetOutput(HttpContext httpContext)
106+
public async Task<string> GetOutput(HttpContext httpContext)
95107
{
96108
var isLoadTest = httpContext.GetFlag(Constants.FlagNames.IsLoadTest);
97109
var outputFormat = GetOutputFormat(httpContext);
98110
return isLoadTest
99-
? GetLoadTestOutput(httpContext, outputFormat)
111+
? await GetLoadTestOutput(httpContext, outputFormat).ConfigureAwait(false)
100112
: GetCommonOutput(httpContext, outputFormat);
101113
}
102114

@@ -135,7 +147,7 @@ private static string GetCommonOutput(HttpContext httpContext, OutputFormat outp
135147
return output.ToString();
136148
}
137149

138-
private static string GetLoadTestOutput(HttpContext httpContext, OutputFormat outputFormat)
150+
private async Task<string> GetLoadTestOutput(HttpContext httpContext, OutputFormat outputFormat)
139151
{
140152
httpContext.TryGetProperty(Constants.ResponseListPropertyName,
141153
out HttpResponseModel[]? responseList);
@@ -164,6 +176,17 @@ private static string GetLoadTestOutput(HttpContext httpContext, OutputFormat ou
164176
P50 = SortedArrayStatistics.Quantile(durationInMs, 0.50),
165177
};
166178

179+
try
180+
{
181+
var exporter = _serviceProvider.GetService<ILoadTestExporter>();
182+
if (exporter != null)
183+
await exporter.Export(httpContext, responseList);
184+
}
185+
catch (Exception ex)
186+
{
187+
_logger.LogError(ex, "Export load test result failed");
188+
}
189+
167190
return $@"{GetCommonOutput(httpContext, outputFormat & OutputFormat.RequestInfo)}
168191
Total requests: {reportModel.TotalRequestCount}({reportModel.TotalElapsed} ms), successCount: {reportModel.SuccessRequestCount}({reportModel.SuccessRequestRate}%), failedCount: {reportModel.FailRequestCount}
169192
@@ -218,3 +241,4 @@ private static string GetHeadersString(IDictionary<string, StringValues> headers
218241
$"{headers.Select(h => $"{h.Key}: {h.Value}").OrderBy(h => h).StringJoin(Environment.NewLine)}";
219242
}
220243
}
244+

src/HTTPie/Implement/RequestExecutor.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
using System.Collections.Concurrent;
99
using System.Diagnostics;
1010
using System.Net;
11+
using WeihanLi.Common.Extensions;
1112
using WeihanLi.Common.Http;
1213

1314
namespace HTTPie.Implement;
1415

15-
public partial class RequestExecutor : IRequestExecutor
16+
public sealed partial class RequestExecutor : IRequestExecutor
1617
{
1718
private readonly Func<HttpClientHandler, Task> _httpHandlerPipeline;
1819
private readonly ILogger _logger;
@@ -21,10 +22,10 @@ public partial class RequestExecutor : IRequestExecutor
2122
private readonly IResponseMapper _responseMapper;
2223
private readonly Func<HttpContext, Task> _responsePipeline;
2324

24-
public static readonly Option<double> TimeoutOption = new("--timeout", "Request timeout in seconds");
25-
public static readonly Option<int> IterationOption = new(new[] { "-n", "--iteration" }, () => 1, "Request iteration");
26-
public static readonly Option<int> VirtualUserOption = new(new[] { "--vu", "--vus", "--virtual-users" }, () => 1, "Virtual users");
27-
public static readonly Option<string> DurationOption = new(new[] { "-d", "--duration" }, "Duration");
25+
private static readonly Option<double> TimeoutOption = new("--timeout", "Request timeout in seconds");
26+
private static readonly Option<int> IterationOption = new(new[] { "-n", "--iteration" }, () => 1, "Request iteration");
27+
private static readonly Option<int> VirtualUserOption = new(new[] { "--vu", "--vus", "--virtual-users" }, () => 1, "Virtual users");
28+
private static readonly Option<string> DurationOption = new(new[] { "--duration" }, "Request duration, 10s/1m ...");
2829

2930
public Option[] SupportedOptions()
3031
{

src/HTTPie/Implement/RequestMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace HTTPie.Implement;
1111

12-
public class RequestMapper : IRequestMapper
12+
public sealed class RequestMapper : IRequestMapper
1313
{
1414
public Task<HttpRequestMessage> ToRequestMessage(HttpContext httpContext)
1515
{

src/HTTPie/Implement/ResponseMapper.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,47 @@
77

88
namespace HTTPie.Implement;
99

10-
public class ResponseMapper : IResponseMapper
10+
public sealed class ResponseMapper : IResponseMapper
1111
{
12-
private readonly HttpContext _httpContext;
13-
14-
public ResponseMapper(HttpContext httpContext)
15-
{
16-
_httpContext = httpContext;
17-
}
1812
public async Task<HttpResponseModel> ToResponseModel(HttpResponseMessage responseMessage)
1913
{
2014
var responseModel = new HttpResponseModel
2115
{
2216
HttpVersion = responseMessage.Version,
2317
StatusCode = responseMessage.StatusCode,
18+
Headers = responseMessage.Headers
19+
.Union(responseMessage.Content.Headers)
20+
.ToDictionary(x => x.Key, x => new StringValues(x.Value.ToArray())),
21+
Bytes = await responseMessage.Content.ReadAsByteArrayAsync(),
2422
};
25-
var outputFormat = OutputFormatter.GetOutputFormat(_httpContext);
26-
if (outputFormat.HasFlag(OutputFormat.ResponseHeaders))
23+
if (IsTextResponse(responseMessage))
2724
{
28-
responseModel.Headers = responseMessage.Headers
29-
.Union(responseMessage.Content.Headers)
30-
.ToDictionary(x => x.Key, x => new StringValues(x.Value.ToArray()));
25+
try
26+
{
27+
responseModel.Body = responseModel.Bytes.GetString();
28+
}
29+
catch
30+
{
31+
// ignored
32+
}
3133
}
32-
if (outputFormat.HasFlag(OutputFormat.ResponseBody))
34+
return responseModel;
35+
}
36+
37+
private static bool IsTextResponse(HttpResponseMessage response)
38+
{
39+
if (response.Content.Headers.ContentType?.MediaType is null)
3340
{
34-
responseModel.Body = await responseMessage.Content.ReadAsStringAsync();
41+
return true;
3542
}
36-
return responseModel;
43+
var contentType = response.Content.Headers.ContentType;
44+
var mediaType = contentType.MediaType;
45+
var isTextContent = mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)
46+
|| mediaType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)
47+
|| mediaType.StartsWith("application/xml", StringComparison.OrdinalIgnoreCase)
48+
|| mediaType.StartsWith("application/javascript", StringComparison.OrdinalIgnoreCase)
49+
;
50+
return isTextContent;
3751
}
52+
3853
}

src/HTTPie/Middleware/AuthorizationMiddleware.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
namespace HTTPie.Middleware;
1010

11-
public class AuthorizationMiddleware : IRequestMiddleware
11+
public sealed class AuthorizationMiddleware : IRequestMiddleware
1212
{
13-
public static readonly Option<string> AuthenticationTypeOption = new(new[] { "--auth-type", "-A" }, () => "Basic", "Authentication type");
14-
public static readonly Option<string> AuthenticationValueOption = new(new[] { "--auth", "-a" }, "Authentication value");
13+
private static readonly Option<string> AuthenticationTypeOption = new(new[] { "--auth-type", "-A" }, () => "Basic", "Authentication type");
14+
private static readonly Option<string> AuthenticationValueOption = new(new[] { "--auth", "-a" }, "Authentication value");
1515

1616
static AuthorizationMiddleware()
1717
{

0 commit comments

Comments
 (0)