Skip to content

Commit

Permalink
Parse versions single header CSVs. Fixes #1070
Browse files Browse the repository at this point in the history
  • Loading branch information
commonsensesoftware committed Mar 22, 2024
1 parent 59ff9e6 commit 4093dc0
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 16 deletions.
73 changes: 57 additions & 16 deletions src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@

namespace Asp.Versioning.Http;

#if NET
using System.Buffers;
#endif
using System.Collections;
#if NET
using static System.StringSplitOptions;
#endif

/// <summary>
/// Represents an enumerator of API versions from a HTTP header.
/// </summary>
public readonly struct ApiVersionEnumerator : IEnumerable<ApiVersion>
{
private readonly IEnumerable<string> values;
private readonly string[] values;
private readonly IApiVersionParser parser;

/// <summary>
Expand All @@ -29,37 +35,72 @@ public ApiVersionEnumerator(
ArgumentNullException.ThrowIfNull( response );
ArgumentException.ThrowIfNullOrEmpty( headerName );

this.values =
response.Headers.TryGetValues( headerName, out var values )
? values
: Enumerable.Empty<string>();

this.values = response.Headers.TryGetValues( headerName, out var values ) ? values.ToArray() : [];
this.parser = parser ?? ApiVersionParser.Default;
}

/// <inheritdoc />
public IEnumerator<ApiVersion> GetEnumerator()
{
using var iterator = values.GetEnumerator();
#if NETSTANDARD
for ( var i = 0; i < values.Length; i++ )
{
var items = values[i].Split( ',' );

if ( !iterator.MoveNext() )
for ( var j = 0; j < items.Length; j++ )
{
var item = items[j].Trim();

if ( item.Length > 0 && parser.TryParse( item, out var result ) )
{
yield return result!;
}
}
}
#else
for ( var i = 0; i < values.Length; i++ )
{
yield break;
var (count, versions) = ParseVersions( values[i] );

for ( var j = 0; j < count; j++ )
{
yield return versions[j];
}
}
#endif
}

if ( parser.TryParse( iterator.Current, out var value ) )
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#if NET
private (int Count, ApiVersion[] Results) ParseVersions( ReadOnlySpan<char> value )
{
var pool = ArrayPool<Range>.Shared;
var ranges = pool.Rent( 5 );
var length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries );

while ( length >= ranges.Length )
{
yield return value!;
pool.Return( ranges );
length <<= 1;
ranges = pool.Rent( length );
length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries );
}

while ( iterator.MoveNext() )
var results = new ApiVersion[length];
var count = 0;

for ( var i = 0; i < length; i++ )
{
if ( parser.TryParse( iterator.Current, out value ) )
var text = value[ranges[i]];

if ( text.Length > 0 && parser.TryParse( text, out var result ) )
{
yield return value!;
results[count++] = result;
}
}
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
pool.Return( ranges );
return (count, results);
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.

namespace Asp.Versioning.Http;

public class ApiVersionEnumeratorTest
{
[Fact]
public void enumerator_should_process_single_header_value()
{
// arrange
var response = new HttpResponseMessage();

response.Headers.Add( "api-supported-versions", "1.0" );

var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" );

// act
var results = enumerator.ToArray();

// assert
results.Should().BeEquivalentTo( [new ApiVersion( 1.0 )] );
}

[Fact]
public void enumerator_should_process_multiple_header_values()
{
// arrange
var response = new HttpResponseMessage();

response.Headers.Add( "api-supported-versions", ["1.0", "2.0"] );

var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" );

// act
var results = enumerator.ToArray();

// assert
results.Should().BeEquivalentTo( new ApiVersion[] { new( 1.0 ), new( 2.0 ) } );
}

[Theory]
[InlineData( "1.0,2.0" )]
[InlineData( "1.0, 2.0" )]
[InlineData( "1.0,,2.0" )]
[InlineData( "1.0, abc, 2.0" )]
public void enumerator_should_process_single_header_comma_separated_values( string value )
{
// arrange
var response = new HttpResponseMessage();

response.Headers.Add( "api-supported-versions", [value] );

var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" );

// act
var results = enumerator.ToArray();

// assert
results.Should().BeEquivalentTo( new ApiVersion[] { new( 1.0 ), new( 2.0 ) } );
}

[Fact]
public void enumerator_should_process_many_header_comma_separated_values()
{
// arrange
const string Value = "1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0";
var response = new HttpResponseMessage();

response.Headers.Add( "api-supported-versions", [Value] );

var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" );

// act
var results = enumerator.ToArray();

// assert
results.Should().BeEquivalentTo(
new ApiVersion[]
{
new( 1.0 ),
new( 2.0 ),
new( 3.0 ),
new( 4.0 ),
new( 5.0 ),
new( 6.0 ),
new( 7.0 ),
new( 8.0 ),
new( 9.0 ),
new( 10.0 ),
} );
}

[Fact]
public void enumerator_should_process_multiple_header_comma_separated_values()
{
// arrange
var response = new HttpResponseMessage();

response.Headers.Add( "api-supported-versions", ["1.0, 2.0", "3.0, 4.0"] );

var enumerator = new ApiVersionEnumerator( response, "api-supported-versions" );

// act
var results = enumerator.ToArray();

// assert
results.Should().BeEquivalentTo(
new ApiVersion[]
{
new( 1.0 ),
new( 2.0 ),
new( 3.0 ),
new( 4.0 ),
} );
}
}

0 comments on commit 4093dc0

Please sign in to comment.