Skip to content

Parse Single Header CSVs #1073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<VersionPrefix>8.0.0</VersionPrefix>
<VersionPrefix>8.0.1</VersionPrefix>
<AssemblyVersion>8.0.0.0</AssemblyVersion>
<TargetFrameworks>$(DefaultTargetFramework);netstandard1.1;netstandard2.0</TargetFrameworks>
<RootNamespace>Asp.Versioning.Http</RootNamespace>
Expand Down
2 changes: 1 addition & 1 deletion src/Client/src/Asp.Versioning.Http.Client/ReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@

Parse single header CSVs ([#1070](https://github.com/dotnet/aspnet-api-versioning/issues/1070))
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 ),
} );
}
}
Loading