diff --git a/CSharpFunctionalExtensions.Tests/ResultTests/Json/Serialization/HttpResponseMessageJsonExtensionsTests.cs b/CSharpFunctionalExtensions.Tests/ResultTests/Json/Serialization/HttpResponseMessageJsonExtensionsTests.cs index fef4e40c..df469458 100644 --- a/CSharpFunctionalExtensions.Tests/ResultTests/Json/Serialization/HttpResponseMessageJsonExtensionsTests.cs +++ b/CSharpFunctionalExtensions.Tests/ResultTests/Json/Serialization/HttpResponseMessageJsonExtensionsTests.cs @@ -39,6 +39,19 @@ public async Task ReadResultAsyncOfT_NullHttpResponseMessage_Failure() result.IsSuccess.Should().BeFalse(); } + [Fact] + public async Task ReadUnitResultAsyncOfT_NullHttpResponseMessage_Failure() + { + // Assign + HttpResponseMessage httpResponseMessage = null; + + // Act + var result = await httpResponseMessage.ReadUnitResultAsync(); + + // Assert + result.IsSuccess.Should().BeFalse(); + } + [Fact] public async Task ReadResultAsync_NullHttpResponseMessageContent_Failure() { @@ -67,6 +80,19 @@ public async Task ReadResultAsyncOfT_NullHttpResponseMessageContent_Failure() result.Error.Should().Be(DtoMessages.ContentJsonNotResult); } + [Fact] + public async Task ReadUnitResultAsyncOfT_NullHttpResponseMessageContent_Failure() + { + // Assign + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + + // Act + var result = await httpResponseMessage.ReadUnitResultAsync(); + + // Assert + result.IsSuccess.Should().BeFalse(); + } + [Fact] public async Task ReadResultAsync_EmptyResponseMessageContent_Failure() { @@ -97,6 +123,20 @@ public async Task ReadResultAsyncOfT_EmptyResponseMessageContent_Failure() result.Error.Should().Be(DtoMessages.ContentJsonNotResult); } + [Fact] + public async Task ReadUnitResultAsyncOfT_EmptyResponseMessageContent_Failure() + { + // Assign + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + httpResponseMessage.Content = new StringContent(string.Empty); + + // Act + var result = await httpResponseMessage.ReadUnitResultAsync(); + + // Assert + result.IsSuccess.Should().BeFalse(); + } + [Fact] public async Task ReadResultAsync_JsonNull_Failure() { @@ -129,6 +169,22 @@ public async Task ReadResultAsyncOfT_JsonNull_Failure() result.Error.Should().Be(DtoMessages.ContentJsonNotResult); } + [Fact] + public async Task ReadUnitResultAsyncOfT_JsonNull_Failure() + { + // Assign + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + httpResponseMessage.Content = JsonContent.Create(null); + + + // Act + var result = await httpResponseMessage.ReadResultAsync(); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be(DtoMessages.ContentJsonNotResult); + } + [Fact] public async Task ReadResultAsync_JsonInt_Failure() { @@ -159,7 +215,7 @@ public async Task ReadResultAsyncOfT_JsonInt_Failure() result.Error.Should().Be(DtoMessages.ContentJsonNotResult); } - [Fact(Skip = "Fails when building on lunux")] + [Fact(Skip = "Fails when building on Linux")] public async Task ReadResultAsync_JsonObject_Failure() { // Assign @@ -174,7 +230,7 @@ public async Task ReadResultAsync_JsonObject_Failure() result.Error.Should().Be(DtoMessages.ContentJsonNotResult); } - [Fact(Skip = "Fails when building on lunux")] + [Fact(Skip = "Fails when building on Linux")] public async Task ReadResultAsyncOfT_JsonObject_Failure() { // Assign @@ -219,6 +275,20 @@ public async Task ReadResultAsyncOfT_JsonResultDtoOfSuccess_Success() result.Value.Should().Be(value); } + [Fact] + public async Task ReadUnitResultAsyncOfT_JsonResultDtoOfSuccess_Success() + { + // Assign + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + httpResponseMessage.Content = JsonContent.Create(UnitResultDto.Success()); + + // Act + var result = await httpResponseMessage.ReadUnitResultAsync(); + + // Assert + result.IsSuccess.Should().BeTrue(); + } + [Fact] public async Task ReadResultAsync_JsonResultDtoOfFailure_Failure() { @@ -250,6 +320,22 @@ public async Task ReadResultAsyncOfT_JsonResultDtoOfFailure_Failure() result.Error.Should().Be(error); } + [Fact] + public async Task ReadUnitResultAsyncOfT_JsonResultDtoOfFailure_Failure() + { + // Assign + var error = new UnitResultError() { ErrorMessage = "Failure" }; + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + httpResponseMessage.Content = JsonContent.Create(UnitResultDto.Failure(error)); + + // Act + var result = await httpResponseMessage.ReadUnitResultAsync(); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().BeEquivalentTo(error); + } + [Fact] public async Task ReadResultAsync_StringResultDtoOfSuccess_Success() { @@ -279,5 +365,27 @@ public async Task ReadResultAsyncOfT_StringResultDtoOfSuccess_Success() result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(value); } + + [Fact] + public async Task ReadUnitResultAsyncOfT_UnitResultErrorResultDtoOfSuccess_Success() + { + // Assign + HttpResponseMessage httpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + httpResponseMessage.Content = new StringContent($"{{ \"Error\": null }}"); + + // Act + var result = await httpResponseMessage.ReadUnitResultAsync(); + + // Assert + result.IsSuccess.Should().BeTrue(); + } + + private class UnitResultError + { + public UnitResultError() { } + + public string ErrorMessage { get; set; } + } + } } diff --git a/CSharpFunctionalExtensions/Result/Json/Serialization/CSharpFunctionalExtensionsJsonSerializerOptions.cs b/CSharpFunctionalExtensions/Result/Json/Serialization/CSharpFunctionalExtensionsJsonSerializerOptions.cs index e9703b15..8bc85e2b 100644 --- a/CSharpFunctionalExtensions/Result/Json/Serialization/CSharpFunctionalExtensionsJsonSerializerOptions.cs +++ b/CSharpFunctionalExtensions/Result/Json/Serialization/CSharpFunctionalExtensionsJsonSerializerOptions.cs @@ -3,7 +3,7 @@ namespace CSharpFunctionalExtensions.Json.Serialization { - internal static class CSharpFunctionalExtensionsJsonSerializerOptions + public static class CSharpFunctionalExtensionsJsonSerializerOptions { private static readonly Lazy LazyOptions = new(() => { diff --git a/CSharpFunctionalExtensions/Result/Json/Serialization/DtoMessages.cs b/CSharpFunctionalExtensions/Result/Json/Serialization/DtoMessages.cs index 9c986270..72868f56 100644 --- a/CSharpFunctionalExtensions/Result/Json/Serialization/DtoMessages.cs +++ b/CSharpFunctionalExtensions/Result/Json/Serialization/DtoMessages.cs @@ -1,9 +1,13 @@ -namespace CSharpFunctionalExtensions.Json.Serialization +using System.Net; + +namespace CSharpFunctionalExtensions.Json.Serialization { internal static class DtoMessages { public static readonly string HttpResponseMessageIsNull = "HttpResponseMessage is null"; public static readonly string ContentJsonNotResult = "The response content in not a Result"; + + public static string NotSuccsessStatusCodeFormat(HttpStatusCode statusCode, string content) => $"HttpStatus code is {statusCode}, Content {content}"; } } diff --git a/CSharpFunctionalExtensions/Result/Json/Serialization/HttpResponseMessageJsonExtensions.cs b/CSharpFunctionalExtensions/Result/Json/Serialization/HttpResponseMessageJsonExtensions.cs index b1f6a792..6b239c24 100644 --- a/CSharpFunctionalExtensions/Result/Json/Serialization/HttpResponseMessageJsonExtensions.cs +++ b/CSharpFunctionalExtensions/Result/Json/Serialization/HttpResponseMessageJsonExtensions.cs @@ -1,6 +1,7 @@ #nullable enable using System.Net.Http; using System.Net.Http.Json; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -8,28 +9,61 @@ namespace CSharpFunctionalExtensions.Json.Serialization { public static class HttpResponseMessageJsonExtensions { - public static Task ReadResultAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default(CancellationToken)) + public static async Task ReadResultAsync(this HttpResponseMessage response, bool ensureSuccessStatusCode = true, CancellationToken cancellationToken = default(CancellationToken)) { if (response is null) { - return Task.FromResult(Result.Failure(DtoMessages.HttpResponseMessageIsNull)); + return Result.Failure(DtoMessages.HttpResponseMessageIsNull); } - return + if (ensureSuccessStatusCode && !response.IsSuccessStatusCode) + { + return Result.Failure(DtoMessages.NotSuccsessStatusCodeFormat(response.StatusCode, await response.Content.ReadAsStringAsync())); + } + + return await Result.Try(() => response.Content.ReadFromJsonAsync(CSharpFunctionalExtensionsJsonSerializerOptions.Options, cancellationToken), ex => DtoMessages.ContentJsonNotResult) .Bind(result => result); } - public static Task> ReadResultAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default(CancellationToken)) + public static async Task> ReadResultAsync(this HttpResponseMessage response, bool ensureSuccessStatusCode = true, CancellationToken cancellationToken = default(CancellationToken)) { if (response is null) { - return Task.FromResult(Result.Failure(DtoMessages.HttpResponseMessageIsNull)); + return Result.Failure(DtoMessages.HttpResponseMessageIsNull); + } + + if (ensureSuccessStatusCode && !response.IsSuccessStatusCode) + { + return Result.Failure(DtoMessages.NotSuccsessStatusCodeFormat(response.StatusCode, await response.Content.ReadAsStringAsync())); } - return + return await Result.Try(() => response.Content.ReadFromJsonAsync>(CSharpFunctionalExtensionsJsonSerializerOptions.Options, cancellationToken), ex => DtoMessages.ContentJsonNotResult) .Bind(result => result); } + + public static async Task> ReadUnitResultAsync(this HttpResponseMessage? response, bool ensureSuccessStatusCode = true, CancellationToken cancellationToken = default(CancellationToken)) + where E : new() + { + if (response is null) + { + return UnitResult.Failure(new()); + } + + if (ensureSuccessStatusCode && !response.IsSuccessStatusCode) + { + return UnitResult.Failure(new()); + } + + try + { + return await response.Content.ReadFromJsonAsync>(CSharpFunctionalExtensionsJsonSerializerOptions.Options, cancellationToken); + } + catch (JsonException) + { + return UnitResult.Failure(new()); + } + } } } diff --git a/CSharpFunctionalExtensions/Result/Json/Serialization/JsonSerializerOptionsExtensionMethods.cs b/CSharpFunctionalExtensions/Result/Json/Serialization/JsonSerializerOptionsExtensionMethods.cs index e4715ef3..fbfe29ac 100644 --- a/CSharpFunctionalExtensions/Result/Json/Serialization/JsonSerializerOptionsExtensionMethods.cs +++ b/CSharpFunctionalExtensions/Result/Json/Serialization/JsonSerializerOptionsExtensionMethods.cs @@ -8,6 +8,7 @@ public static JsonSerializerOptions AddCSharpFunctionalExtensionsConverters(this { options.Converters.Add(new ResultJsonConverter()); options.Converters.Add(new ResultJsonConverterFactory()); + options.Converters.Add(new UnitResultJsonConverterFactory()); return options; } } diff --git a/CSharpFunctionalExtensions/Result/Json/Serialization/UnitResultDto.cs b/CSharpFunctionalExtensions/Result/Json/Serialization/UnitResultDto.cs new file mode 100644 index 00000000..a4ce9331 --- /dev/null +++ b/CSharpFunctionalExtensions/Result/Json/Serialization/UnitResultDto.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace CSharpFunctionalExtensions.Json.Serialization +{ + /// + /// Alternative entry-point for to avoid ambiguous calls + /// + internal static partial class UnitResultDto + { + /// + /// Creates a failure result with the given error. + /// + public static UnitResultDto Failure(E error) + { + return new UnitResultDto(error); + } + + /// + /// Creates a success . + /// + public static UnitResultDto Success() + { + return new UnitResultDto(); + } + } + + internal record UnitResultDto([property:Required] E? Error = default) + { + [JsonIgnore] + public bool IsSuccess => Error is null; + } +} diff --git a/CSharpFunctionalExtensions/Result/Json/Serialization/UnitResultJsonConverterFactoryOfT.cs b/CSharpFunctionalExtensions/Result/Json/Serialization/UnitResultJsonConverterFactoryOfT.cs new file mode 100644 index 00000000..dceb0e60 --- /dev/null +++ b/CSharpFunctionalExtensions/Result/Json/Serialization/UnitResultJsonConverterFactoryOfT.cs @@ -0,0 +1,68 @@ +#nullable enable +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CSharpFunctionalExtensions.Json.Serialization +{ + internal class UnitResultJsonConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + if (!typeToConvert.IsGenericType) return false; + + return typeToConvert.GetGenericTypeDefinition() == typeof(UnitResult<>); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type wrappedType = typeToConvert.GetGenericArguments()[0]; + + var genericResultType = typeof(UnitResultJsonConverterOfT<>).MakeGenericType(wrappedType); + JsonConverter? converter = Activator.CreateInstance(genericResultType) as JsonConverter; + + return converter!; + } + } + + internal class UnitResultJsonConverterOfT : JsonConverter> + { + public override UnitResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + return ToUnitResult(JsonSerializer.Deserialize>(ref reader, options)); + } + catch (JsonException) + { + return UnitResult.Failure(default!); + } + } + + public override void Write(Utf8JsonWriter writer, UnitResult value, JsonSerializerOptions options) + => JsonSerializer.Serialize(writer, ToUnitResultDto(value), options); + + private static UnitResult ToUnitResult(UnitResultDto? resultDto) + { + if (resultDto is not null) + { + if (resultDto.IsSuccess) + { + return UnitResult.Success(); + } + + if (resultDto.Error is not null) + { + return UnitResult.Failure(resultDto.Error); + } + } + + return UnitResult.Failure(default!); + } + + private static UnitResultDto ToUnitResultDto(UnitResult unitResult) + => unitResult.IsSuccess + ? UnitResultDto.Success() + : UnitResultDto.Failure(unitResult.Error); + } +}