From 465fda43e61b1f0831c3208e44207eca9d089043 Mon Sep 17 00:00:00 2001 From: Mattias Nordqvist Date: Sat, 26 Oct 2024 17:11:19 +0200 Subject: [PATCH] . --- ...ingleField_NonNullableNonOptional_Tests.cs | 108 +++++++++++++++++- FartingUnicorn/Mapper.cs | 70 +++++++++--- 2 files changed, 158 insertions(+), 20 deletions(-) diff --git a/FartingUnicorn.Tests/SingleField_NonNullableNonOptional_Tests.cs b/FartingUnicorn.Tests/SingleField_NonNullableNonOptional_Tests.cs index e1681cc..2b70fea 100644 --- a/FartingUnicorn.Tests/SingleField_NonNullableNonOptional_Tests.cs +++ b/FartingUnicorn.Tests/SingleField_NonNullableNonOptional_Tests.cs @@ -1073,7 +1073,7 @@ public class Author } [Fact] - public void Test1() + public void Valid() { var json = JsonSerializer.Deserialize(""" { @@ -1119,11 +1119,11 @@ public void AgeIsOptional_ButIsNotAllowedToBeMissing() """); var blogPost = Mapper.Map(json); blogPost.Success.Should().BeFalse(); - blogPost.Errors.Should().ContainSingle(e => e.Message == "Age is required"); // It'd be nice if the error message could include the path to the missing field + blogPost.Errors.Should().ContainSingle(e => e.Message == "Author.Age is required"); } [Fact] - public void AgeIsOtional() + public void AgeIsOptional() { var json = JsonSerializer.Deserialize(""" { @@ -1192,4 +1192,106 @@ public void AuthorIsWrongType() } } + + public class Optional + { + public class BlogPost + { + public string Title { get; set; } + public bool IsDraft { get; set; } + public Option Category { get; set; } + public Option Rating { get; set; } + public Option Author { get; set; } + } + public class Author + { + public string Name { get; set; } + public Option Age { get; set; } + } + [Fact] + public void Valid() + { + var json = JsonSerializer.Deserialize(""" + { + "Title": "Farting Unicorns", + "IsDraft": true, + "Category": "Horses", + "Rating": 5, + "Author": { + "Name": "John Doe", + "Age": 42 + } + } + """); + var blogPost = Mapper.Map(json); + blogPost.Success.Should().BeTrue(); + blogPost.Value.Title.Should().Be("Farting Unicorns"); + blogPost.Value.IsDraft.Should().BeTrue(); + blogPost.Value.Category.Should().BeOfType>(); + var someCategory = (blogPost.Value.Category as Some)!; + someCategory.Value.Should().Be("Horses"); + blogPost.Value.Rating.Should().BeOfType>(); + var someRating = (blogPost.Value.Rating as Some)!; + someRating.Value.Should().Be(5); + blogPost.Value.Author.Should().BeOfType>(); + var someAuthor = (blogPost.Value.Author as Some)!; + someAuthor.Value.Name.Should().Be("John Doe"); + someAuthor.Value.Age.Should().BeOfType>(); + var someAge = (someAuthor.Value.Age as Some)!; + someAge.Value.Should().Be(42); + } + + [Fact] + public void AgeIsOptional_ButIsNotAllowedToBeMissing() + { + var json = JsonSerializer.Deserialize(""" + { + "Title": "Farting Unicorns", + "IsDraft": true, + "Category": "Horses", + "Rating": 5, + "Author": { + "Name": "John Doe" + } + } + """); + var blogPost = Mapper.Map(json); + blogPost.Success.Should().BeFalse(); + blogPost.Errors.Should().ContainSingle(e => e.Message == "Author.Age is required"); + } + + [Fact] + public void AuthorCannoBeMissing() + { + var json = JsonSerializer.Deserialize(""" + { + "Title": "Farting Unicorns", + "IsDraft": true, + "Category": "Horses", + "Rating": 5 + } + """); + var blogPost = Mapper.Map(json); + blogPost.Success.Should().BeFalse(); + blogPost.Errors.Should().ContainSingle(e => e.Message == "Author is required"); + } + + [Fact] + public void AuthorIsOptional() + { + var json = JsonSerializer.Deserialize(""" + { + "Title": "Farting Unicorns", + "IsDraft": true, + "Category": "Horses", + "Rating": 5, + "Author": null + } + """); + var blogPost = Mapper.Map(json); + blogPost.Success.Should().BeTrue(); + blogPost.Value.Author.Should().BeOfType>(); + + } + } } \ No newline at end of file diff --git a/FartingUnicorn/Mapper.cs b/FartingUnicorn/Mapper.cs index 4057723..2cc6a64 100644 --- a/FartingUnicorn/Mapper.cs +++ b/FartingUnicorn/Mapper.cs @@ -12,9 +12,13 @@ public class Mapper public record RequiredPropertyMissingError(string propertyName) : ErrorBase($"{propertyName} is required"); public record RequiredValueMissingError(string propertyName) : ErrorBase($"{propertyName} must have a value"); public record ValueHasWrongTypeError(string propertyName, string expectedType, string actualType) : ErrorBase($"Value of {propertyName} has the wrong type. Expected {expectedType}, got {actualType}"); - public static Result Map(JsonElement json) + public static Result Map(JsonElement json, string[] path = null) where T : new() { + if(path is null) + { + path = Array.Empty(); + } // Rewrite this with source generator to avoid type generics and reflection Result validationResult = UnitResult.Ok; @@ -107,26 +111,58 @@ public static Result Map(JsonElement json) } else { - var mapMethod = typeof(Mapper).GetMethod("Map", BindingFlags.Public | BindingFlags.Static); - var genericMapMethod = mapMethod.MakeGenericMethod(property.PropertyType); - var result = genericMapMethod.Invoke(null, [jsonProperty]); - var resultSuccessProperty = result.GetType().GetProperty("Success"); + var newPath = path.Append(property.Name).ToArray(); - if ((bool)resultSuccessProperty.GetValue(result)) + if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Option<>)) { - var valueProperty = result.GetType().GetProperty("Value"); - property.SetValue(obj, valueProperty.GetValue(result)); + var mapMethod = typeof(Mapper).GetMethod("Map", BindingFlags.Public | BindingFlags.Static); + var genericMapMethod = mapMethod.MakeGenericMethod(property.PropertyType.GetGenericArguments()[0]); + var result = genericMapMethod.Invoke(null, [jsonProperty, newPath]); + var resultSuccessProperty = result.GetType().GetProperty("Success"); + + if ((bool)resultSuccessProperty.GetValue(result)) + { + var valueProperty = result.GetType().GetProperty("Value"); + var getValue = valueProperty.GetValue(result); + var someType = typeof(Some<>).MakeGenericType(property.PropertyType.GetGenericArguments()[0]); + var someInstance = Activator.CreateInstance(someType, getValue); + property.SetValue(obj, someInstance); + } + else + { + var errorProperty = result.GetType().GetProperty("Errors"); + var errors = (IReadOnlyList)errorProperty.GetValue(result); + var newErrorsResult = UnitResult.Ok; + foreach (var error in errors) + { + newErrorsResult = newErrorsResult.Or(Result.Error(error)); + } + validationResult = validationResult.Or(newErrorsResult); + } } - else - { - var errorProperty = result.GetType().GetProperty("Errors"); - var errors = (IReadOnlyList)errorProperty.GetValue(result); - var newErrorsResult = UnitResult.Ok; - foreach (var error in errors) + else + { + var mapMethod = typeof(Mapper).GetMethod("Map", BindingFlags.Public | BindingFlags.Static); + var genericMapMethod = mapMethod.MakeGenericMethod(property.PropertyType); + var result = genericMapMethod.Invoke(null, [jsonProperty, newPath]); + var resultSuccessProperty = result.GetType().GetProperty("Success"); + + if ((bool)resultSuccessProperty.GetValue(result)) + { + var valueProperty = result.GetType().GetProperty("Value"); + property.SetValue(obj, valueProperty.GetValue(result)); + } + else { - newErrorsResult = newErrorsResult.Or(Result.Error(error)); + var errorProperty = result.GetType().GetProperty("Errors"); + var errors = (IReadOnlyList)errorProperty.GetValue(result); + var newErrorsResult = UnitResult.Ok; + foreach (var error in errors) + { + newErrorsResult = newErrorsResult.Or(Result.Error(error)); + } + validationResult = validationResult.Or(newErrorsResult); } - validationResult = validationResult.Or(newErrorsResult); } } } @@ -140,7 +176,7 @@ public static Result Map(JsonElement json) } else { - validationResult = validationResult.Or(Result.Error(new RequiredPropertyMissingError(property.Name))); + validationResult = validationResult.Or(Result.Error(new RequiredPropertyMissingError(string.Join(".", path.Append(property.Name))))); } } }