diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/G.CellValueConverter_ClassWithConverterOnComplexProperty#MyNamespace.MyGenRowContext.g.verified.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/G.CellValueConverter_ClassWithConverterOnComplexProperty#MyNamespace.MyGenRowContext.g.verified.cs new file mode 100644 index 0000000..42c928b --- /dev/null +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Snapshots/G.CellValueConverter_ClassWithConverterOnComplexProperty#MyNamespace.MyGenRowContext.g.verified.cs @@ -0,0 +1,135 @@ +//HintName: MyNamespace.MyGenRowContext.g.cs +// +#nullable enable +using SpreadCheetah; +using SpreadCheetah.SourceGeneration; +using SpreadCheetah.Styling; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MyNamespace +{ + public partial class MyGenRowContext + { + private static MyGenRowContext? _default; + public static MyGenRowContext Default => _default ??= new MyGenRowContext(); + + public MyGenRowContext() + { + } + private static readonly MyNamespace.ObjectConverter _valueConverter0 = new MyNamespace.ObjectConverter(); + private static readonly MyNamespace.StringConverter _valueConverter1 = new MyNamespace.StringConverter(); + + private WorksheetRowTypeInfo? _ClassWithoutParameterlessConstructor; + public WorksheetRowTypeInfo ClassWithoutParameterlessConstructor => _ClassWithoutParameterlessConstructor + ??= WorksheetRowMetadataServices.CreateObjectInfo( + AddHeaderRow0Async, AddAsRowAsync, AddRangeAsRowsAsync, null, CreateWorksheetRowDependencyInfo0); + + private static WorksheetRowDependencyInfo CreateWorksheetRowDependencyInfo0(Spreadsheet spreadsheet) + { + var styleIds = new[] + { + spreadsheet.GetStyleId("object style"), + }; + return new WorksheetRowDependencyInfo(styleIds); + } + + private static async ValueTask AddHeaderRow0Async(SpreadCheetah.Spreadsheet spreadsheet, SpreadCheetah.Styling.StyleId? styleId, CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(4); + try + { + cells[0] = new StyledCell("Value", styleId); + cells[1] = new StyledCell("Value1", styleId); + cells[2] = new StyledCell("StringProp", styleId); + cells[3] = new StyledCell("StringProp1", styleId); + await spreadsheet.AddRowAsync(cells.AsMemory(0, 4), token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static ValueTask AddAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, MyNamespace.ClassWithoutParameterlessConstructor? obj, CancellationToken token) + { + if (spreadsheet is null) + throw new ArgumentNullException(nameof(spreadsheet)); + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + return AddAsRowInternalAsync(spreadsheet, obj, token); + } + + private static ValueTask AddRangeAsRowsAsync(SpreadCheetah.Spreadsheet spreadsheet, + IEnumerable objs, + CancellationToken token) + { + if (spreadsheet is null) + throw new ArgumentNullException(nameof(spreadsheet)); + if (objs is null) + throw new ArgumentNullException(nameof(objs)); + return AddRangeAsRowsInternalAsync(spreadsheet, objs, token); + } + + private static async ValueTask AddAsRowInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, + MyNamespace.ClassWithoutParameterlessConstructor obj, + CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(4); + try + { + var worksheetRowDependencyInfo = spreadsheet.GetOrCreateWorksheetRowDependencyInfo(Default.ClassWithoutParameterlessConstructor); + var styleIds = worksheetRowDependencyInfo.StyleIds; + await AddCellsAsRowAsync(spreadsheet, obj, cells, styleIds, token).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static async ValueTask AddRangeAsRowsInternalAsync(SpreadCheetah.Spreadsheet spreadsheet, + IEnumerable objs, + CancellationToken token) + { + var cells = ArrayPool.Shared.Rent(4); + try + { + var worksheetRowDependencyInfo = spreadsheet.GetOrCreateWorksheetRowDependencyInfo(Default.ClassWithoutParameterlessConstructor); + var styleIds = worksheetRowDependencyInfo.StyleIds; + foreach (var obj in objs) + { + await AddCellsAsRowAsync(spreadsheet, obj, cells, styleIds, token).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(cells, true); + } + } + + private static ValueTask AddCellsAsRowAsync(SpreadCheetah.Spreadsheet spreadsheet, + MyNamespace.ClassWithoutParameterlessConstructor? obj, + StyledCell[] cells, IReadOnlyList styleIds, CancellationToken token) + { + if (obj is null) + return spreadsheet.AddRowAsync(ReadOnlyMemory.Empty, token); + + cells[0] = new StyledCell(_valueConverter0.ConvertToDataCell(obj.Value), null); + cells[1] = new StyledCell(_valueConverter0.ConvertToDataCell(obj.Value1), styleIds[0]); + cells[2] = new StyledCell(new DataCell(obj.StringProp), null); + cells[3] = new StyledCell(_valueConverter1.ConvertToDataCell(obj.StringProp1), null); + return spreadsheet.AddRowAsync(cells.AsMemory(0, 4), token); + } + + private static DataCell ConstructTruncatedDataCell(string? value, int truncateLength) + { + return value is null || value.Length <= truncateLength + ? new DataCell(value) + : new DataCell(value.AsMemory(0, truncateLength)); + } + } +} diff --git a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/CellValueConverterTests.cs b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/CellValueConverterTests.cs index def593e..223ee72 100644 --- a/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/CellValueConverterTests.cs +++ b/SpreadCheetah.SourceGenerator.SnapshotTest/Tests/CellValueConverterTests.cs @@ -256,4 +256,45 @@ public partial class MyGenRowContext : WorksheetRowContext; // Act & Assert return TestHelper.CompileAndVerify(source, onlyDiagnostics: true); } + + [Fact] + public Task CellValueConverter_ClassWithConverterOnComplexProperty() + { + // Arrange + const string source = """ + using SpreadCheetah.SourceGeneration; + + namespace MyNamespace; + public class ClassWithoutParameterlessConstructor + { + [CellValueConverter(typeof(ObjectConverter))] + public object? Value { get; set; } + + [CellValueConverter(typeof(ObjectConverter))] + [CellStyle("object style")] + public object? Value1 { get; set; } + + public string StringProp { get; set; } + + [CellValueConverter(typeof(StringConverter))] + public string StringProp1 { get; set; } + } + + internal class ObjectConverter : CellValueConverter + { + public override DataCell ConvertToCell(object value) => new(value.ToString()); + } + + internal class StringConverter : CellValueConverter + { + public override DataCell ConvertToCell(string value) => new(value); + } + + [WorksheetRow(typeof(ClassWithoutParameterlessConstructor))] + public partial class MyGenRowContext : WorksheetRowContext; + """; + + // Act & Assert + return TestHelper.CompileAndVerify(source); + } } \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/CellValueConverterContext.cs b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/CellValueConverterContext.cs index 3f02a39..ac401e9 100644 --- a/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/CellValueConverterContext.cs +++ b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/CellValueConverterContext.cs @@ -6,4 +6,5 @@ namespace SpreadCheetah.SourceGenerator.Test.Models.CellValueConverter; [WorksheetRow(typeof(ClassWithCellValueConverterAndCellStyle))] [WorksheetRow(typeof(ClassWithGenericConverter))] [WorksheetRow(typeof(ClassWithReusedConverter))] +[WorksheetRow(typeof(ClassWithCellValueConverterOnCustomType))] internal partial class CellValueConverterContext : WorksheetRowContext; \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/ClassWithCellValueConverterOnCustomType.cs b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/ClassWithCellValueConverterOnCustomType.cs new file mode 100644 index 0000000..4dd76f3 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/ClassWithCellValueConverterOnCustomType.cs @@ -0,0 +1,15 @@ +using SpreadCheetah.SourceGeneration; + +namespace SpreadCheetah.SourceGenerator.Test.Models.CellValueConverter; + +public class ClassWithCellValueConverterOnCustomType +{ + public string Property { get; init; } = null!; + + [CellValueConverter(typeof(NullToDashValueConverter))] + public object? ComplexProperty { get; init; } + + [CellStyle("PercentType")] + [CellValueConverter(typeof(PercentToNumberConverter))] + public Percent? PercentType { get; init; } +} \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/Percent.cs b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/Percent.cs new file mode 100644 index 0000000..22082ff --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/Percent.cs @@ -0,0 +1,3 @@ +namespace SpreadCheetah.SourceGenerator.Test.Models.CellValueConverter; + +public readonly record struct Percent(decimal Value); \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/PercentToNumberConverter.cs b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/PercentToNumberConverter.cs new file mode 100644 index 0000000..6ecbb38 --- /dev/null +++ b/SpreadCheetah.SourceGenerator.Test/Models/CellValueConverter/PercentToNumberConverter.cs @@ -0,0 +1,11 @@ +using SpreadCheetah.SourceGeneration; + +namespace SpreadCheetah.SourceGenerator.Test.Models.CellValueConverter; + +public class PercentToNumberConverter : CellValueConverter +{ + public override DataCell ConvertToDataCell(Percent? value) + { + return value is null ? new("-") : new(value.GetValueOrDefault().Value); + } +} \ No newline at end of file diff --git a/SpreadCheetah.SourceGenerator.Test/Tests/CellValueConverterTests.cs b/SpreadCheetah.SourceGenerator.Test/Tests/CellValueConverterTests.cs index 19e598c..0c50011 100644 --- a/SpreadCheetah.SourceGenerator.Test/Tests/CellValueConverterTests.cs +++ b/SpreadCheetah.SourceGenerator.Test/Tests/CellValueConverterTests.cs @@ -107,4 +107,32 @@ public async Task CellValueConverter_ClassWithConverterAndCellStyle() Assert.Equal("ABC123", sheet["A1"].StringValue); Assert.True(sheet["A1"].Style.Font.Bold); } + + [Fact] + public async Task CellValueConverter_ClassWithConverterOnCustomType() + { + // Arrange + using var stream = new MemoryStream(); + await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream); + await spreadsheet.StartWorksheetAsync("Sheet"); + var style = new Style { Font = { Bold = true } }; + spreadsheet.AddStyle(style, "PercentType"); + var obj = new ClassWithCellValueConverterOnCustomType + { + Property = "Abc123", ComplexProperty = null, PercentType = new Percent(123) + }; + + // Act + await spreadsheet.AddAsRowAsync(obj, CellValueConverterContext.Default.ClassWithCellValueConverterOnCustomType); + await spreadsheet.FinishAsync(); + + // Assert + using var sheet = SpreadsheetAssert.SingleSheet(stream); + Assert.Equal("Abc123", sheet["A1"].StringValue); + + Assert.Equal("-", sheet["B1"].StringValue); + + Assert.Equal(123, sheet["C1"].DecimalValue); + Assert.True(sheet["C1"].Style.Font.Bold); + } } diff --git a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs index dd6e602..f94b13e 100644 --- a/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs +++ b/SpreadCheetah.SourceGenerator/WorksheetRowGenerator.cs @@ -93,7 +93,7 @@ private static RowType AnalyzeTypeProperties(ITypeSymbol classType, LocationInfo if (property.IsWriteOnly || property.IsStatic || property.DeclaredAccessibility != Accessibility.Public) continue; - if (!property.Type.IsSupportedType()) + if (!property.Type.IsSupportedType() && !HasCellValueConverter(property)) { unsupportedPropertyTypeNames.Add(property.Type.Name); continue; @@ -169,6 +169,13 @@ private static IEnumerable GetClassAndBaseClassProperties(IType _ => throw new ArgumentOutOfRangeException(nameof(classType), "Unsupported inheritance strategy type") }; } + + + private static bool HasCellValueConverter(IPropertySymbol property) + { + return property.GetAttributes().Any(data => string.Equals(data.AttributeClass?.ToDisplayString(), + Attributes.CellValueConverter, StringComparison.Ordinal)); + } private static void Execute(ContextClass? contextClass, SourceProductionContext context) {