Skip to content

Commit

Permalink
Support loading bundles from streams
Browse files Browse the repository at this point in the history
Add support for loading bundles from streams to support the use case of embedding bundles as resources in an assembly.
  • Loading branch information
martincostello committed Apr 13, 2024
1 parent 282d533 commit 452f006
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 22 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</ItemGroup>
<PropertyGroup>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisMode>All</AnalysisMode>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)justeat-oss.snk</AssemblyOriginatorKeyFile>
<Authors>JUSTEAT_OSS</Authors>
<ChecksumAlgorithm>SHA256</ChecksumAlgorithm>
Expand Down Expand Up @@ -41,7 +41,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<UseArtifactsOutput>true</UseArtifactsOutput>
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<VersionPrefix>4.2.2</VersionPrefix>
<VersionPrefix>4.3.0</VersionPrefix>
</PropertyGroup>
<PropertyGroup Condition=" '$(GITHUB_ACTIONS)' != '' ">
<VersionSuffix Condition=" '$(VersionSuffix)' == '' AND '$(GITHUB_HEAD_REF)' == '' ">beta.$(GITHUB_RUN_NUMBER)</VersionSuffix>
Expand Down
82 changes: 78 additions & 4 deletions src/HttpClientInterception/BundleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static class BundleExtensions
/// The version of the serialized bundle is not supported.
/// </exception>
public static HttpClientInterceptorOptions RegisterBundle(this HttpClientInterceptorOptions options, string path)
=> options.RegisterBundle(path, Array.Empty<KeyValuePair<string, string>>());
=> options.RegisterBundle(path, []);

/// <summary>
/// Registers a bundle of HTTP request interceptions from a specified JSON file.
Expand Down Expand Up @@ -64,7 +64,44 @@ public static HttpClientInterceptorOptions RegisterBundle(
throw new ArgumentNullException(nameof(templateValues));
}

var bundle = BundleFactory.Create(path);
using var stream = File.OpenRead(path);

return options.RegisterBundleFromStream(stream, templateValues);
}

/// <summary>
/// Registers a bundle of HTTP request interceptions from a specified JSON stream.
/// </summary>
/// <param name="options">The <see cref="HttpClientInterceptorOptions"/> to register the bundle with.</param>
/// <param name="stream">A <see cref="Stream"/> of JSON containing the serialized bundle.</param>
/// <param name="templateValues">The optional template values to specify.</param>
/// <returns>
/// The value specified by <paramref name="options"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="options"/> or <paramref name="stream"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="NotSupportedException">
/// The version of the serialized bundle is not supported.
/// </exception>
public static HttpClientInterceptorOptions RegisterBundleFromStream(
this HttpClientInterceptorOptions options,
Stream stream,
IEnumerable<KeyValuePair<string, string>>? templateValues = default)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}

templateValues ??= [];

var bundle = BundleFactory.Create(stream);

return options.RegisterBundle(bundle, templateValues);
}
Expand Down Expand Up @@ -101,9 +138,46 @@ public static async Task<HttpClientInterceptorOptions> RegisterBundleAsync(
throw new ArgumentNullException(nameof(path));
}

templateValues ??= Array.Empty<KeyValuePair<string, string>>();
using var stream = File.OpenRead(path);

return await options.RegisterBundleFromStreamAsync(stream, templateValues, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Registers a bundle of HTTP request interceptions from a specified stream.
/// </summary>
/// <param name="options">The <see cref="HttpClientInterceptorOptions"/> to register the bundle with.</param>
/// <param name="stream">A <see cref="Stream"/> of JSON containing the serialized bundle.</param>
/// <param name="templateValues">The optional template values to specify.</param>
/// <param name="cancellationToken">The optional <see cref="CancellationToken"/> to use.</param>
/// <returns>
/// The value specified by <paramref name="options"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="options"/> or <paramref name="stream"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="NotSupportedException">
/// The version of the serialized bundle is not supported.
/// </exception>
public static async Task<HttpClientInterceptorOptions> RegisterBundleFromStreamAsync(
this HttpClientInterceptorOptions options,
Stream stream,
IEnumerable<KeyValuePair<string, string>>? templateValues = default,
CancellationToken cancellationToken = default)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}

templateValues ??= [];

var bundle = await BundleFactory.CreateAsync(path, cancellationToken).ConfigureAwait(false);
var bundle = await BundleFactory.CreateAsync(stream, cancellationToken).ConfigureAwait(false);

return options.RegisterBundle(bundle!, templateValues);
}
Expand Down
28 changes: 16 additions & 12 deletions src/HttpClientInterception/Bundles/BundleFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,39 @@ internal static class BundleFactory
#endif

/// <summary>
/// Creates a <see cref="Bundle"/> from the specified JSON file.
/// Creates a <see cref="Bundle"/> from the specified JSON stream.
/// </summary>
/// <param name="path">The path of the JSON file containing the bundle.</param>
/// <param name="stream">The <see cref="Stream"/> of JSON containing the bundle.</param>
/// <returns>
/// The <see cref="Bundle"/> deserialized from the file specified by <paramref name="path"/>.
/// The <see cref="Bundle"/> deserialized from the stream specified by <paramref name="stream"/>.
/// </returns>
public static Bundle? Create(string path)
public static Bundle? Create(Stream stream)
{
string json = File.ReadAllText(path);
#if NET6_0_OR_GREATER
return JsonSerializer.Deserialize(json, BundleJsonSerializerContext.Default.Bundle);
return JsonSerializer.Deserialize(stream, BundleJsonSerializerContext.Default.Bundle);
#else
string json;

using (var reader = new StreamReader(stream))
{
json = reader.ReadToEnd();
}

return JsonSerializer.Deserialize<Bundle>(json, Settings);
#endif
}

/// <summary>
/// Creates a <see cref="Bundle"/> from the specified JSON file as an asynchronous operation.
/// Creates a <see cref="Bundle"/> from the specified JSON stream as an asynchronous operation.
/// </summary>
/// <param name="path">The path of the JSON file containing the bundle.</param>
/// <param name="stream">The <see cref="Stream"/> of JSON containing the bundle.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
/// <returns>
/// A <see cref="ValueTask{Bundle}"/> representing the asynchronous operation which
/// returns the bundle deserialized from the file specified by <paramref name="path"/>.
/// returns the bundle deserialized from the stream specified by <paramref name="stream"/>.
/// </returns>
public static async ValueTask<Bundle?> CreateAsync(string path, CancellationToken cancellationToken)
public static async ValueTask<Bundle?> CreateAsync(Stream stream, CancellationToken cancellationToken)
{
using var stream = File.OpenRead(path);

#if NET6_0_OR_GREATER
return await JsonSerializer.DeserializeAsync(stream, BundleJsonSerializerContext.Default.Bundle, cancellationToken).ConfigureAwait(false);
#else
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStream(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null) -> JustEat.HttpClientInterception.HttpClientInterceptorOptions!
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStreamAsync(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<JustEat.HttpClientInterception.HttpClientInterceptorOptions!>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStream(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null) -> JustEat.HttpClientInterception.HttpClientInterceptorOptions!
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStreamAsync(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<JustEat.HttpClientInterception.HttpClientInterceptorOptions!>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStream(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null) -> JustEat.HttpClientInterception.HttpClientInterceptorOptions!
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStreamAsync(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<JustEat.HttpClientInterception.HttpClientInterceptorOptions!>!
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStream(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null) -> JustEat.HttpClientInterception.HttpClientInterceptorOptions!
static JustEat.HttpClientInterception.BundleExtensions.RegisterBundleFromStreamAsync(this JustEat.HttpClientInterception.HttpClientInterceptorOptions! options, System.IO.Stream! stream, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, string!>>? templateValues = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<JustEat.HttpClientInterception.HttpClientInterceptorOptions!>!
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,56 @@ public static async Task Can_Intercept_Http_Requests_From_Bundle_File_With_Templ
.ShouldBe(@"[{""id"":123456,""name"":""httpclient-interception"",""full_name"":""justeattakeaway/httpclient-interception"",""private"":false,""owner"":{""login"":""justeattakeaway"",""id"":1516790}}]");
}

[Fact]
public static async Task Can_Intercept_Http_Requests_From_Bundle_Stream()
{
// Arrange
var options = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration();

var headers = new Dictionary<string, string>()
{
["accept"] = "application/vnd.github.v3+json",
["authorization"] = "token my-token",
["user-agent"] = "My-App/1.0.0",
};

using var stream = typeof(BundleExtensionsTests).Assembly.GetManifestResourceStream("JustEat.HttpClientInterception.Bundles.http-request-bundle.json");
stream.ShouldNotBeNull();

// Act
options.RegisterBundleFromStream(stream);

// Assert
await HttpAssert.GetAsync(options, "https://www.just-eat.co.uk/", mediaType: "text/html");
await HttpAssert.GetAsync(options, "https://www.just-eat.co.uk/order-history");
await HttpAssert.GetAsync(options, "https://api.github.com/orgs/justeattakeaway", headers: headers, mediaType: "application/json");
}

[Fact]
public static async Task Can_Intercept_Http_Requests_From_Bundle_Stream_Async()
{
// Arrange
var options = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration();

var headers = new Dictionary<string, string>()
{
["accept"] = "application/vnd.github.v3+json",
["authorization"] = "token my-token",
["user-agent"] = "My-App/1.0.0",
};

using var stream = typeof(BundleExtensionsTests).Assembly.GetManifestResourceStream("JustEat.HttpClientInterception.Bundles.http-request-bundle.json");
stream.ShouldNotBeNull();

// Act
await options.RegisterBundleFromStreamAsync(stream);

// Assert
await HttpAssert.GetAsync(options, "https://www.just-eat.co.uk/", mediaType: "text/html");
await HttpAssert.GetAsync(options, "https://www.just-eat.co.uk/order-history");
await HttpAssert.GetAsync(options, "https://api.github.com/orgs/justeattakeaway", headers: headers, mediaType: "application/json");
}

[Fact]
public static void RegisterBundle_Validates_Parameters()
{
Expand All @@ -336,6 +386,18 @@ public static void RegisterBundle_Validates_Parameters()
Should.Throw<ArgumentNullException>(() => options.RegisterBundle(path, null)).ParamName.ShouldBe("templateValues");
}

[Fact]
public static void RegisterBundleFromStream_Validates_Parameters()
{
// Arrange
var options = new HttpClientInterceptorOptions();
var stream = Stream.Null;

// Act and Assert
Should.Throw<ArgumentNullException>(() => ((HttpClientInterceptorOptions)null).RegisterBundleFromStream(stream)).ParamName.ShouldBe("options");
Should.Throw<ArgumentNullException>(() => options.RegisterBundleFromStream(null)).ParamName.ShouldBe("stream");
}

[Fact]
public static async Task RegisterBundleAsync_Validates_Parameters()
{
Expand All @@ -348,6 +410,18 @@ public static async Task RegisterBundleAsync_Validates_Parameters()
(await Should.ThrowAsync<ArgumentNullException>(() => options.RegisterBundleAsync(null))).ParamName.ShouldBe("path");
}

[Fact]
public static async Task RegisterBundleFromStreamAsync_Validates_Parameters()
{
// Arrange
var options = new HttpClientInterceptorOptions();
var stream = Stream.Null;

// Act and Assert
(await Should.ThrowAsync<ArgumentNullException>(() => ((HttpClientInterceptorOptions)null).RegisterBundleFromStreamAsync(stream))).ParamName.ShouldBe("options");
(await Should.ThrowAsync<ArgumentNullException>(() => options.RegisterBundleFromStreamAsync(null))).ParamName.ShouldBe("stream");
}

[Fact]
public static void RegisterBundle_Throws_If_Bundle_Version_Is_Not_Supported()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
<NoWarn>$(NoWarn);CA1303;CA1600;CA1707;CA1812;CA1861;CA2000;CA2007;SA1600;SA1601</NoWarn>
<RootNamespace>JustEat.HttpClientInterception</RootNamespace>
<Summary>Tests for JustEat.HttpClientInterception</Summary>
<TargetFrameworks>net8.0</TargetFrameworks>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="example-bundle.json;xunit.runner.json;Bundles\*.json" CopyToOutputDirectory="PreserveNewest" />
<None Remove="Bundles\*.json" />
<Content Include="..\..\src\HttpClientInterception\Bundles\http-request-bundle-schema.json" CopyToOutputDirectory="PreserveNewest" />
<EmbeddedResource Include="Bundles\http-request-bundle.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\HttpClientInterception\JustEat.HttpClientInterception.csproj" />
Expand All @@ -27,7 +28,4 @@
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

0 comments on commit 452f006

Please sign in to comment.