Add structured syntax suffixes and facets to MediaTypeHeaderValue (#921)
This commit is contained in:
parent
ce68ec23c0
commit
e97e6546c2
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue