diff --git a/src/CloudNative.CloudEvents/MimeUtilities.cs b/src/CloudNative.CloudEvents/MimeUtilities.cs new file mode 100644 index 0000000..3b5fec1 --- /dev/null +++ b/src/CloudNative.CloudEvents/MimeUtilities.cs @@ -0,0 +1,65 @@ +// Copyright 2021 Cloud Native Foundation. +// Licensed under the Apache 2.0 license. +// See LICENSE file in the project root for full license information. + +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; + +namespace CloudNative.CloudEvents +{ + // TODO: Consider this name and namespace carefully. It really does need to be public, as all the event formatters are elsewhere. + // But it's not ideal... + + /// + /// Utility and extension methods around MIME. + /// + public static class MimeUtilities + { + // TODO: Should we return null, and force the caller to do the appropriate defaulting? + /// + /// Returns an encoding from a content type, defaulting to UTF-8. + /// + /// The content type, or null if no content type is known. + /// An encoding suitable for the charset specified in , + /// or UTF-8 if no charset has been specified. + public static Encoding GetEncoding(this ContentType contentType) => + contentType?.CharSet is string charSet ? Encoding.GetEncoding(charSet) : Encoding.UTF8; + + /// + /// Converts a into a . + /// + /// The header value to convert. May be null. + /// The converted content type, or null if is null. + public static ContentType ToContentType(this MediaTypeHeaderValue headerValue) => + headerValue is null ? null : new ContentType(headerValue.ToString()); + + /// + /// Converts a into a . + /// + /// The content type to convert. May be null. + /// The converted media type header value, or null if is null. + public static MediaTypeHeaderValue ToMediaTypeHeaderValue(this ContentType contentType) + { + if (contentType is null) + { + return null; + } + var header = new MediaTypeHeaderValue(contentType.MediaType); + foreach (string parameterName in contentType.Parameters.Keys) + { + header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName].ToString())); + } + return header; + } + + /// + /// Creates a from the given value, or returns null + /// if the input is null. + /// + /// The content type textual value. May be null. + /// The converted content type, or null if is null. + public static ContentType CreateContentTypeOrNull(string contentType) => + contentType is null ? null : new ContentType(contentType); + } +} diff --git a/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs b/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs new file mode 100644 index 0000000..1819a89 --- /dev/null +++ b/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs @@ -0,0 +1,82 @@ +// Copyright 2021 Cloud Native Foundation. +// Licensed under the Apache 2.0 license. +// See LICENSE file in the project root for full license information. + +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using Xunit; + +namespace CloudNative.CloudEvents.UnitTests.Http +{ + public class MimeUtilitiesTest + { + [Theory] + [InlineData("application/json")] + [InlineData("application/json; charset=iso-8859-1")] + [InlineData("application/json; charset=iso-8859-1; name=some-name")] + [InlineData("application/json; charset=iso-8859-1; name=some-name; x=y; a=b")] + [InlineData("application/json; charset=iso-8859-1; name=some-name; boundary=xyzzy; x=y")] + public void ContentTypeConversions(string text) + { + var originalContentType = new ContentType(text); + var header = originalContentType.ToMediaTypeHeaderValue(); + AssertEqualParts(text, header.ToString()); + var convertedContentType = header.ToContentType(); + AssertEqualParts(originalContentType.ToString(), convertedContentType.ToString()); + + // Conversions can end up reordering the parameters. In reality we're only + // likely to end up with a media type and charset, but our tests use more parameters. + // This just makes them deterministic. + void AssertEqualParts(string expected, string actual) + { + expected = string.Join(";", expected.Split(";").OrderBy(x => x)); + actual = string.Join(";", actual.Split(";").OrderBy(x => x)); + Assert.Equal(expected, actual); + } + } + + [Fact] + public void ContentTypeConversions_Null() + { + Assert.Null(default(ContentType).ToMediaTypeHeaderValue()); + Assert.Null(default(MediaTypeHeaderValue).ToContentType()); + } + + [Theory] + [InlineData("iso-8859-1")] + [InlineData("utf-8")] + public void ContentTypeGetEncoding(string charSet) + { + var contentType = new ContentType($"text/plain; charset={charSet}"); + Encoding encoding = contentType.GetEncoding(); + Assert.Equal(charSet, encoding.WebName); + } + + [Fact] + public void ContentTypeGetEncoding_NoContentType() + { + ContentType contentType = null; + Encoding encoding = contentType.GetEncoding(); + Assert.Equal(Encoding.UTF8, encoding); + } + + [Fact] + public void ContentTypeGetEncoding_NoCharSet() + { + ContentType contentType = new ContentType("text/plain"); + Encoding encoding = contentType.GetEncoding(); + Assert.Equal(Encoding.UTF8, encoding); + } + + [Theory] + [InlineData(null)] + [InlineData("text/plain")] + public void CreateContentTypeOrNull_WithContentType(string text) + { + ContentType ct = MimeUtilities.CreateContentTypeOrNull(text); + Assert.Equal(text, ct?.ToString()); + } + } +}