Add structured syntax suffixes and facets to MediaTypeHeaderValue (#921)

This commit is contained in:
Justin Kotalik 2017-08-29 10:20:56 -07:00 committed by GitHub
parent ce68ec23c0
commit e97e6546c2
4 changed files with 448 additions and 63 deletions

View File

@ -11,10 +11,22 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.Net.Http.Headers
{
/// <summary>
/// Representation of the media type header. See <see href="https://tools.ietf.org/html/rfc6838"/>.
/// </summary>
public class MediaTypeHeaderValue
{
private const string CharsetString = "charset";
private const string BoundaryString = "boundary";
private const string CharsetString = "charset";
private const string MatchesAllString = "*/*";
private const string QualityString = "q";
private const string WildcardString = "*";
private const char ForwardSlashCharacter = '/';
private const char PeriodCharacter = '.';
private const char PlusCharacter = '+';
private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter };
private static readonly HttpHeaderParser<MediaTypeHeaderValue> SingleValueParser
= new GenericHeaderParser<MediaTypeHeaderValue>(false, GetMediaTypeLength);
@ -31,18 +43,33 @@ namespace Microsoft.Net.Http.Headers
// Used by the parser to create a new instance of this type.
}
/// <summary>
/// Initializes a <see cref="MediaTypeHeaderValue"/> instance.
/// </summary>
/// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type.
/// The text provided must be a single media type without parameters. </param>
public MediaTypeHeaderValue(StringSegment mediaType)
{
CheckMediaTypeFormat(mediaType, "mediaType");
CheckMediaTypeFormat(mediaType, nameof(mediaType));
_mediaType = mediaType;
}
/// <summary>
/// Initializes a <see cref="MediaTypeHeaderValue"/> instance.
/// </summary>
/// <param name="mediaType">A <see cref="StringSegment"/> representation of a media type.
/// The text provided must be a single media type without parameters. </param>
/// <param name="quality">The <see cref="double"/> with the quality of the media type.</param>
public MediaTypeHeaderValue(StringSegment mediaType, double quality)
: this(mediaType)
{
Quality = quality;
}
/// <summary>
/// Gets or sets the value of the charset parameter. Returns <see cref="StringSegment.Empty"/>
/// if there is no charset.
/// </summary>
public StringSegment Charset
{
get
@ -77,6 +104,10 @@ namespace Microsoft.Net.Http.Headers
}
}
/// <summary>
/// Gets or sets the value of the Encoding parameter. Setting the Encoding will set
/// the <see cref="Charset"/> to <see cref="Encoding.WebName"/>.
/// </summary>
public Encoding Encoding
{
get
@ -109,6 +140,10 @@ namespace Microsoft.Net.Http.Headers
}
}
/// <summary>
/// Gets or sets the value of the boundary parameter. Returns <see cref="StringSegment.Empty"/>
/// if there is no boundary.
/// </summary>
public StringSegment Boundary
{
get
@ -141,6 +176,10 @@ namespace Microsoft.Net.Http.Headers
}
}
/// <summary>
/// Gets or sets the media type's parameters. Returns an empty <see cref="IList{T}"/>
/// if there are no parameters.
/// </summary>
public IList<NameValueHeaderValue> Parameters
{
get
@ -160,6 +199,10 @@ namespace Microsoft.Net.Http.Headers
}
}
/// <summary>
/// Gets or sets the value of the quality parameter. Returns null
/// if there is no quality.
/// </summary>
public double? Quality
{
get { return HeaderUtilities.GetQuality(_parameters); }
@ -170,55 +213,155 @@ namespace Microsoft.Net.Http.Headers
}
}
/// <summary>
/// Gets or sets the value of the media type. Returns <see cref="StringSegment.Empty"/>
/// if there is no media type.
/// </summary>
/// <example>
/// For the media type <c>"application/json"</c>, the property gives the value
/// <c>"application/json"</c>.
/// </example>
public StringSegment MediaType
{
get { return _mediaType; }
set
{
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
CheckMediaTypeFormat(value, "value");
CheckMediaTypeFormat(value, nameof(value));
_mediaType = value;
}
}
/// <summary>
/// Gets the type of the <see cref="MediaTypeHeaderValue"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/json"</c>, the property gives the value <c>"application"</c>.
/// </example>
/// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/> for more details on the type.</remarks>
public StringSegment Type
{
get
{
return _mediaType.Subsegment(0, _mediaType.IndexOf('/'));
return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter));
}
}
/// <summary>
/// Gets the subtype of the <see cref="MediaTypeHeaderValue"/>.
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example+json"</c>.
/// </example>
/// <remarks>See <see href="https://tools.ietf.org/html/rfc6838#section-4.2"/> for more details on the subtype.</remarks>
public StringSegment SubType
{
get
{
return _mediaType.Subsegment(_mediaType.IndexOf('/') + 1);
return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1);
}
}
/// <summary>
/// MediaType = "*/*"
/// Gets subtype of the <see cref="MediaTypeHeaderValue"/>, excluding any structured syntax suffix. Returns <see cref="StringSegment.Empty"/>
/// if there is no subtype without suffix.
/// </summary>
public bool MatchesAllTypes
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"vnd.example"</c>.
/// </example>
public StringSegment SubTypeWithoutSuffix
{
get
{
return MediaType.Equals("*/*", StringComparison.Ordinal);
var subType = SubType;
var startOfSuffix = subType.LastIndexOf(PlusCharacter);
if (startOfSuffix == -1)
{
return subType;
}
else
{
return subType.Subsegment(0, startOfSuffix);
}
}
}
/// <summary>
/// SubType = "*"
/// Gets the structured syntax suffix of the <see cref="MediaTypeHeaderValue"/> if it has one.
/// See <see href="https://tools.ietf.org/html/rfc6838#section-4.8">The RFC documentation on structured syntaxes.</see>
/// </summary>
public bool MatchesAllSubTypes
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value
/// <c>"json"</c>.
/// </example>
public StringSegment Suffix
{
get
{
return SubType.Equals("*", StringComparison.Ordinal);
var subType = SubType;
var startOfSuffix = subType.LastIndexOf(PlusCharacter);
if (startOfSuffix == -1)
{
return default(StringSegment);
}
else
{
return subType.Subsegment(startOfSuffix + 1);
}
}
}
/// <summary>
/// Get a <see cref="IList{T}"/> of facets of the <see cref="MediaTypeHeaderValue"/>. Facets are a
/// period separated list of StringSegments in the <see cref="SubTypeWithoutSuffix"/>.
/// See <see href="https://tools.ietf.org/html/rfc6838#section-3">The RFC documentation on facets.</see>
/// </summary>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, the property gives the value:
/// <c>{"vnd", "example"}</c>
/// </example>
public IEnumerable<StringSegment> Facets
{
get
{
return SubTypeWithoutSuffix.Split(PeriodCharacterArray);
}
}
/// <summary>
/// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all types.
/// </summary>
public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal);
/// <summary>
/// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes.
/// </summary>
/// <example>
/// For the media type <c>"application/*"</c>, this property is <c>true</c>.
/// </example>
/// <example>
/// For the media type <c>"application/json"</c>, this property is <c>false</c>.
/// </example>
public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal);
/// <summary>
/// Gets whether this <see cref="MediaTypeHeaderValue"/> matches all subtypes, ignoring any structured syntax suffix.
/// </summary>
/// <example>
/// For the media type <c>"application/*+json"</c>, this property is <c>true</c>.
/// </example>
/// <example>
/// For the media type <c>"application/vnd.example+json"</c>, this property is <c>false</c>.
/// </example>
public bool MatchesAllSubTypesWithoutSuffix =>
SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Gets whether the <see cref="MediaTypeHeaderValue"/> is readonly.
/// </summary>
public bool IsReadOnly
{
get { return _isReadOnly; }
@ -247,56 +390,14 @@ namespace Microsoft.Net.Http.Headers
}
// "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*".
if (!Type.Equals(otherMediaType.Type, comparisonType: StringComparison.OrdinalIgnoreCase))
{
if (!otherMediaType.MatchesAllTypes)
{
return false;
}
}
else if (!SubType.Equals(otherMediaType.SubType, comparisonType: StringComparison.OrdinalIgnoreCase))
{
if (!otherMediaType.MatchesAllSubTypes)
{
return false;
}
}
// "text/plain; charset=utf-8; level=1" is a subset of "text/plain; charset=utf-8". In turn
// "text/plain; charset=utf-8" is a subset of "text/plain".
if (otherMediaType._parameters != null && otherMediaType._parameters.Count != 0)
{
// Make sure all parameters in the potential superset are included locally. Fine to have additional
// parameters locally; they make this one more specific.
foreach (var parameter in otherMediaType._parameters)
{
if (parameter.Name.Equals("q", StringComparison.OrdinalIgnoreCase))
{
// "q" and later parameters are not involved in media type matching. Quoting the RFC: The first
// "q" parameter (if any) separates the media-range parameter(s) from the accept-params.
break;
}
var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name);
if (localParameter == null)
{
// Not found.
return false;
}
if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
return MatchesType(otherMediaType) &&
MatchesSubtype(otherMediaType) &&
MatchesParameters(otherMediaType);
}
/// <summary>
/// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,
/// while avoiding the cost of revalidating the components.
/// while avoiding the cost of re-validating the components.
/// </summary>
/// <returns>A deep copy.</returns>
public MediaTypeHeaderValue Copy()
@ -314,7 +415,7 @@ namespace Microsoft.Net.Http.Headers
/// <summary>
/// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components,
/// while avoiding the cost of revalidating the components. This copy is read-only.
/// while avoiding the cost of re-validating the components. This copy is read-only.
/// </summary>
/// <returns>A deep, read-only, copy.</returns>
public MediaTypeHeaderValue CopyAsReadOnly()
@ -362,33 +463,67 @@ namespace Microsoft.Net.Http.Headers
return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters);
}
/// <summary>
/// Takes a media type and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters.
/// </summary>
/// <param name="input">The <see cref="StringSegment"/> with the media type.</param>
/// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>
public static MediaTypeHeaderValue Parse(StringSegment input)
{
var index = 0;
return SingleValueParser.ParseValue(input, ref index);
}
/// <summary>
/// Takes a media type, which can include parameters, and parses it into the <see cref="MediaTypeHeaderValue" /> and its associated parameters.
/// </summary>
/// <param name="input">The <see cref="StringSegment"/> with the media type. The media type constructed here must not have an y</param>
/// <param name="parsedValue">The parsed <see cref="MediaTypeHeaderValue"/></param>
/// <returns>True if the value was successfully parsed.</returns>
public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue)
{
var index = 0;
return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>
public static IList<MediaTypeHeaderValue> ParseList(IList<string> inputs)
{
return MultipleValueParser.ParseValues(inputs);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// Throws if there is invalid data in a string.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <returns>The parsed <see cref="MediaTypeHeaderValue"/>.</returns>
public static IList<MediaTypeHeaderValue> ParseStrictList(IList<string> inputs)
{
return MultipleValueParser.ParseStrictValues(inputs);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param>
/// <returns>True if the value was successfully parsed.</returns>
public static bool TryParseList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseValues(inputs, out parsedValues);
}
/// <summary>
/// Takes an <see cref="IList{T}"/> of <see cref="string"/> and parses it into the <see cref="MediaTypeHeaderValue"></see> and its associated parameters.
/// </summary>
/// <param name="inputs">A list of media types</param>
/// <param name="parsedValues">The parsed <see cref="MediaTypeHeaderValue"/>.</param>
/// <returns>True if the value was successfully parsed.</returns>
public static bool TryParseStrictList(IList<string> inputs, out IList<MediaTypeHeaderValue> parsedValues)
{
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
@ -481,7 +616,7 @@ namespace Microsoft.Net.Http.Headers
}
else
{
mediaType = input.Substring(startIndex, typeLength) + "/" + input.Substring(current, subtypeLength);
mediaType = input.Substring(startIndex, typeLength) + ForwardSlashCharacter + input.Substring(current, subtypeLength);
}
return mediaTypeLength;
@ -502,5 +637,85 @@ namespace Microsoft.Net.Http.Headers
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType));
}
}
private bool MatchesType(MediaTypeHeaderValue set)
{
return set.MatchesAllTypes ||
set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase);
}
private bool MatchesSubtype(MediaTypeHeaderValue set)
{
if (set.MatchesAllSubTypes)
{
return true;
}
if (set.Suffix.HasValue)
{
if (Suffix.HasValue)
{
return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set);
}
else
{
return false;
}
}
else
{
return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase);
}
}
private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set)
{
return set.MatchesAllSubTypesWithoutSuffix ||
set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase);
}
private bool MatchesParameters(MediaTypeHeaderValue set)
{
if (set._parameters != null && set._parameters.Count != 0)
{
// Make sure all parameters in the potential superset are included locally. Fine to have additional
// parameters locally; they make this one more specific.
foreach (var parameter in set._parameters)
{
if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase))
{
// A parameter named "*" has no effect on media type matching, as it is only used as an indication
// that the entire media type string should be treated as a wildcard.
continue;
}
if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase))
{
// "q" and later parameters are not involved in media type matching. Quoting the RFC: The first
// "q" parameter (if any) separates the media-range parameter(s) from the accept-params.
break;
}
var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name);
if (localParameter == null)
{
// Not found.
return false;
}
if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return true;
}
private bool MatchesSubtypeSuffix(MediaTypeHeaderValue set)
{
// We don't have support for wildcards on suffixes alone (e.g., "application/entity+*")
// because there's no clear use case for it.
return set.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -28,11 +28,15 @@ namespace Microsoft.Net.Http.Headers
/// <remarks>
/// Performs comparisons based on the arguments' quality values
/// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0)
/// with the exception that subtype wildcards are considered less than specific media types and full
/// wildcards are considered less than subtype wildcards. This allows callers to sort a sequence of
/// <see cref="MediaTypeHeaderValue"/> following their q-values in the order of specific
/// media types, subtype wildcards, and last any full wildcards.
/// with the exception that suffixed subtype wildcards are considered less than subtype wildcards, subtype wildcards
/// are considered less than specific media types and full wildcards are considered less than
/// subtype wildcards. This allows callers to sort a sequence of <see cref="MediaTypeHeaderValue"/> following
/// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards.
/// </remarks>
/// <example>
/// If we had a list of media types (comma separated): { text/*;q=0.8, text/*+json;q=0.8, */*;q=1, */*;q=0.8, text/plain;q=0.8 }
/// Sorting them using Compare would return: { */*;q=0.8, text/*;q=0.8, text/*+json;q=0.8, text/plain;q=0.8, */*;q=1 }
/// </example>
public int Compare(MediaTypeHeaderValue mediaType1, MediaTypeHeaderValue mediaType2)
{
if (object.ReferenceEquals(mediaType1, mediaType2))
@ -62,6 +66,14 @@ namespace Microsoft.Net.Http.Headers
{
return 1;
}
else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return -1;
}
else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return 1;
}
}
else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase))
{
@ -73,6 +85,25 @@ namespace Microsoft.Net.Http.Headers
{
return 1;
}
else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return -1;
}
else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return 1;
}
}
else if (!mediaType1.Suffix.Equals(mediaType2.Suffix, StringComparison.OrdinalIgnoreCase))
{
if (mediaType1.MatchesAllSubTypesWithoutSuffix)
{
return -1;
}
else if (mediaType2.MatchesAllSubTypesWithoutSuffix)
{
return 1;
}
}
}

View File

@ -18,8 +18,10 @@ namespace Microsoft.Net.Http.Headers
{
"application/*",
"text/plain",
"text/*+json;q=0.8",
"text/plain;q=1.0",
"text/plain",
"text/*+json;q=0.6",
"text/plain;q=0",
"*/*;q=0.8",
"*/*;q=1",
@ -27,6 +29,7 @@ namespace Microsoft.Net.Http.Headers
"text/plain;q=0.8",
"text/*;q=0.8",
"text/*;q=0.6",
"text/*+json;q=0.4",
"text/*;q=1.0",
"*/*;q=0.4",
"text/plain;q=0.6",
@ -43,10 +46,13 @@ namespace Microsoft.Net.Http.Headers
"text/*;q=1.0",
"*/*;q=1",
"text/plain;q=0.8",
"text/*+json;q=0.8",
"text/*;q=0.8",
"*/*;q=0.8",
"text/plain;q=0.6",
"text/*+json;q=0.6",
"text/*;q=0.6",
"text/*+json;q=0.4",
"*/*;q=0.4",
"text/plain;q=0",
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.Net.Http.Headers
@ -39,6 +40,68 @@ namespace Microsoft.Net.Http.Headers
AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters
}
public static TheoryData<string, string, string> MediaTypesWithSuffixes =>
new TheoryData<string, string, string>
{
// See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec
{ "application/json", "json", null },
{ "application/json+", "json", "" },
{ "application/+json", "", "json" },
{ "application/entitytype+json", "entitytype", "json" },
{ "applica+tion/entitytype+json", "entitytype", "json" },
};
[Theory]
[MemberData(nameof(MediaTypesWithSuffixes))]
public void Ctor_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix)
{
var result = new MediaTypeHeaderValue(mediaType);
Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix?
Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix);
}
public static TheoryData<string, string, string> MediaTypesWithSuffixesAndSpaces =>
new TheoryData<string, string, string>
{
// See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec
{ " application / json+xml", "json", "xml" },
{ " application / vnd.com-pany.some+entity!.v2+js.#$&^_n ; q=\"0.3+1\"", "vnd.com-pany.some+entity!.v2", "js.#$&^_n"},
{ " application/ +json", "", "json" },
{ " application/ entitytype+json ", "entitytype", "json" },
{ " applica+tion/ entitytype+json ", "entitytype", "json" }
};
[Theory]
[MemberData(nameof(MediaTypesWithSuffixesAndSpaces))]
public void Parse_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix)
{
var result = MediaTypeHeaderValue.Parse(mediaType);
Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix?
Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix);
}
[Theory]
[InlineData("*/*", true)]
[InlineData("text/*", true)]
[InlineData("text/*+suffix", true)]
[InlineData("text/*+", true)]
[InlineData("text/*+*", true)]
[InlineData("text/json+suffix", false)]
[InlineData("*/json+*", false)]
public void MatchesAllSubTypesWithoutSuffix_ReturnsExpectedResult(string value, bool expectedReturnValue)
{
// Arrange
var mediaType = new MediaTypeHeaderValue(value);
// Act
var result = mediaType.MatchesAllSubTypesWithoutSuffix;
// Assert
Assert.Equal(expectedReturnValue, result);
}
[Fact]
public void Ctor_MediaTypeValidFormat_SuccessfullyCreated()
{
@ -644,6 +707,8 @@ namespace Microsoft.Net.Http.Headers
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")]
[InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")]
[InlineData("application/json;v=2", "application/json;*")]
[InlineData("application/json;v=2;charset=utf-8", "application/json;v=2;*")]
public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2)
{
// Arrange
@ -681,6 +746,74 @@ namespace Microsoft.Net.Http.Headers
Assert.False(isSubset);
}
[Theory]
[InlineData("application/entity+json", "application/entity+json")]
[InlineData("application/*+json", "application/entity+json")]
[InlineData("application/*+json", "application/*+json")]
[InlineData("application/*", "application/*+JSON")]
[InlineData("application/vnd.github+json", "application/vnd.github+json")]
[InlineData("application/*", "application/entity+JSON")]
[InlineData("*/*", "application/entity+json")]
public void IsSubsetOfWithSuffixes_PositiveCases(string set, string subset)
{
// Arrange
var setMediaType = MediaTypeHeaderValue.Parse(set);
var subSetMediaType = MediaTypeHeaderValue.Parse(subset);
// Act
var result = subSetMediaType.IsSubsetOf(setMediaType);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("application/entity+json", "application/entity+txt")]
[InlineData("application/entity+json", "application/entity.v2+json")]
[InlineData("application/*+json", "application/entity+txt")]
[InlineData("application/*+*", "application/json")]
[InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards
[InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards
public void IsSubSetOfWithSuffixes_NegativeCases(string set, string subset)
{
// Arrange
var setMediaType = MediaTypeHeaderValue.Parse(set);
var subSetMediaType = MediaTypeHeaderValue.Parse(subset);
// Act
var result = subSetMediaType.IsSubsetOf(setMediaType);
// Assert
Assert.False(result);
}
public static TheoryData<string, List<StringSegment>> MediaTypesWithFacets =>
new TheoryData<string, List<StringSegment>>
{
{ "application/vdn.github",
new List<StringSegment>(){ "vdn", "github" } },
{ "application/vdn.github+json",
new List<StringSegment>(){ "vdn", "github" } },
{ "application/vdn.github.v3+json",
new List<StringSegment>(){ "vdn", "github", "v3" } },
{ "application/vdn.github.+json",
new List<StringSegment>(){ "vdn", "github", "" } },
};
[Theory]
[MemberData(nameof(MediaTypesWithFacets))]
public void Facets_TestPositiveCases(string input, List<StringSegment> expected)
{
// Arrange
var mediaType = MediaTypeHeaderValue.Parse(input);
// Act
var result = mediaType.Facets;
// Assert
Assert.Equal(expected, result);
}
private void CheckValidParse(string input, MediaTypeHeaderValue expectedResult)
{
var result = MediaTypeHeaderValue.Parse(input);