Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/AvroConvert/AvroConvert.SerializeHeadless.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ public static partial class AvroConvert
/// <summary>
/// Serializes given object to Avro format - <c>excluding</c> header
/// </summary>
public static byte[] SerializeHeadless(object obj, string schema)
public static byte[] SerializeHeadless(object obj, string schema, AvroConvertOptions options = null)
{
MemoryStream resultStream = new MemoryStream();
var encoder = new Writer(resultStream);
var schemaObject = Schema.Parse(schema);
var resolver = new WriteResolver();
var resolver = new WriteResolver(options);
var writer = resolver.ResolveWriter(schemaObject);

writer(obj, encoder);
Expand All @@ -44,12 +44,12 @@ public static byte[] SerializeHeadless(object obj, string schema)
/// <summary>
/// Serializes given object to Avro format - <c>excluding</c> header
/// </summary>
public static byte[] SerializeHeadless(object obj, Type objectType)
public static byte[] SerializeHeadless(object obj, Type objectType, AvroConvertOptions options = null)
{
MemoryStream resultStream = new MemoryStream();
var encoder = new Writer(resultStream);
var schemaObject = BuildSchema(objectType);
var resolver = new WriteResolver();
var resolver = new WriteResolver(options);
var writer = resolver.ResolveWriter(schemaObject);

writer(obj, encoder);
Expand Down
5 changes: 5 additions & 0 deletions src/AvroConvert/AvroConvertOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ public class AvroConvertOptions
/// Gets or sets the naming policy that can determine how types and fields are named.
/// </summary>
public IAvroNamingPolicy NamingPolicy { get; set; }

/// <summary>
/// Gets or sets the number handling behavior for Avro types.
/// </summary>
public AvroNumberHandling NumberHandling { get; set; }
}
26 changes: 24 additions & 2 deletions src/AvroConvert/AvroObjectServices/Write/Resolvers/Decimal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using SolTechnology.Avro.AvroObjectServices.Schemas.AvroTypes;
using SolTechnology.Avro.Features.Serialize;
using SolTechnology.Avro.Infrastructure.Exceptions;
using SolTechnology.Avro.Policies;

// ReSharper disable once CheckNamespace
namespace SolTechnology.Avro.AvroObjectServices.Write
Expand All @@ -36,11 +37,32 @@ internal void ResolveDecimal(DecimalSchema schema, object logicalValue, IWriter
int sizeDiff = logicalScale - scale;
if (sizeDiff < 0)
{
throw new AvroTypeException(
$@"Decimal Scale for value [{logicalValue}] is equal to [{scale}]. This exceeds default setting [{logicalScale}].
if (_numberHandling == AvroNumberHandling.Strict)
{
throw new AvroTypeException(
$@"Decimal Scale for value [{logicalValue}] is equal to [{scale}]. This exceeds default setting [{logicalScale}].
Consider adding following attribute to your property:
[AvroDecimal(Precision = 28, Scale = {scale})]
");
}

if (_numberHandling == AvroNumberHandling.Truncate)
{
var multiplier = (decimal)Math.Pow(10, logicalScale);
var truncatedValue = Math.Truncate((decimal)logicalValue * multiplier) / multiplier;

logicalValue = truncatedValue;
avroDecimal = new AvroDecimal(truncatedValue);
sizeDiff = 0;
}
else if (_numberHandling == AvroNumberHandling.Rounding)
{
var roundedValue = Math.Round((decimal)logicalValue, logicalScale);

logicalValue = roundedValue;
avroDecimal = new AvroDecimal(roundedValue);
sizeDiff = 0;
}
}

string trailingZeros = new string('0', sizeDiff);
Expand Down
2 changes: 2 additions & 0 deletions src/AvroConvert/AvroObjectServices/Write/WriteResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal partial class WriteResolver
private static bool _hasCustomConverters;
private readonly Dictionary<Type, Action<object, IWriter>> _customSerializerMapping;
private readonly IAvroNamingPolicy _namingPolicy;
private readonly AvroNumberHandling _numberHandling;

internal WriteResolver(AvroConvertOptions options = null)
{
Expand All @@ -43,6 +44,7 @@ internal WriteResolver(AvroConvertOptions options = null)
x => x.TypeSchema.RuntimeType,
y => (Action<object, IWriter>)y.Serialize);
_namingPolicy = options?.NamingPolicy;
_numberHandling = options?.NumberHandling ?? AvroNumberHandling.Strict;
}

internal Encoder.WriteItem ResolveWriter(TypeSchema schema)
Expand Down
19 changes: 19 additions & 0 deletions src/AvroConvert/Policies/AvroNumberHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace SolTechnology.Avro.Policies;

public enum AvroNumberHandling
{
/// <summary>
/// Decimals that are too large for the defined scale are rejected.
/// </summary>
Strict,

/// <summary>
/// Decimals that are too large for the defined scale are truncated.
/// </summary>
Truncate,

/// <summary>
/// Decimals that are too large for the defined scale are rounded.
/// </summary>
Rounding
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using Xunit;

namespace AvroConvertComponentTests.FullSerializationAndDeserialization;

public class NumberTests
{
[Theory]
[MemberData(nameof(TestEngine.Core), MemberType = typeof(TestEngine))]
public void Class_WithTooBigDecimal_ShouldThrow_ByDefault(Func<object, Type, dynamic> engine)
{
//Arrange
var record = new LogicalTypesClass();
record.One = 1.12345678901234567890m; // Default scale is 14

//Act
var exception = Record.Exception(() => engine.Invoke(record, typeof(LogicalTypesClass)));

//Assert
Assert.NotNull(exception);
}

[Theory]
[MemberData(nameof(TestEngine.HeadlessUsingNumberHandling), MemberType = typeof(TestEngine))]
public void Class_WithTooBigDecimal_ShouldTruncateOrRound_UsingHandling(Func<object, Type, dynamic> engine)
{
//Arrange
var record = new LogicalTypesClass();
record.One = 1.12345678901234567890m; // Default scale is 14

//Act
var deserialized = engine.Invoke(record, typeof(LogicalTypesClass));
var value = (decimal)deserialized.One;

//Assert
Assert.NotNull(deserialized);
Assert.Equal(14, GetScale(value));
Assert.True(value >= 1.12345678901234m && value <= 1.12345678901235m);
}

private int GetScale(decimal value)
{
if (value == 0)
{
return 0;
}

var bits = decimal.GetBits(value);

return (bits[3] >> 16) & 0x7F;
}
}
40 changes: 40 additions & 0 deletions tests/AvroConvertTests/TestEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using Newtonsoft.Json;
using SolTechnology.Avro;
using SolTechnology.Avro.Policies;

namespace AvroConvertComponentTests;

Expand Down Expand Up @@ -52,6 +53,13 @@ public static IEnumerable<object[]> CoreUsingSchema()
yield return BrotliWithSchema;
}

public static IEnumerable<object[]> HeadlessUsingNumberHandling()
{
yield return HeadlessWithTruncateNumberHandling;

yield return HeadlessWithRoundingNumberHandling;
}

public static IEnumerable<object[]> DefaultOnly()
{
yield return Default;
Expand Down Expand Up @@ -98,6 +106,38 @@ private static object[] Headless
}
}

private static object[] HeadlessWithTruncateNumberHandling
{
get
{
var headless = new Func<object, Type, dynamic>((input, type) =>
{
var schema = AvroConvert.GenerateSchema(type);
var options = new AvroConvertOptions { NumberHandling = AvroNumberHandling.Truncate };
var serialized = AvroConvert.SerializeHeadless(input, schema, options);
return AvroConvert.DeserializeHeadless(serialized, type);
});

return new object[] { headless };
}
}

private static object[] HeadlessWithRoundingNumberHandling
{
get
{
var headless = new Func<object, Type, dynamic>((input, type) =>
{
var schema = AvroConvert.GenerateSchema(type);
var options = new AvroConvertOptions { NumberHandling = AvroNumberHandling.Rounding };
var serialized = AvroConvert.SerializeHeadless(input, schema, options);
return AvroConvert.DeserializeHeadless(serialized, type);
});

return new object[] { headless };
}
}

private static object[] GenericJson
{
get
Expand Down
Loading