-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Json Serialization and Deserialization (#22)
* added support for serializing `Result` and `Result<T>` to JSON * added support for deserializing`Result` and `Result<T>` from JSON * added unit test to cover JSON serializing and deserializing
- Loading branch information
1 parent
76941d2
commit dadfa6f
Showing
18 changed files
with
586 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace RailwayResult.Helpers; | ||
|
||
internal record TypeDefinition(string AssemblyName, string TypeName) : ISpanParsable<TypeDefinition> | ||
{ | ||
public TypeDefinition(Type type) : this(type.Assembly.GetName().Name!, type.FullName!) { } | ||
|
||
public static TypeDefinition Parse(ReadOnlySpan<char> s, IFormatProvider? provider) | ||
{ | ||
Span<Range> range = stackalloc Range[2]; | ||
s.Split(range, '/'); | ||
|
||
var assemblyName = s[range[0]]; | ||
var typeName = s[range[1]]; | ||
|
||
return new TypeDefinition(assemblyName.ToString(), typeName.ToString()); | ||
} | ||
|
||
public static TypeDefinition Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), provider); | ||
|
||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out TypeDefinition result) | ||
{ | ||
result = Parse(s, provider); | ||
return true; | ||
} | ||
|
||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TypeDefinition result) => TryParse(s.AsSpan(), provider, out result); | ||
|
||
public override string ToString() => $"{AssemblyName}/{TypeName}"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
namespace RailwayResult.Helpers; | ||
|
||
internal static class Utf8JsonReaderExtensions | ||
{ | ||
public static void ReadOrThrow(ref this Utf8JsonReader reader, JsonTokenType type, string? errorMessage = null) | ||
{ | ||
reader.Read(); | ||
if (reader.TokenType != type) | ||
{ | ||
throw new JsonException(errorMessage); | ||
} | ||
} | ||
|
||
public static string? ReadPropertyName(ref this Utf8JsonReader reader, params string[] expectedParams) | ||
{ | ||
reader.Read(); | ||
var name = reader.GetString(); | ||
if (reader.TokenType != JsonTokenType.PropertyName || !expectedParams.Contains(name)) | ||
{ | ||
throw new JsonException($"Expected one of '{string.Join("', '", expectedParams)}'."); | ||
} | ||
|
||
return name; | ||
} | ||
|
||
public static void ReadPropertyName(ref this Utf8JsonReader reader, string expectedPropertyName) | ||
{ | ||
reader.Read(); | ||
var name = reader.GetString(); | ||
if (reader.TokenType != JsonTokenType.PropertyName || name != expectedPropertyName) | ||
{ | ||
throw new JsonException($"Expected property '{expectedPropertyName}'."); | ||
} | ||
} | ||
|
||
public static string ReadStringValue(ref this Utf8JsonReader reader, string? errorMessage = null) | ||
{ | ||
reader.Read(); | ||
if (reader.TokenType != JsonTokenType.String) | ||
{ | ||
throw new JsonException(errorMessage); | ||
} | ||
|
||
return reader.GetString()!; | ||
} | ||
|
||
public static Type ReadErrorType(ref this Utf8JsonReader reader, string? errorMessage = null) | ||
{ | ||
reader.Read(); | ||
if (reader.TokenType != JsonTokenType.String) | ||
{ | ||
throw new JsonException(errorMessage); | ||
} | ||
|
||
var typeDefinitionString = reader.GetString()!; | ||
var typeDefinition = TypeDefinition.Parse(typeDefinitionString, default); | ||
|
||
try | ||
{ | ||
var assembly = Assembly.Load(typeDefinition.AssemblyName); | ||
return assembly.GetType(typeDefinition.TypeName)!; | ||
} | ||
catch | ||
{ | ||
throw new JsonException($"Failed to load error type [{typeDefinitionString}]."); | ||
} | ||
} | ||
|
||
public static Error ReadError(ref this Utf8JsonReader reader, Type valueType, JsonSerializerOptions options) | ||
{ | ||
reader.Read(); | ||
var converter = Unsafe.As<JsonConverter<Error>>(options.GetConverter(valueType)); | ||
return converter.Read(ref reader, valueType, options)!; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System.Runtime.CompilerServices; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
namespace RailwayResult.Helpers; | ||
|
||
internal static class Utf8JsonWriterExtensions | ||
{ | ||
public static void WriteError(this Utf8JsonWriter writer, Error error, Type valueType, JsonSerializerOptions options) | ||
{ | ||
var converter = Unsafe.As<JsonConverter<Error>>(options.GetConverter(valueType)); | ||
converter.Write(writer, error, options); | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
src/RailwayResult/JsonConverters/GenericResultJsonConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
using RailwayResult.Helpers; | ||
|
||
namespace RailwayResult.JsonConverters; | ||
|
||
internal sealed class GenericResultJsonConverter<T> : JsonConverter<Result<T>> | ||
{ | ||
internal const string JSON_VALUE = "Value"; | ||
internal const string JSON_ERROR_TYPE = "ErrorType"; | ||
internal const string JSON_ERROR = "Error"; | ||
|
||
private static readonly Type ValueType = typeof(T); | ||
|
||
private readonly JsonConverter<T> _valueConverter; | ||
|
||
public GenericResultJsonConverter(JsonSerializerOptions options) | ||
{ | ||
_valueConverter = (JsonConverter<T>)options.GetConverter(typeof(T)); | ||
} | ||
|
||
public override Result<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
if (reader.TokenType != JsonTokenType.StartObject) | ||
{ | ||
throw new JsonException("Expected '{'."); | ||
} | ||
|
||
var name = reader.ReadPropertyName(JSON_ERROR_TYPE, JSON_VALUE); | ||
Result<T>? result; | ||
|
||
if (name == JSON_ERROR_TYPE) | ||
{ | ||
var errorType = reader.ReadErrorType("Expected error type definition."); | ||
|
||
reader.ReadPropertyName(JSON_ERROR); | ||
var error = reader.ReadError(errorType, options); | ||
|
||
result = new(error); | ||
} | ||
else | ||
{ | ||
reader.Read(); | ||
result = _valueConverter.Read(ref reader, ValueType, options); | ||
} | ||
|
||
reader.ReadOrThrow(JsonTokenType.EndObject, "Expected '}'."); | ||
return result; | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, Result<T> value, JsonSerializerOptions options) | ||
{ | ||
writer.WriteStartObject(); | ||
|
||
if (value!.IsSuccess) | ||
{ | ||
writer.WritePropertyName(JSON_VALUE); | ||
_valueConverter.Write(writer, value.Value, options); | ||
} | ||
else | ||
{ | ||
var errorType = value.Error.GetType(); | ||
writer.WriteString(JSON_ERROR_TYPE, new TypeDefinition(errorType).ToString()); | ||
|
||
writer.WritePropertyName(JSON_ERROR); | ||
writer.WriteError(value.Error, errorType, options); | ||
} | ||
|
||
writer.WriteEndObject(); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/RailwayResult/JsonConverters/GenericResultJsonConverterFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using System.Reflection; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
namespace RailwayResult.JsonConverters; | ||
|
||
internal sealed class GenericResultJsonConverterFactory : JsonConverterFactory | ||
{ | ||
private static readonly Type GenericResultType = typeof(Result<>); | ||
private static readonly Type GenericResultConverterType = typeof(GenericResultJsonConverter<>); | ||
|
||
public override bool CanConvert(Type typeToConvert) | ||
{ | ||
return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == GenericResultType; | ||
} | ||
|
||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
var nestedType = typeToConvert.GetGenericArguments()[0]; | ||
var converterType = GenericResultConverterType.MakeGenericType(nestedType); | ||
var flags = BindingFlags.Instance | BindingFlags.Public; | ||
return Activator.CreateInstance(converterType, flags, null, [options], null) as JsonConverter; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
using RailwayResult.Helpers; | ||
|
||
namespace RailwayResult.JsonConverters; | ||
|
||
internal sealed class ResultJsonConverter : JsonConverter<Result> | ||
{ | ||
internal const string JSON_ERROR_TYPE = "Type"; | ||
internal const string JSON_ERROR = "Error"; | ||
|
||
public override Result? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
if (reader.TokenType != JsonTokenType.StartObject) | ||
{ | ||
throw new JsonException("Expected '{'."); | ||
} | ||
|
||
var name = reader.ReadPropertyName(JSON_ERROR_TYPE, JSON_ERROR); | ||
Result? result; | ||
|
||
if (name == JSON_ERROR_TYPE) | ||
{ | ||
var errorType = reader.ReadErrorType("Expected error type definition."); | ||
|
||
reader.ReadPropertyName(JSON_ERROR); | ||
var error = reader.ReadError(errorType, options); | ||
|
||
result = new(error); | ||
} | ||
else | ||
{ | ||
reader.ReadOrThrow(JsonTokenType.Null, "Expected null, failure Result has to have specified error type."); | ||
result = Result.Success; | ||
} | ||
|
||
reader.ReadOrThrow(JsonTokenType.EndObject, "Expected '}'."); | ||
return result; | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, Result value, JsonSerializerOptions options) | ||
{ | ||
writer.WriteStartObject(); | ||
|
||
if (value.IsSuccess) | ||
{ | ||
writer.WritePropertyName(JSON_ERROR); | ||
writer.WriteNullValue(); | ||
} | ||
else | ||
{ | ||
var errorType = value.Error.GetType(); | ||
writer.WriteString(JSON_ERROR_TYPE, new TypeDefinition(errorType).ToString()); | ||
|
||
writer.WritePropertyName(JSON_ERROR); | ||
writer.WriteError(value.Error, errorType, options); | ||
} | ||
|
||
writer.WriteEndObject(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
namespace RailwayResult.Tests.Extensions; | ||
|
||
public static class ResultExtensions | ||
{ | ||
public static void ShouldBe(this Result self, Result expected) | ||
{ | ||
if (self!.IsSuccess) | ||
{ | ||
expected.IsSuccess.Should().BeTrue(); | ||
} | ||
else | ||
{ | ||
expected.IsSuccess.Should().BeFalse(); | ||
self.Error.Should().BeEquivalentTo(expected.Error); | ||
} | ||
} | ||
|
||
public static void ShouldBe<T>(this Result<T> self, Result<T> expected) | ||
{ | ||
if (self.IsSuccess) | ||
{ | ||
expected.IsSuccess.Should().BeTrue(); | ||
self.Value.Should().BeEquivalentTo(expected.Value); | ||
} | ||
else | ||
{ | ||
expected.IsSuccess.Should().BeFalse(); | ||
self.Error.Should().BeEquivalentTo(expected.Error); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
namespace RailwayResults.Tests.Mocks; | ||
|
||
public sealed record BasicError(string Key, string Message) : Error(Key, Message) | ||
{ | ||
public static readonly BasicError ErrorA = new("Key", "Error A"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using static RailwayResults.Tests.Mocks.ComplexError; | ||
|
||
namespace RailwayResults.Tests.Mocks; | ||
|
||
public sealed record ComplexError( | ||
string Key, | ||
string Message, | ||
string AdditionalString, | ||
int AdditionalInt, | ||
NestedRecord Record, | ||
List<NestedRecord> Records) : Error(Key, Message) | ||
{ | ||
public static readonly ComplexError One = new("key", "msg", "one", 1, new(2, "two"), [ | ||
new(3, "three"), | ||
new(4, "four") | ||
]); | ||
|
||
public record NestedRecord(int A, string B); | ||
} |
Oops, something went wrong.