-
-
Notifications
You must be signed in to change notification settings - Fork 70
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
using System.Globalization; | ||
using NetDaemon.Client.Internal.Json; | ||
|
||
namespace NetDaemon.HassClient.Tests.Json; | ||
|
||
public class EnsureExpectedDatatypeConverterTest | ||
{ | ||
private readonly JsonSerializerOptions _defaultSerializerOptions = DefaultSerializerOptions.DeserializationOptions; | ||
|
||
[Fact] | ||
public void TestConvertAllSupportedTypesConvertsCorrectly() | ||
{ | ||
var json = @" | ||
{ | ||
""string"": ""string"", | ||
""int"": 10000, | ||
""short"": 2000, | ||
""float"": 1.1, | ||
""bool"": true, | ||
""datetime"": ""2019-02-16T18:11:44.183673+00:00"" | ||
} | ||
"; | ||
|
||
var record = JsonSerializer.Deserialize<SupportedTypesTestRecord>(json, _defaultSerializerOptions); | ||
record!.SomeString.Should().Be("string"); | ||
record!.SomeInt.Should().Be(10000); | ||
record!.SomeShort.Should().Be(2000); | ||
record!.SomeFloat.Should().Be(1.1f); | ||
record!.SomeBool.Should().BeTrue(); | ||
record!.SomeDateTime.Should().Be(DateTime.Parse("2019-02-16T18:11:44.183673+00:00", CultureInfo.InvariantCulture)); | ||
} | ||
|
||
[Fact] | ||
public void TestConvertAllSupportedTypesConvertsToNullWhenWrongDatatypeCorrectly() | ||
{ | ||
var json = @" | ||
{ | ||
""string"": 1, | ||
""int"": ""10000"", | ||
""short"": ""2000"", | ||
""float"": {""property"": ""100""}, | ||
""bool"": ""hello"", | ||
""datetime"": ""test"" | ||
} | ||
"; | ||
|
||
var record = JsonSerializer.Deserialize<SupportedTypesTestRecord>(json, _defaultSerializerOptions); | ||
record!.SomeString.Should().BeNull(); | ||
record!.SomeInt.Should().BeNull(); | ||
record!.SomeShort.Should().BeNull(); | ||
record!.SomeFloat.Should().BeNull(); | ||
record!.SomeBool.Should().BeNull(); | ||
record!.SomeDateTime.Should().BeNull(); | ||
} | ||
|
||
[Fact] | ||
public void TestConvertAllSupportedTypesConvertsToNullWhenNullJsonCorrectly() | ||
{ | ||
var json = @" | ||
{ | ||
""string"": null, | ||
""int"": null, | ||
""short"": null, | ||
""float"": null, | ||
""bool"": null, | ||
""datetime"": null | ||
} | ||
"; | ||
|
||
var record = JsonSerializer.Deserialize<SupportedTypesTestRecord>(json, _defaultSerializerOptions); | ||
record!.SomeString.Should().BeNull(); | ||
record!.SomeInt.Should().BeNull(); | ||
record!.SomeShort.Should().BeNull(); | ||
record!.SomeFloat.Should().BeNull(); | ||
record!.SomeBool.Should().BeNull(); | ||
record!.SomeDateTime.Should().BeNull(); | ||
} | ||
|
||
[Fact] | ||
public void TestConvertAllNonNullShouldThrowExcptionIfThereAreADatatypeError() | ||
{ | ||
var json = @" | ||
{ | ||
""string"": 1 | ||
} | ||
"; | ||
var result = JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions); | ||
// The string can be null even if not nullable so it will not throw. | ||
result!.SomeString.Should().BeNull(); | ||
json = @" | ||
{ | ||
""int"": ""10000"" | ||
} | ||
"; | ||
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions)) | ||
.Should().Throw<JsonException>(); | ||
json = @" | ||
{ | ||
""short"": ""2000"" | ||
} | ||
"; | ||
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions)) | ||
.Should().Throw<JsonException>(); | ||
json = @" | ||
{ | ||
""float"": {""property"": ""100""} | ||
} | ||
"; | ||
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions)) | ||
.Should().Throw<JsonException>(); | ||
json = @" | ||
{ | ||
""bool"": ""hello"" | ||
} | ||
"; | ||
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions)) | ||
.Should().Throw<JsonException>(); | ||
json = @" | ||
{ | ||
""datetime"": ""test"" | ||
} | ||
"; | ||
FluentActions.Invoking(() => JsonSerializer.Deserialize<SupportedTypesNonNullTestRecord>(json, _defaultSerializerOptions)) | ||
.Should().Throw<JsonException>(); | ||
} | ||
} | ||
|
||
public record SupportedTypesTestRecord | ||
{ | ||
[JsonPropertyName("string")] public string? SomeString { get; init; } | ||
[JsonPropertyName("int")] public int? SomeInt { get; init; } | ||
[JsonPropertyName("short")] public short? SomeShort { get; init; } | ||
[JsonPropertyName("float")] public float? SomeFloat { get; init; } | ||
[JsonPropertyName("bool")] public bool? SomeBool { get; init; } | ||
[JsonPropertyName("datetime")] public DateTime? SomeDateTime { get; init; } | ||
} | ||
|
||
public record SupportedTypesNonNullTestRecord | ||
{ | ||
[JsonPropertyName("string")] public string SomeString { get; init; } = string.Empty; | ||
[JsonPropertyName("int")] public int SomeInt { get; init; } | ||
[JsonPropertyName("short")] public short SomeShort { get; init; } | ||
[JsonPropertyName("float")] public float SomeFloat { get; init; } | ||
[JsonPropertyName("bool")] public bool SomeBool { get; init; } | ||
[JsonPropertyName("datetime")] public DateTime SomeDateTime { get; init; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
namespace NetDaemon.Client.Internal.Json; | ||
|
||
//<summary> | ||
// Default options for serialization when serializing and deserializing json | ||
// </summary> | ||
internal static class DefaultSerializerOptions | ||
{ | ||
public static JsonSerializerOptions DeserializationOptions => new() | ||
{ | ||
Converters = | ||
{ | ||
new EnsureStringConverter(), | ||
new EnsureIntConverter(), | ||
new EnsureShortConverter(), | ||
new EnsureBooleanConverter(), | ||
new EnsureFloatConverter(), | ||
new EnsureDateTimeConverter() | ||
} | ||
}; | ||
|
||
public static JsonSerializerOptions SerializationOptions => new() | ||
{ | ||
WriteIndented = false, | ||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
namespace NetDaemon.Client.Internal.Json; | ||
|
||
/// <summary> | ||
/// Base class for converters that ensures the expected suported datatyps | ||
/// </summary> | ||
/// <remarks> | ||
/// This is a workaround to make the serializer to not throw exceptions when there are unexpected datatypes returning from Home Assistant json | ||
/// This converter will only be used when deserializing json | ||
/// | ||
/// Note: Tried to make a even smarter generic class but could not get it to avoid recursion | ||
/// </remarks> | ||
internal abstract class EnsureExcpectedDatatypeConverterBase<T> : JsonConverter<T?> | ||
{ | ||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
throw new NotImplementedException(); | ||
|
||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) | ||
{ | ||
JsonSerializer.Serialize(writer, value, typeof(T), options); | ||
} | ||
Check warning on line 20 in src/Client/NetDaemon.HassClient/Internal/Json/EnsureExcpectedDatatypeConverter.cs
|
||
|
||
protected static object? ReadTokenSuccessfullyOrNull(ref Utf8JsonReader reader, JsonTokenType[] tokenType) | ||
{ | ||
if (!tokenType.Contains(reader.TokenType)) | ||
{ | ||
// Skip the children of current token if it is not the expected one | ||
reader.Skip(); | ||
return null; | ||
} | ||
|
||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); | ||
|
||
try | ||
{ | ||
return Type.GetTypeCode(type) switch | ||
{ | ||
TypeCode.String => reader.GetString(), | ||
TypeCode.Int32 => reader.GetInt32(), | ||
TypeCode.Int16 => reader.GetInt16(), | ||
TypeCode.Boolean => reader.GetBoolean(), | ||
TypeCode.Single => reader.GetSingle(), | ||
TypeCode.DateTime => reader.GetDateTime(), | ||
_ => throw new NotImplementedException($"Type {typeof(T)} with timecode {Type.GetTypeCode(type)} is not implemented") | ||
}; | ||
} | ||
catch (JsonException) | ||
{ | ||
// Skip the children of current token | ||
reader.Skip(); | ||
return null; | ||
Check warning on line 50 in src/Client/NetDaemon.HassClient/Internal/Json/EnsureExcpectedDatatypeConverter.cs
|
||
} | ||
catch (FormatException) | ||
{ | ||
// We are getting this exception when for example there are a format error of dates etc | ||
// I am reluctant if this error really should just return null, codereview should discuss | ||
// Maybe trace log the error? | ||
reader.Skip(); | ||
return null; | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Converts a Json element that can be a string or returns null if it is not a string | ||
/// </summary> | ||
internal class EnsureStringConverter : EnsureExcpectedDatatypeConverterBase<string?> | ||
{ | ||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.String, JsonTokenType.Null]) as string; | ||
} | ||
|
||
/// <summary> | ||
/// Converts a Json element that can be a int or returns null if it is not a int | ||
/// </summary> | ||
internal class EnsureIntConverter : EnsureExcpectedDatatypeConverterBase<int?> | ||
{ | ||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
(int?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.Number, JsonTokenType.Null]); | ||
} | ||
|
||
/// <summary> | ||
/// Converts a Json element that can be a short or returns null if it is not a short | ||
/// </summary> | ||
internal class EnsureShortConverter : EnsureExcpectedDatatypeConverterBase<short?> | ||
{ | ||
public override short? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
(short?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.Number, JsonTokenType.Null]); | ||
} | ||
|
||
/// <summary> | ||
/// Converts a Json element that can be a float or returns null if it is not afloat | ||
/// </summary> | ||
internal class EnsureFloatConverter : EnsureExcpectedDatatypeConverterBase<float?> | ||
{ | ||
public override float? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
(float?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.Number, JsonTokenType.Null]); | ||
} | ||
|
||
/// <summary> | ||
/// Converts a Json element that can be a boolean or returns null if it is not a boolean | ||
/// </summary> | ||
internal class EnsureBooleanConverter : EnsureExcpectedDatatypeConverterBase<bool?> | ||
{ | ||
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
(bool?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.True, JsonTokenType.False, JsonTokenType.Null]); | ||
} | ||
|
||
/// <summary> | ||
/// Converts a Json element that can be a string or returns null if it is not a string | ||
/// </summary> | ||
internal class EnsureDateTimeConverter : EnsureExcpectedDatatypeConverterBase<DateTime?> | ||
{ | ||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => | ||
(DateTime?) ReadTokenSuccessfullyOrNull(ref reader, [JsonTokenType.String, JsonTokenType.Null]); | ||
} | ||
|
||
/// <summary> | ||
/// Return all the converters that should be used when deserializing | ||
/// </summary> | ||
internal static class EnsureExpectedDatatypeConverter | ||
{ | ||
public static IList<JsonConverter> Converters() => | ||
[ | ||
new EnsureStringConverter(), | ||
new EnsureIntConverter(), | ||
new EnsureShortConverter(), | ||
new EnsureFloatConverter(), | ||
new EnsureBooleanConverter(), | ||
new EnsureDateTimeConverter() | ||
]; | ||
Check warning on line 130 in src/Client/NetDaemon.HassClient/Internal/Json/EnsureExcpectedDatatypeConverter.cs
|
||
} |
This file was deleted.