From cab31b02051c45a752f6a7fa71fb1d629073a9b1 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:53:10 -0400 Subject: [PATCH] Added JsonConverters. (#9) * Added JsonConverters. * Fix analyzer errors. * Fixed tests. --- .github/workflows/publish.yml | 6 + LightResults.Extensions.sln | 6 + README.md | 5 + docfx/docs/json.md | 13 ++ docfx/index.md | 4 + ...ults.Extensions.EntityFrameworkCore.csproj | 100 +++++----- ...esults.Extensions.ExceptionHandling.csproj | 86 +++++---- .../LightResults.Extensions.Json.csproj | 64 +++++++ .../ResultJsonConverter.cs | 177 +++++++++++++++++ .../ResultJsonConverterFactory.cs | 36 ++++ .../ResultJsonConverter`1.cs | 181 ++++++++++++++++++ .../LightResults.Extensions.Tests.csproj | 5 +- .../ResultJsonConverterTests.cs | 142 ++++++++++++++ 13 files changed, 736 insertions(+), 89 deletions(-) create mode 100644 docfx/docs/json.md create mode 100644 src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj create mode 100644 src/LightResults.Extensions.Json/ResultJsonConverter.cs create mode 100644 src/LightResults.Extensions.Json/ResultJsonConverterFactory.cs create mode 100644 src/LightResults.Extensions.Json/ResultJsonConverter`1.cs create mode 100644 tests/LightResults.Extensions.Tests/ResultJsonConverterTests.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e3d7f2f..dc5685e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,5 +50,11 @@ jobs: - name: Pack EntityFrameworkCore run: dotnet pack ./src/LightResults.Extensions.EntityFrameworkCore/LightResults.Extensions.EntityFrameworkCore.csproj --configuration Release --no-build --output . + - name: Build Json + run: dotnet build ./src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj --configuration Release --no-restore + + - name: Pack Json + run: dotnet pack ./src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj --configuration Release --no-build --output . + - name: Push to NuGet run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/LightResults.Extensions.sln b/LightResults.Extensions.sln index 27696f4..9ba7c85 100644 --- a/LightResults.Extensions.sln +++ b/LightResults.Extensions.sln @@ -8,6 +8,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A6C9FED0 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightResults.Extensions.EntityFrameworkCore", "src\LightResults.Extensions.EntityFrameworkCore\LightResults.Extensions.EntityFrameworkCore.csproj", "{9FEADBCA-FD94-437D-9C98-79C673A84C10}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightResults.Extensions.Json", "src\LightResults.Extensions.Json\LightResults.Extensions.Json.csproj", "{7628B10B-1D07-43BA-AC64-1DDF02A2A20F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {9FEADBCA-FD94-437D-9C98-79C673A84C10}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FEADBCA-FD94-437D-9C98-79C673A84C10}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FEADBCA-FD94-437D-9C98-79C673A84C10}.Release|Any CPU.Build.0 = Release|Any CPU + {7628B10B-1D07-43BA-AC64-1DDF02A2A20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7628B10B-1D07-43BA-AC64-1DDF02A2A20F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7628B10B-1D07-43BA-AC64-1DDF02A2A20F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7628B10B-1D07-43BA-AC64-1DDF02A2A20F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {93C8BC67-7F34-4935-A671-F0B12AEDB072} = {A6C9FED0-42B6-488C-8961-DE13F291434B} diff --git a/README.md b/README.md index 67eb6c0..9f88748 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ Extensions for [LightResults](https://github.com/jscarle/LightResults), an extre [![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.EntityFrameworkCore)](https://www.nuget.org/packages/LightResults.Extensions.EntityFrameworkCore) [![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.EntityFrameworkCore)](https://www.nuget.org/packages/LightResults.Extensions.EntityFrameworkCore) +- [Json](https://jscarle.github.io/LightResults.Extensions/docs/json.html) - Provides System.Text.Json converters. + + [![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) + [![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) + ## Documentation Make sure to [read the docs](https://jscarle.github.io/LightResults.Extensions/) for the full API. diff --git a/docfx/docs/json.md b/docfx/docs/json.md new file mode 100644 index 0000000..6abd99d --- /dev/null +++ b/docfx/docs/json.md @@ -0,0 +1,13 @@ +# Json + +Provides System.Text.Json converters for LightResults. + +[![main](https://img.shields.io/github/actions/workflow/status/jscarle/LightResults.Extensions/main.yml?logo=github)](https://github.com/jscarle/LightResults.Extensions) +[![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) +[![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) + +### Available converters + +- `ResultJsonConverter` +- `ResultJsonConverter` +- `ResultJsonConverterFactory` diff --git a/docfx/index.md b/docfx/index.md index 6f18769..ffd187b 100644 --- a/docfx/index.md +++ b/docfx/index.md @@ -18,3 +18,7 @@ Extensions for [LightResults](https://github.com/jscarle/LightResults), an extre [![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.EntityFrameworkCore)](https://www.nuget.org/packages/LightResults.Extensions.EntityFrameworkCore) [![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.EntityFrameworkCore)](https://www.nuget.org/packages/LightResults.Extensions.EntityFrameworkCore) +- [Json](https://jscarle.github.io/LightResults.Extensions/docs/json.html) - Provides System.Text.Json converters. + + [![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) + [![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) diff --git a/src/LightResults.Extensions.EntityFrameworkCore/LightResults.Extensions.EntityFrameworkCore.csproj b/src/LightResults.Extensions.EntityFrameworkCore/LightResults.Extensions.EntityFrameworkCore.csproj index 07dd545..4982d9b 100644 --- a/src/LightResults.Extensions.EntityFrameworkCore/LightResults.Extensions.EntityFrameworkCore.csproj +++ b/src/LightResults.Extensions.EntityFrameworkCore/LightResults.Extensions.EntityFrameworkCore.csproj @@ -1,61 +1,21 @@  + + LightResults.Extensions.EntityFrameworkCore net6.0;net7.0;net8.0 enable enable - LightResults.Extensions.EntityFrameworkCore latest - 8.0.0 - LightResults.Extensions.EntityFrameworkCore - Jean-Sebastien Carle - EntityFrameworkCore with LightResults. - Copyright © Jean-Sebastien Carle 2024 - LightResults.Extensions.EntityFrameworkCore - https://github.com/jscarle/LightResults.Extensions - LICENSE.md - README.md - Icon.png - https://github.com/jscarle/LightResults.Extensions - git - result results pattern lightresults entityframework entityframeworkcore - 8.0.0.0 - 8.0.0.0 - en-US - true - snupkg + + + + latest-All true - true - snupkg - true - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - - - - - - - - True - \ - False - - - True - \ - False - - - True - \ - False - - - + @@ -68,4 +28,50 @@ + + + + + + + + LightResults.Extensions.EntityFrameworkCore + 8.0.0 + 8.0.0.0 + 8.0.0.0 + en-US + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + true + LightResults.Extensions.EntityFrameworkCore + LightResults.Extensions.EntityFrameworkCore + EntityFrameworkCore with LightResults. + README.md + https://github.com/jscarle/LightResults.Extensions + result results pattern lightresults entityframework entityframeworkcore + https://github.com/jscarle/LightResults.Extensions + git + Jean-Sebastien Carle + Copyright © Jean-Sebastien Carle 2024 + LICENSE.md + Icon.png + true + snupkg + + + + + + + + + + + + + diff --git a/src/LightResults.Extensions.ExceptionHandling/LightResults.Extensions.ExceptionHandling.csproj b/src/LightResults.Extensions.ExceptionHandling/LightResults.Extensions.ExceptionHandling.csproj index 6c43a67..8191e9f 100644 --- a/src/LightResults.Extensions.ExceptionHandling/LightResults.Extensions.ExceptionHandling.csproj +++ b/src/LightResults.Extensions.ExceptionHandling/LightResults.Extensions.ExceptionHandling.csproj @@ -1,64 +1,70 @@  + + LightResults.Extensions.ExceptionHandling netstandard2.0;net6.0;net7.0;net8.0 enable enable - LightResults.Extensions.ExceptionHandling latest + + + + + latest-All + true + + + + + + + + + + + + + + + LightResults.Extensions.ExceptionHandling 8.0.2 + 8.0.2.0 + 8.0.2.0 + en-US + true + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + true + LightResults.Extensions.ExceptionHandling LightResults.Extensions.ExceptionHandling - Jean-Sebastien Carle Exception handling for LightResults. - Copyright © Jean-Sebastien Carle 2024 - LightResults.Extensions.ExceptionHandling - https://github.com/jscarle/LightResults.Extensions - LICENSE.md README.md - Icon.png + https://github.com/jscarle/LightResults.Extensions + result results pattern lightresults exception handling https://github.com/jscarle/LightResults.Extensions git - result results pattern lightresults exception handling - 8.0.2.0 - 8.0.2.0 - en-US - true - snupkg - latest-All - true + Jean-Sebastien Carle + Copyright © Jean-Sebastien Carle 2024 + LICENSE.md + Icon.png true snupkg - true - true - bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - - - - + + + + - - - - True - \ - False - - - True - \ - False - - - True - \ - False - + diff --git a/src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj b/src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj new file mode 100644 index 0000000..a3782e0 --- /dev/null +++ b/src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj @@ -0,0 +1,64 @@ + + + + + LightResults.Extensions.Json + net6.0;net7.0;net8.0 + enable + enable + latest + + + + + latest-All + true + + + + + + + + + + + 8.0.0 + 8.0.0.0 + 8.0.0.0 + en-US + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + + + true + LightResults.Extensions.Json + LightResults.Extensions.Json + System.Text.Json converters for LightResults. + README.md + https://github.com/jscarle/LightResults.Extensions + result results pattern lightresults system-text-json json-converter + https://github.com/jscarle/LightResults.Extensions + git + Jean-Sebastien Carle + Copyright © Jean-Sebastien Carle 2024 + LICENSE.md + Icon.png + true + snupkg + + + + + + + + + + + + + + diff --git a/src/LightResults.Extensions.Json/ResultJsonConverter.cs b/src/LightResults.Extensions.Json/ResultJsonConverter.cs new file mode 100644 index 0000000..004a23f --- /dev/null +++ b/src/LightResults.Extensions.Json/ResultJsonConverter.cs @@ -0,0 +1,177 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LightResults.Extensions.Json; + +/// Converts a to JSON. +/// This converter only supports serialization. +/// +[SuppressMessage("Design", "CA1062: Validate arguments of public methods", Justification = "Arguments are provided by the SDK.")] +public sealed class ResultJsonConverter : JsonConverter +{ + private const string TypeDiscriminator = "$type"; + private const string IsSuccess = "IsSuccess"; + private const string Errors = "Errors"; + private const string Message = "Message"; + private const string Metadata = "Metadata"; + private const string MetadataValue = "Value"; + private const string ExceptionMessage = "Message"; + private const string ExceptionStackTrace = "StackTrace"; + private const string ExceptionInnerException = "InnerException"; + + /// Reads and converts JSON to a object. + /// The JSON reader. + /// The type of the object to convert. + /// The serialization options to use. + /// The deserialized object. + /// Thrown when the method is called as it's not implemented. + public override Result Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + /// Writes a object to JSON. + /// The JSON writer. + /// The object to write. + /// The serialization options to use. + public override void Write(Utf8JsonWriter writer, Result value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteBoolean(IsSuccess, value.IsSuccess); + if (!value.IsSuccess) + WriteErrors(writer, value); + writer.WriteEndObject(); + } + + private static void WriteErrors(Utf8JsonWriter writer, Result result) + { + writer.WritePropertyName(Errors); + writer.WriteStartArray(); + foreach (var error in result.Errors) + WriteError(writer, error); + writer.WriteEndArray(); + } + + private static void WriteError(Utf8JsonWriter writer, IError error) + { + writer.WriteStartObject(); + writer.WriteString(TypeDiscriminator, error.GetType().FullName ?? error.GetType().Name); + writer.WriteString(Message, error.Message); + if (error.Metadata.Count > 0) + WriteMetadata(writer, error); + writer.WriteEndObject(); + } + + private static void WriteMetadata(Utf8JsonWriter writer, IError error) + { + writer.WritePropertyName(Metadata); + writer.WriteStartObject(); + + var keys = error.Metadata.Keys.OrderBy(x => x, StringComparer.InvariantCulture); + foreach (var key in keys) + WriteMetadataItem(writer, key, error.Metadata[key]); + writer.WriteEndObject(); + } + + private static void WriteMetadataItem(Utf8JsonWriter writer, string key, object obj) + { + if (obj is Exception ex) + { + writer.WritePropertyName(key); + WriteExceptionValue(writer, ex); + return; + } + + writer.WritePropertyName(key); + writer.WriteStartObject(); + writer.WriteString(TypeDiscriminator, obj.GetType().FullName ?? obj.GetType().Name); + WriteObject(writer, MetadataValue, obj); + writer.WriteEndObject(); + } + + private static void WriteObject(Utf8JsonWriter writer, string name, object? obj) + { + if (obj is null) + { + writer.WriteNull(name); + return; + } + + switch (obj) + { + case bool value: + writer.WriteBoolean(name, value); + break; + case byte value: + writer.WriteNumber(name, value); + break; + case DateTime value: + writer.WriteString(name, value); + break; + case DateTimeOffset value: + writer.WriteString(name, value); + break; + case DateOnly value: + writer.WritePropertyName(name); + writer.WriteStringValue(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + break; + case TimeOnly value: + writer.WritePropertyName(name); + writer.WriteStringValue(value.ToString("HH:mm:ss", CultureInfo.InvariantCulture)); + break; + case TimeSpan value: + writer.WritePropertyName(name); + writer.WriteNumberValue(value.Ticks); + break; + case double value: + writer.WriteNumber(name, value); + break; + case Guid value: + writer.WriteString(name, value); + break; + case short value: + writer.WriteNumber(name, value); + break; + case int value: + writer.WriteNumber(name, value); + break; + case long value: + writer.WriteNumber(name, value); + break; + case sbyte value: + writer.WriteNumber(name, value); + break; + case float value: + writer.WriteNumber(name, value); + break; + case string value: + writer.WriteString(name, value); + break; + case ushort value: + writer.WriteNumber(name, value); + break; + case uint value: + writer.WriteNumber(name, value); + break; + case ulong value: + writer.WriteNumber(name, value); + break; + } + } + + private static void WriteExceptionValue(Utf8JsonWriter writer, Exception ex) + { + writer.WriteStartObject(); + writer.WriteString(TypeDiscriminator, ex.GetType().FullName ?? ex.GetType().Name); + writer.WriteString(ExceptionMessage, ex.Message); + writer.WriteString(ExceptionStackTrace, ex.StackTrace); + if (ex.InnerException is not null) + { + writer.WritePropertyName(ExceptionInnerException); + WriteExceptionValue(writer, ex.InnerException); + } + writer.WriteEndObject(); + } +} diff --git a/src/LightResults.Extensions.Json/ResultJsonConverterFactory.cs b/src/LightResults.Extensions.Json/ResultJsonConverterFactory.cs new file mode 100644 index 0000000..bfd5703 --- /dev/null +++ b/src/LightResults.Extensions.Json/ResultJsonConverterFactory.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LightResults.Extensions.Json; + +/// JsonConverterFactory for converting Result types to and from JSON. +public class ResultJsonConverterFactory : JsonConverterFactory +{ + /// + public override bool CanConvert(Type? typeToConvert) + { + if (typeToConvert is null) + return false; + + return (typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Result<>)) || typeToConvert == typeof(Result); + } + + /// + public override JsonConverter? CreateConverter(Type? typeToConvert, JsonSerializerOptions options) + { + if (typeToConvert is null) + return null; + + if (typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(Result<>)) + { + var valueType = typeToConvert.GetGenericArguments()[0]; + var converterType = typeof(ResultJsonConverter<>).MakeGenericType(valueType); + return (JsonConverter?)Activator.CreateInstance(converterType); + } + else + { + var converterType = typeof(ResultJsonConverter); + return (JsonConverter?)Activator.CreateInstance(converterType); + } + } +} diff --git a/src/LightResults.Extensions.Json/ResultJsonConverter`1.cs b/src/LightResults.Extensions.Json/ResultJsonConverter`1.cs new file mode 100644 index 0000000..962b388 --- /dev/null +++ b/src/LightResults.Extensions.Json/ResultJsonConverter`1.cs @@ -0,0 +1,181 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LightResults.Extensions.Json; + +/// Converts a to JSON. +/// This converter only supports serialization. +/// The type of value of the result. +/// +[SuppressMessage("Design", "CA1062: Validate arguments of public methods", Justification = "Arguments are provided by the SDK.")] +public sealed class ResultJsonConverter : JsonConverter> +{ + private const string TypeDiscriminator = "$type"; + private const string IsSuccess = "IsSuccess"; + private const string Value = "Value"; + private const string Errors = "Errors"; + private const string Message = "Message"; + private const string Metadata = "Metadata"; + private const string MetadataValue = "Value"; + private const string ExceptionMessage = "Message"; + private const string ExceptionStackTrace = "StackTrace"; + private const string ExceptionInnerException = "InnerException"; + + /// Reads and converts JSON to a object. + /// The JSON reader. + /// The type of the object to convert. + /// The serialization options to use. + /// The deserialized object. + /// Thrown when the method is called as it's not implemented. + public override Result Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + /// Writes a object to JSON. + /// The JSON writer. + /// The object to write. + /// The serialization options to use. + public override void Write(Utf8JsonWriter writer, Result value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteBoolean(IsSuccess, value.IsSuccess); + if (value.IsSuccess) + WriteObject(writer, Value, value.Value); + else + WriteErrors(writer, value); + writer.WriteEndObject(); + } + + private static void WriteErrors(Utf8JsonWriter writer, Result result) + { + writer.WritePropertyName(Errors); + writer.WriteStartArray(); + foreach (var error in result.Errors) + WriteError(writer, error); + writer.WriteEndArray(); + } + + private static void WriteError(Utf8JsonWriter writer, IError error) + { + writer.WriteStartObject(); + writer.WriteString(TypeDiscriminator, error.GetType().FullName ?? error.GetType().Name); + writer.WriteString(Message, error.Message); + if (error.Metadata.Count > 0) + WriteMetadata(writer, error); + writer.WriteEndObject(); + } + + private static void WriteMetadata(Utf8JsonWriter writer, IError error) + { + writer.WritePropertyName(Metadata); + writer.WriteStartObject(); + + var keys = error.Metadata.Keys.OrderBy(x => x, StringComparer.InvariantCulture); + foreach (var key in keys) + WriteMetadataItem(writer, key, error.Metadata[key]); + writer.WriteEndObject(); + } + + private static void WriteMetadataItem(Utf8JsonWriter writer, string key, object obj) + { + if (obj is Exception ex) + { + writer.WritePropertyName(key); + WriteExceptionValue(writer, ex); + return; + } + + writer.WritePropertyName(key); + writer.WriteStartObject(); + writer.WriteString(TypeDiscriminator, obj.GetType().FullName ?? obj.GetType().Name); + WriteObject(writer, MetadataValue, obj); + writer.WriteEndObject(); + } + + private static void WriteObject(Utf8JsonWriter writer, string name, object? obj) + { + if (obj is null) + { + writer.WriteNull(name); + return; + } + + switch (obj) + { + case bool value: + writer.WriteBoolean(name, value); + break; + case byte value: + writer.WriteNumber(name, value); + break; + case DateTime value: + writer.WriteString(name, value); + break; + case DateTimeOffset value: + writer.WriteString(name, value); + break; + case DateOnly value: + writer.WritePropertyName(name); + writer.WriteStringValue(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + break; + case TimeOnly value: + writer.WritePropertyName(name); + writer.WriteStringValue(value.ToString("HH:mm:ss", CultureInfo.InvariantCulture)); + break; + case TimeSpan value: + writer.WritePropertyName(name); + writer.WriteNumberValue(value.Ticks); + break; + case double value: + writer.WriteNumber(name, value); + break; + case Guid value: + writer.WriteString(name, value); + break; + case short value: + writer.WriteNumber(name, value); + break; + case int value: + writer.WriteNumber(name, value); + break; + case long value: + writer.WriteNumber(name, value); + break; + case sbyte value: + writer.WriteNumber(name, value); + break; + case float value: + writer.WriteNumber(name, value); + break; + case string value: + writer.WriteString(name, value); + break; + case ushort value: + writer.WriteNumber(name, value); + break; + case uint value: + writer.WriteNumber(name, value); + break; + case ulong value: + writer.WriteNumber(name, value); + break; + } + } + + private static void WriteExceptionValue(Utf8JsonWriter writer, Exception ex) + { + writer.WriteStartObject(); + writer.WriteString(TypeDiscriminator, ex.GetType().FullName ?? ex.GetType().Name); + writer.WriteString(ExceptionMessage, ex.Message); + writer.WriteString(ExceptionStackTrace, ex.StackTrace); + if (ex.InnerException is not null) + { + writer.WritePropertyName(ExceptionInnerException); + WriteExceptionValue(writer, ex.InnerException); + } + writer.WriteEndObject(); + } +} diff --git a/tests/LightResults.Extensions.Tests/LightResults.Extensions.Tests.csproj b/tests/LightResults.Extensions.Tests/LightResults.Extensions.Tests.csproj index 0aa1dcc..739428b 100644 --- a/tests/LightResults.Extensions.Tests/LightResults.Extensions.Tests.csproj +++ b/tests/LightResults.Extensions.Tests/LightResults.Extensions.Tests.csproj @@ -1,7 +1,7 @@ - net481;net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0 enable enable latest @@ -11,7 +11,8 @@ - + + diff --git a/tests/LightResults.Extensions.Tests/ResultJsonConverterTests.cs b/tests/LightResults.Extensions.Tests/ResultJsonConverterTests.cs new file mode 100644 index 0000000..7041cfd --- /dev/null +++ b/tests/LightResults.Extensions.Tests/ResultJsonConverterTests.cs @@ -0,0 +1,142 @@ +using System.Text.Json; +using FluentAssertions; +using LightResults.Extensions.Json; +using Xunit; + +namespace LightResults.Extensions.Tests; + +public sealed class ResultJsonConverterTests +{ + private static readonly JsonSerializerOptions Options = new() { Converters = { new ResultJsonConverterFactory() } }; + + [Fact] + public void SuccessResult() + { + // Arrange + var result = Result.Ok(); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should().Be("{\"IsSuccess\":true}"); + } + + [Fact] + public void SuccessWithValueResult() + { + // Arrange + var result = Result.Ok(42); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should().Be("{\"IsSuccess\":true,\"Value\":42}"); + } + + [Fact] + public void FailedResultWithSingleError() + { + // Arrange + const string errorMessage = "Sample error message"; + var result = Result.Fail(errorMessage); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should().Be("{\"IsSuccess\":false,\"Errors\":[{\"$type\":\"LightResults.Error\",\"Message\":\"Sample error message\"}]}"); + } + + [Fact] + public void FailedResultWithSingleErrorAndMetadata() + { + // Arrange + const string errorMessage = "Sample error message"; + IDictionary metadata = new Dictionary { { "Key", 0 } }; + var result = Result.Fail(errorMessage, metadata); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should() + .Be( + "{\"IsSuccess\":false,\"Errors\":[{\"$type\":\"LightResults.Error\",\"Message\":\"Sample error message\",\"Metadata\":{\"Key\":{\"$type\":\"System.Int32\",\"Value\":0}}}]}" + ); + } + + [Fact] + public void FailedResultWithSingleErrorAndMetadataWithException() + { + // Arrange + const string errorMessage = "Sample error message"; + var exception = new InvalidOperationException(); + IDictionary metadata = new Dictionary { { "Exception", exception } }; + var result = Result.Fail(errorMessage, metadata); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should() + .Be( + "{\"IsSuccess\":false,\"Errors\":[{\"$type\":\"LightResults.Error\",\"Message\":\"Sample error message\",\"Metadata\":{\"Exception\":{\"$type\":\"System.InvalidOperationException\",\"Message\":\"Operation is not valid due to the current state of the object.\",\"StackTrace\":null}}}]}" + ); + } + + [Fact] + public void FailedResultWithSingleErrorAndMetadataWithExceptionAndInnerException() + { + // Arrange + const string errorMessage = "Sample error message"; + var exception = new InvalidProgramException("Invalid program!", new InvalidOperationException()); + IDictionary metadata = new Dictionary { { "Exception", exception } }; + var result = Result.Fail(errorMessage, metadata); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should() + .Be( + "{\"IsSuccess\":false,\"Errors\":[{\"$type\":\"LightResults.Error\",\"Message\":\"Sample error message\",\"Metadata\":{\"Exception\":{\"$type\":\"System.InvalidProgramException\",\"Message\":\"Invalid program!\",\"StackTrace\":null,\"InnerException\":{\"$type\":\"System.InvalidOperationException\",\"Message\":\"Operation is not valid due to the current state of the object.\",\"StackTrace\":null}}}}]}" + ); + } + + [Fact] + public void FailedResultWithSingleErrorAndMultipleMetadata() + { + // Arrange + const string errorMessage = "Sample error message"; + IDictionary metadata = new Dictionary { { "Key", 0 }, { "OtherKey", 1 } }; + var result = Result.Fail(errorMessage, metadata); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should() + .Be( + "{\"IsSuccess\":false,\"Errors\":[{\"$type\":\"LightResults.Error\",\"Message\":\"Sample error message\",\"Metadata\":{\"Key\":{\"$type\":\"System.Int32\",\"Value\":0},\"OtherKey\":{\"$type\":\"System.Int32\",\"Value\":1}}}]}" + ); + } + + [Fact] + public void FailedResultWithMultipleErrors() + { + // Arrange + var errors = new List { new Error("Error 1"), new Error("Error 2") }; + var result = Result.Fail(errors); + + // Act + var json = JsonSerializer.Serialize(result, Options); + + // Assert + json.Should() + .Be( + "{\"IsSuccess\":false,\"Errors\":[{\"$type\":\"LightResults.Error\",\"Message\":\"Error 1\"},{\"$type\":\"LightResults.Error\",\"Message\":\"Error 2\"}]}" + ); + } +}