diff --git a/JSONAPI.Tests/Data/ErrorSerializerTest.json b/JSONAPI.Tests/Data/ErrorSerializerTest.json new file mode 100644 index 00000000..e64ce626 --- /dev/null +++ b/JSONAPI.Tests/Data/ErrorSerializerTest.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "id": "TEST-ERROR-ID", + "status": "500", + "title": "System.Exception", + "detail": "This is the exception message!", + "inner": null, + "stackTrace": "Stack trace would go here" + } + ] +} \ No newline at end of file diff --git a/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json b/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json new file mode 100644 index 00000000..f5439ca2 --- /dev/null +++ b/JSONAPI.Tests/Data/FormatterErrorSerializationTest.json @@ -0,0 +1 @@ +{"test":"foo"} \ No newline at end of file diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index fa20d111..b6265c16 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -38,6 +38,12 @@ false + + ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.dll + + + ..\packages\FluentAssertions.3.2.2\lib\net45\FluentAssertions.Core.dll + False @@ -60,6 +66,8 @@ False ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.2\lib\net45\System.Web.Http.WebHost.dll + + @@ -68,7 +76,9 @@ + + @@ -82,6 +92,12 @@ + + Always + + + Always + Always diff --git a/JSONAPI.Tests/Json/ErrorSerializerTests.cs b/JSONAPI.Tests/Json/ErrorSerializerTests.cs new file mode 100644 index 00000000..9a1a60e5 --- /dev/null +++ b/JSONAPI.Tests/Json/ErrorSerializerTests.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Web.Http; +using FluentAssertions; +using JSONAPI.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace JSONAPI.Tests.Json +{ + [TestClass] + public class ErrorSerializerTests + { + private class TestErrorIdProvider : IErrorIdProvider + { + public string GenerateId(HttpError error) + { + return "TEST-ERROR-ID"; + } + } + + [TestMethod] + public void CanSerialize_returns_true_for_HttpError() + { + var serializer = new ErrorSerializer(); + var result = serializer.CanSerialize(typeof (HttpError)); + result.Should().BeTrue(); + } + + [TestMethod] + public void CanSerialize_returns_false_for_Exception() + { + var serializer = new ErrorSerializer(); + var result = serializer.CanSerialize(typeof(Exception)); + result.Should().BeFalse(); + } + + [TestMethod] + [DeploymentItem(@"Data\ErrorSerializerTest.json")] + public void SerializeError_serializes_httperror() + { + using (var stream = new MemoryStream()) + { + var textWriter = new StreamWriter(stream); + var writer = new JsonTextWriter(textWriter); + var error = new HttpError(new Exception("This is the exception message!"), true) + { + StackTrace = "Stack trace would go here" + }; + var jsonSerializer = new JsonSerializer(); + + var serializer = new ErrorSerializer(new TestErrorIdProvider()); + serializer.SerializeError(error, stream, writer, jsonSerializer); + + writer.Flush(); + + var expectedJson = File.ReadAllText("ErrorSerializerTest.json"); + var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); + var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + output.Should().Be(minifiedExpectedJson); + } + } + } +} diff --git a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs index 1834171c..1a572d10 100644 --- a/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs +++ b/JSONAPI.Tests/Json/JsonApiMediaFormaterTests.cs @@ -1,5 +1,8 @@ using System; using System.Linq; +using System.Text.RegularExpressions; +using System.Web.Http; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using JSONAPI.Tests.Models; using Newtonsoft.Json; @@ -17,6 +20,22 @@ public class JsonApiMediaFormaterTests Author a; Post p, p2, p3, p4; + private class MockErrorSerializer : IErrorSerializer + { + public bool CanSerialize(Type type) + { + return true; + } + + public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName("test"); + serializer.Serialize(writer, "foo"); + writer.WriteEndObject(); + } + } + [TestInitialize] public void SetupModels() { @@ -155,6 +174,52 @@ public void SerializeArrayIntegrationTest() //Assert.AreEqual("[2,3,4]", sw.ToString()); } + [TestMethod] + [DeploymentItem(@"Data\FormatterErrorSerializationTest.json")] + public void Should_serialize_error() + { + // Arrange + var formatter = new JSONAPI.Json.JsonApiFormatter(new MockErrorSerializer()); + formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + var stream = new MemoryStream(); + + // Act + var payload = new HttpError(new Exception(), true); + formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + var expectedJson = File.ReadAllText("FormatterErrorSerializationTest.json"); + var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); + var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + output.Should().Be(minifiedExpectedJson); + } + + [TestMethod] + [DeploymentItem(@"Data\ErrorSerializerTest.json")] + public void SerializeErrorIntegrationTest() + { + // Arrange + JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter(); + formatter.PluralizationService = new JSONAPI.Core.PluralizationService(); + MemoryStream stream = new MemoryStream(); + + // Act + var payload = new HttpError(new Exception("This is the exception message!"), true) + { + StackTrace = "Stack trace would go here" + }; + formatter.WriteToStreamAsync(typeof(HttpError), payload, stream, (System.Net.Http.HttpContent)null, (System.Net.TransportContext)null); + + // Assert + var expectedJson = File.ReadAllText("ErrorSerializerTest.json"); + var minifiedExpectedJson = JsonHelpers.MinifyJson(expectedJson); + var output = System.Text.Encoding.ASCII.GetString(stream.ToArray()); + output = Regex.Replace(output, + @"[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}", + "TEST-ERROR-ID"); // We don't know what the GUID will be, so replace it + output.Should().Be(minifiedExpectedJson); + } + [TestMethod] public void DeserializeCollectionIntegrationTest() { diff --git a/JSONAPI.Tests/Json/JsonHelpers.cs b/JSONAPI.Tests/Json/JsonHelpers.cs new file mode 100644 index 00000000..440e2e1c --- /dev/null +++ b/JSONAPI.Tests/Json/JsonHelpers.cs @@ -0,0 +1,13 @@ +using System.Text.RegularExpressions; + +namespace JSONAPI.Tests.Json +{ + static class JsonHelpers + { + // http://stackoverflow.com/questions/8913138/minify-indented-json-string-in-net + public static string MinifyJson(string input) + { + return Regex.Replace(input, "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1"); + } + } +} diff --git a/JSONAPI.Tests/packages.config b/JSONAPI.Tests/packages.config index 22761302..21995fcd 100644 --- a/JSONAPI.Tests/packages.config +++ b/JSONAPI.Tests/packages.config @@ -1,5 +1,6 @@  + diff --git a/JSONAPI/JSONAPI.csproj b/JSONAPI/JSONAPI.csproj index 06d28f1b..b7e05b55 100644 --- a/JSONAPI/JSONAPI.csproj +++ b/JSONAPI/JSONAPI.csproj @@ -83,6 +83,10 @@ + + + + diff --git a/JSONAPI/Json/ErrorSerializer.cs b/JSONAPI/Json/ErrorSerializer.cs new file mode 100644 index 00000000..3451193a --- /dev/null +++ b/JSONAPI/Json/ErrorSerializer.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Web.Http; +using Newtonsoft.Json; + +namespace JSONAPI.Json +{ + internal class ErrorSerializer : IErrorSerializer + { + private class JsonApiError + { + [JsonProperty(PropertyName = "id")] + public string Id { get; set; } + + [JsonProperty(PropertyName = "status")] + public string Status { get; set; } + + [JsonProperty(PropertyName = "title")] + public string Title { get; set; } + + [JsonProperty(PropertyName = "detail")] + public string Detail { get; set; } + + [JsonProperty(PropertyName = "inner")] + public JsonApiError Inner { get; set; } + + [JsonProperty(PropertyName = "stackTrace")] + public string StackTrace { get; set; } + + public JsonApiError(HttpError error) + { + Title = error.ExceptionType ?? error.Message; + Status = "500"; + Detail = error.ExceptionMessage ?? error.MessageDetail; + StackTrace = error.StackTrace; + + if (error.InnerException != null) + Inner = new JsonApiError(error.InnerException); + } + } + + private readonly IErrorIdProvider _errorIdProvider; + + public ErrorSerializer() + : this(new GuidErrorIdProvider()) + { + + } + + public ErrorSerializer(IErrorIdProvider errorIdProvider) + { + _errorIdProvider = errorIdProvider; + } + + public bool CanSerialize(Type type) + { + return type == typeof (HttpError); + } + + public void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer) + { + var httpError = error as HttpError; + if (httpError == null) throw new Exception("Unsupported error type."); + + writer.WriteStartObject(); + writer.WritePropertyName("errors"); + + var jsonApiError = new JsonApiError(httpError) + { + Id = _errorIdProvider.GenerateId(httpError) + }; + serializer.Serialize(writer, new[] { jsonApiError }); + + writer.WriteEndObject(); + } + } +} diff --git a/JSONAPI/Json/GuidErrorIdProvider.cs b/JSONAPI/Json/GuidErrorIdProvider.cs new file mode 100644 index 00000000..5205d478 --- /dev/null +++ b/JSONAPI/Json/GuidErrorIdProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Web.Http; + +namespace JSONAPI.Json +{ + internal class GuidErrorIdProvider : IErrorIdProvider + { + public string GenerateId(HttpError error) + { + return Guid.NewGuid().ToString(); + } + } +} diff --git a/JSONAPI/Json/IErrorIdProvider.cs b/JSONAPI/Json/IErrorIdProvider.cs new file mode 100644 index 00000000..206cf5c7 --- /dev/null +++ b/JSONAPI/Json/IErrorIdProvider.cs @@ -0,0 +1,9 @@ +using System.Web.Http; + +namespace JSONAPI.Json +{ + internal interface IErrorIdProvider + { + string GenerateId(HttpError error); + } +} diff --git a/JSONAPI/Json/IErrorSerializer.cs b/JSONAPI/Json/IErrorSerializer.cs new file mode 100644 index 00000000..886c832c --- /dev/null +++ b/JSONAPI/Json/IErrorSerializer.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using System; +using System.IO; + +namespace JSONAPI.Json +{ + internal interface IErrorSerializer + { + bool CanSerialize(Type type); + void SerializeError(object error, Stream writeStream, JsonWriter writer, JsonSerializer serializer); + } +} diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index 770e74f9..e8116f2f 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -20,11 +20,18 @@ namespace JSONAPI.Json public class JsonApiFormatter : JsonMediaTypeFormatter { public JsonApiFormatter() + : this(new ErrorSerializer()) { + } + + internal JsonApiFormatter(IErrorSerializer errorSerializer) + { + _errorSerializer = errorSerializer; SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.api+json")); } public IPluralizationService PluralizationService { get; set; } + private readonly IErrorSerializer _errorSerializer; private Lazy> _relationAggregators = new Lazy>( @@ -79,32 +86,40 @@ public override Task WriteToStreamAsync(System.Type type, object value, Stream w this.RelationAggregators[writeStream] = aggregator; } } - - Type valtype = GetSingleType(value.GetType()); - if (IsMany(value.GetType())) - aggregator.AddPrimary(valtype, (IEnumerable)value); - else - aggregator.AddPrimary(valtype, value); - + var contentHeaders = content == null ? null : content.Headers; var effectiveEncoding = SelectCharacterEncoding(contentHeaders); JsonWriter writer = this.CreateJsonWriter(typeof(object), writeStream, effectiveEncoding); JsonSerializer serializer = this.CreateJsonSerializer(); - //writer.Formatting = Formatting.Indented; + if (_errorSerializer.CanSerialize(type)) + { + // `value` is an error + _errorSerializer.SerializeError(value, writeStream, writer, serializer); + } + else + { + Type valtype = GetSingleType(value.GetType()); + if (IsMany(value.GetType())) + aggregator.AddPrimary(valtype, (IEnumerable) value); + else + aggregator.AddPrimary(valtype, value); - var root = GetPropertyName(type, value); + //writer.Formatting = Formatting.Indented; - writer.WriteStartObject(); - writer.WritePropertyName(root); - if (IsMany(value.GetType())) - this.SerializeMany(value, writeStream, writer, serializer, aggregator); - else - this.Serialize(value, writeStream, writer, serializer, aggregator); + var root = GetPropertyName(type, value); - // Include links from aggregator - SerializeLinkedResources(writeStream, writer, serializer, aggregator); + writer.WriteStartObject(); + writer.WritePropertyName(root); + if (IsMany(value.GetType())) + this.SerializeMany(value, writeStream, writer, serializer, aggregator); + else + this.Serialize(value, writeStream, writer, serializer, aggregator); - writer.WriteEndObject(); + // Include links from aggregator + SerializeLinkedResources(writeStream, writer, serializer, aggregator); + + writer.WriteEndObject(); + } writer.Flush(); lock (this.RelationAggregators) diff --git a/JSONAPI/Properties/AssemblyInfo.cs b/JSONAPI/Properties/AssemblyInfo.cs index ba88548a..7009c211 100644 --- a/JSONAPI/Properties/AssemblyInfo.cs +++ b/JSONAPI/Properties/AssemblyInfo.cs @@ -22,6 +22,8 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("5b46482f-733f-42bf-b507-37767a6bb948")] +[assembly: InternalsVisibleTo("JSONAPI.Tests")] + // Version information for an assembly consists of the following four values: // // Major Version