Skip to content

Commit

Permalink
.
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiasnordqvist committed Oct 26, 2024
1 parent b43ba83 commit 465fda4
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 20 deletions.
108 changes: 105 additions & 3 deletions FartingUnicorn.Tests/SingleField_NonNullableNonOptional_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,7 +1073,7 @@ public class Author
}

[Fact]
public void Test1()
public void Valid()
{
var json = JsonSerializer.Deserialize<JsonElement>("""
{
Expand Down Expand Up @@ -1119,11 +1119,11 @@ public void AgeIsOptional_ButIsNotAllowedToBeMissing()
""");
var blogPost = Mapper.Map<BlogPost>(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<JsonElement>("""
{
Expand Down Expand Up @@ -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<string> Category { get; set; }
public Option<int> Rating { get; set; }
public Option<Author> Author { get; set; }
}
public class Author
{
public string Name { get; set; }
public Option<int> Age { get; set; }
}
[Fact]
public void Valid()
{
var json = JsonSerializer.Deserialize<JsonElement>("""
{
"Title": "Farting Unicorns",
"IsDraft": true,
"Category": "Horses",
"Rating": 5,
"Author": {
"Name": "John Doe",
"Age": 42
}
}
""");
var blogPost = Mapper.Map<BlogPost>(json);
blogPost.Success.Should().BeTrue();
blogPost.Value.Title.Should().Be("Farting Unicorns");
blogPost.Value.IsDraft.Should().BeTrue();
blogPost.Value.Category.Should().BeOfType<Some<string>>();
var someCategory = (blogPost.Value.Category as Some<string>)!;
someCategory.Value.Should().Be("Horses");
blogPost.Value.Rating.Should().BeOfType<Some<int>>();
var someRating = (blogPost.Value.Rating as Some<int>)!;
someRating.Value.Should().Be(5);
blogPost.Value.Author.Should().BeOfType<Some<Author>>();
var someAuthor = (blogPost.Value.Author as Some<Author>)!;
someAuthor.Value.Name.Should().Be("John Doe");
someAuthor.Value.Age.Should().BeOfType<Some<int>>();
var someAge = (someAuthor.Value.Age as Some<int>)!;
someAge.Value.Should().Be(42);
}

[Fact]
public void AgeIsOptional_ButIsNotAllowedToBeMissing()
{
var json = JsonSerializer.Deserialize<JsonElement>("""
{
"Title": "Farting Unicorns",
"IsDraft": true,
"Category": "Horses",
"Rating": 5,
"Author": {
"Name": "John Doe"
}
}
""");
var blogPost = Mapper.Map<BlogPost>(json);
blogPost.Success.Should().BeFalse();
blogPost.Errors.Should().ContainSingle(e => e.Message == "Author.Age is required");
}

[Fact]
public void AuthorCannoBeMissing()
{
var json = JsonSerializer.Deserialize<JsonElement>("""
{
"Title": "Farting Unicorns",
"IsDraft": true,
"Category": "Horses",
"Rating": 5
}
""");
var blogPost = Mapper.Map<BlogPost>(json);
blogPost.Success.Should().BeFalse();
blogPost.Errors.Should().ContainSingle(e => e.Message == "Author is required");
}

[Fact]
public void AuthorIsOptional()
{
var json = JsonSerializer.Deserialize<JsonElement>("""
{
"Title": "Farting Unicorns",
"IsDraft": true,
"Category": "Horses",
"Rating": 5,
"Author": null
}
""");
var blogPost = Mapper.Map<BlogPost>(json);
blogPost.Success.Should().BeTrue();
blogPost.Value.Author.Should().BeOfType<None<Author>>();

}
}
}
70 changes: 53 additions & 17 deletions FartingUnicorn/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> Map<T>(JsonElement json)
public static Result<T> Map<T>(JsonElement json, string[] path = null)

Check warning on line 15 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
where T : new()
{
if(path is null)
{
path = Array.Empty<string>();
}
// Rewrite this with source generator to avoid type generics and reflection

Result<Unit> validationResult = UnitResult.Ok;
Expand Down Expand Up @@ -107,26 +111,58 @@ public static Result<T> Map<T>(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]);

Check warning on line 119 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
var result = genericMapMethod.Invoke(null, [jsonProperty, newPath]);
var resultSuccessProperty = result.GetType().GetProperty("Success");

Check warning on line 121 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

if ((bool)resultSuccessProperty.GetValue(result))

Check warning on line 123 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 123 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Unboxing a possibly null value.
{
var valueProperty = result.GetType().GetProperty("Value");
var getValue = valueProperty.GetValue(result);

Check warning on line 126 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
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<IError>)errorProperty.GetValue(result);

Check warning on line 134 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 134 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
var newErrorsResult = UnitResult.Ok;
foreach (var error in errors)

Check warning on line 136 in FartingUnicorn/Mapper.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
newErrorsResult = newErrorsResult.Or(Result<Unit>.Error(error));
}
validationResult = validationResult.Or(newErrorsResult);
}
}
else
{
var errorProperty = result.GetType().GetProperty("Errors");
var errors = (IReadOnlyList<IError>)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<Unit>.Error(error));
var errorProperty = result.GetType().GetProperty("Errors");
var errors = (IReadOnlyList<IError>)errorProperty.GetValue(result);
var newErrorsResult = UnitResult.Ok;
foreach (var error in errors)
{
newErrorsResult = newErrorsResult.Or(Result<Unit>.Error(error));
}
validationResult = validationResult.Or(newErrorsResult);
}
validationResult = validationResult.Or(newErrorsResult);
}
}
}
Expand All @@ -140,7 +176,7 @@ public static Result<T> Map<T>(JsonElement json)
}
else
{
validationResult = validationResult.Or(Result<Unit>.Error(new RequiredPropertyMissingError(property.Name)));
validationResult = validationResult.Or(Result<Unit>.Error(new RequiredPropertyMissingError(string.Join(".", path.Append(property.Name)))));
}
}
}
Expand Down

0 comments on commit 465fda4

Please sign in to comment.