From 5a0db0188b6f60320b193c75780648df8f3ff0d2 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 10:54:19 -0500 Subject: [PATCH 01/17] feat(mappers): add helper methods for nested object mappings Introduced `HelperMethodEmitter` for generating reusable helper methods for nested object mappings: - `ToItem` and `FromItem` helper methods are now extracted into separate methods. - Updated `HelperMethodRegistry` to manage these reusable methods. - Enhanced `MapperClassInfo` to include helper methods for better organization. Updated test snapshots to reflect these changes. --- global.json | 2 +- .../Emitters/HelperMethodEmitter.cs | 210 ++++++ .../Emitters/MapperEmitter.cs | 78 +- .../Models/HelperMethodInfo.cs | 17 + .../Models/MapperClassInfo.cs | 107 ++- .../Models/MapperInfo.cs | 33 +- .../Models/ModelClassInfo.cs | 177 ++--- .../Models/PropertyInfo.cs | 54 +- .../PropertyMapping/HelperMethodRegistry.cs | 87 +++ .../PropertyMappingCodeRenderer.cs | 684 ++++++++++-------- .../Templates/Mapper.scriban | 9 + ...dSucceed#ExampleEntityMapper.g.verified.cs | 22 +- ...thOptIn_Succeeds#OrderMapper.g.verified.cs | 24 +- ...yOfNestedObjects#OrderMapper.g.verified.cs | 24 +- ...Objects_Inline#CatalogMapper.g.verified.cs | 24 +- ...edObjects_Inline#OrderMapper.g.verified.cs | 26 +- ...ngScalarTypes#EventLogMapper.g.verified.cs | 28 +- ...Objects_Inline#CatalogMapper.g.verified.cs | 24 +- ...tOfNestedObjects#OrderMapper.g.verified.cs | 24 +- ...edObjects_Inline#OrderMapper.g.verified.cs | 24 +- ...allsBackToInline#OrderMapper.g.verified.cs | 26 +- ...bject_MultiLevel#OrderMapper.g.verified.cs | 26 +- ...t_NullableInline#OrderMapper.g.verified.cs | 24 +- ...ect_SimpleInline#OrderMapper.g.verified.cs | 26 +- ...NotationOverride#OrderMapper.g.verified.cs | 26 +- ...ithScalarTypes#ProductMapper.g.verified.cs | 28 +- ...pes.DotNet10_0#ProductMapper.g.verified.cs | 64 ++ 27 files changed, 1349 insertions(+), 549 deletions(-) create mode 100644 src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs create mode 100644 src/LayeredCraft.DynamoMapper.Generators/Models/HelperMethodInfo.cs create mode 100644 src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes.DotNet10_0#ProductMapper.g.verified.cs diff --git a/global.json b/global.json index eb470ca..4ea6570 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMinor", - "version": "10.0.102" + "version": "10.0.101" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs new file mode 100644 index 0000000..d908c1e --- /dev/null +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -0,0 +1,210 @@ +using System.Text; +using DynamoMapper.Generator.Models; +using DynamoMapper.Generator.PropertyMapping; + +namespace DynamoMapper.Generator.Emitters; + +/// Generates helper method code for nested object mapping. +internal static class HelperMethodEmitter +{ + /// Renders a ToItem helper method. + public static string RenderToItemHelper(HelperMethodInfo helper, GeneratorContext context) + { + var sb = new StringBuilder(); + var typeName = ExtractSimpleTypeName(helper.ModelFullyQualifiedType); + var paramName = typeName.ToLowerInvariant(); + + // Method signature with block syntax (no leading spaces - template handles base + // indentation) + sb.AppendLine( + $"private static Dictionary {helper.MethodName}({helper.ModelFullyQualifiedType} {paramName})" + ); + sb.AppendLine("{"); + sb.Append(" return "); + + // Reuse existing RenderInlineNestedToItem logic + var bodyCode = + PropertyMappingCodeRenderer.RenderInlineNestedToItem( + paramName, + helper.InlineInfo, + context + ); + + // Format chained method calls on separate lines + var formattedBody = FormatToItemChainedCalls(bodyCode); + sb.Append(formattedBody); + sb.AppendLine(";"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + /// Renders a FromItem helper method. + public static string RenderFromItemHelper(HelperMethodInfo helper, GeneratorContext context) + { + var sb = new StringBuilder(); + var mapParamName = "map"; + + // Method signature with block syntax (no leading spaces - template handles base + // indentation) + sb.AppendLine( + $"private static {helper.ModelFullyQualifiedType} {helper.MethodName}(Dictionary {mapParamName})" + ); + sb.AppendLine("{"); + sb.Append(" return "); + + // Reuse existing RenderInlineNestedFromItem logic + var bodyCode = + PropertyMappingCodeRenderer.RenderInlineNestedFromItem( + mapParamName, + helper.InlineInfo, + context + ); + + // Format object initializer on separate lines + var formattedBody = FormatFromItemObjectInitializer(bodyCode); + sb.Append(formattedBody); + sb.AppendLine(";"); + sb.AppendLine("}"); + + return sb.ToString(); + } + + /// + /// Formats ToItem chained method calls on separate lines. Input: new Dictionary<string, + /// AttributeValue>().SetString(...).SetDecimal(...) Output: new Dictionary<string, + /// AttributeValue>() .SetString(...) .SetDecimal(...) + /// + private static string FormatToItemChainedCalls(string bodyCode) + { + var sb = new StringBuilder(); + sb.Append("new Dictionary()"); + + // Find each .Set* method call and put it on a new line + var startIndex = bodyCode.IndexOf(".Set", StringComparison.Ordinal); + + while (startIndex >= 0) + { + // Find the matching closing parenthesis for this method call + var i = startIndex + 1; // skip the '.' + + // Find the opening parenthesis + while (i < bodyCode.Length && bodyCode[i] != '(') + i++; + + if (i >= bodyCode.Length) + break; + + i++; // skip the '(' + var parenCount = 1; + + // Find the matching closing parenthesis + while (i < bodyCode.Length && parenCount > 0) + { + if (bodyCode[i] == '(') + parenCount++; + else if (bodyCode[i] == ')') + parenCount--; + i++; + } + + // Extract the method call + var methodCall = bodyCode.Substring(startIndex, i - startIndex); + sb.AppendLine(); + sb.Append(" "); + sb.Append(methodCall); + + // Find the next .Set call + startIndex = bodyCode.IndexOf(".Set", i, StringComparison.Ordinal); + } + + return sb.ToString(); + } + + /// + /// Formats FromItem object initializer on separate lines. Input: new MyType { Prop1 = val1, + /// Prop2 = val2, } Output: new MyType { Prop1 = val1, Prop2 = val2, } + /// + private static string FormatFromItemObjectInitializer(string bodyCode) + { + // Find the opening brace + var braceIndex = bodyCode.IndexOf('{'); + if (braceIndex < 0) + return bodyCode; // No object initializer, return as-is + + var sb = new StringBuilder(); + + // Type name before the brace + var typePart = bodyCode.Substring(0, braceIndex).TrimEnd(); + sb.AppendLine(typePart); + sb.AppendLine(" {"); + + // Extract the properties inside the braces + var propsStart = braceIndex + 1; + var propsEnd = bodyCode.LastIndexOf('}'); + if (propsEnd < 0) + return bodyCode; // Malformed, return as-is + + var propsContent = bodyCode.Substring(propsStart, propsEnd - propsStart).Trim(); + + // Split on commas, but need to be careful about nested commas + var properties = SplitPropertiesRespectingNesting(propsContent); + + for (var i = 0; i < properties.Length; i++) + { + var trimmed = properties[i].Trim(); + if (!string.IsNullOrEmpty(trimmed)) + { + sb.Append(" "); + sb.Append(trimmed); + // Add comma after each property (including the last one for trailing comma style) + if (!trimmed.EndsWith(",")) + sb.Append(","); + sb.AppendLine(); + } + } + + sb.Append(" }"); + + return sb.ToString(); + } + + /// Splits property assignments by comma, respecting nested parentheses. + private static string[] SplitPropertiesRespectingNesting(string content) + { + var result = new List(); + var current = new StringBuilder(); + var depth = 0; + + for (var i = 0; i < content.Length; i++) + { + var ch = content[i]; + + if (ch == '(' || ch == '<') + { + depth++; + } + else if (ch == ')' || ch == '>') + { + depth--; + } + else if (ch == ',' && depth == 0) + { + result.Add(current.ToString()); + current.Clear(); + continue; + } + + current.Append(ch); + } + + if (current.Length > 0) + result.Add(current.ToString()); + + return result.ToArray(); + } + + private static string ExtractSimpleTypeName(string fullyQualifiedType) => + // "global::MyNamespace.Address" -> "address" + fullyQualifiedType.Split('.').Last().ToLowerInvariant(); +} diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs index e825b61..1cae88d 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs @@ -26,34 +26,56 @@ private static string GeneratedCodeAttribute internal static void Generate(SourceProductionContext context, MapperInfo mapperInfo) { - var toAssignments = mapperInfo - .ModelClass!.Properties.Where(p => !string.IsNullOrEmpty(p.ToAssignments)) - .Select(p => p.ToAssignments) - .ToArray(); - - var fromAssignments = mapperInfo - .ModelClass!.Properties.Where(p => !string.IsNullOrEmpty(p.FromAssignment)) - .Select(p => p.FromAssignment) - .ToArray(); - - var fromInitAssignments = mapperInfo - .ModelClass!.Properties.Where(p => !string.IsNullOrEmpty(p.FromInitAssignment)) - .Select(p => p.FromInitAssignment) - .ToArray(); - - var model = new - { - GeneratedCodeAttribute, - mapperInfo.MapperClass, - ModelClass = mapperInfo.ModelClass!, - ModelVarName = mapperInfo.ModelClass.VarName, - ToAssignments = toAssignments, - FromAssignments = fromAssignments, - FromInitAssignments = fromInitAssignments, - MapperClassNamespace = mapperInfo.MapperClass?.Namespace, - MapperClassSignature = mapperInfo.MapperClass?.ClassSignature, - DictionaryCapacity = toAssignments.Length, - }; + var toAssignments = + mapperInfo.ModelClass!.Properties.Where(p => !string.IsNullOrEmpty(p.ToAssignments)) + .Select(p => p.ToAssignments) + .ToArray(); + + var fromAssignments = + mapperInfo.ModelClass!.Properties.Where(p => !string.IsNullOrEmpty(p.FromAssignment)) + .Select(p => p.FromAssignment) + .ToArray(); + + var fromInitAssignments = + mapperInfo.ModelClass!.Properties + .Where(p => !string.IsNullOrEmpty(p.FromInitAssignment)) + .Select(p => p.FromInitAssignment) + .ToArray(); + + // Render helper methods for nested objects + var helperMethods = Array.Empty(); + if (mapperInfo.MapperClass is not null && mapperInfo.Context is not null && + mapperInfo.MapperClass.HelperMethods.Any()) + helperMethods = + mapperInfo.MapperClass.HelperMethods.Select( + helper => + helper.Direction == HelperMethodDirection.ToItem + ? HelperMethodEmitter.RenderToItemHelper( + helper, + mapperInfo.Context! + ) + : HelperMethodEmitter.RenderFromItemHelper( + helper, + mapperInfo.Context! + ) + ) + .ToArray(); + + var model = + new + { + GeneratedCodeAttribute, + mapperInfo.MapperClass, + ModelClass = mapperInfo.ModelClass!, + ModelVarName = mapperInfo.ModelClass.VarName, + ToAssignments = toAssignments, + FromAssignments = fromAssignments, + FromInitAssignments = fromInitAssignments, + MapperClassNamespace = mapperInfo.MapperClass?.Namespace, + MapperClassSignature = mapperInfo.MapperClass?.ClassSignature, + DictionaryCapacity = toAssignments.Length, + HelperMethods = helperMethods, + }; var outputCode = TemplateHelper.Render("Templates.Mapper.scriban", model); diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/HelperMethodInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/HelperMethodInfo.cs new file mode 100644 index 0000000..1a7ed71 --- /dev/null +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/HelperMethodInfo.cs @@ -0,0 +1,17 @@ +using DynamoMapper.Generator.PropertyMapping.Models; + +namespace DynamoMapper.Generator.Models; + +/// Represents a helper method that maps a nested type to/from AttributeValue dictionary. +internal sealed record HelperMethodInfo( + string MethodName, // e.g., "ToItem_Address" + string ModelFullyQualifiedType, // e.g., "global::MyNamespace.Address" + NestedInlineInfo InlineInfo, // Property specs for the nested type + HelperMethodDirection Direction // ToItem or FromItem +); + +internal enum HelperMethodDirection +{ + ToItem, + FromItem, +} diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperClassInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperClassInfo.cs index 700bbf7..217670d 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperClassInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperClassInfo.cs @@ -1,4 +1,5 @@ using DynamoMapper.Generator.Diagnostics; +using LayeredCraft.SourceGeneratorTools.Types; using Microsoft.CodeAnalysis; using WellKnownType = DynamoMapper.Generator.WellKnownTypes.WellKnownTypeData.WellKnownType; @@ -11,7 +12,8 @@ internal sealed record MapperClassInfo( string? ToItemSignature, string? FromItemSignature, string? FromItemParameterName, - LocationInfo? Location + LocationInfo? Location, + EquatableArray HelperMethods ); internal static class MapperClassInfoExtensions @@ -22,8 +24,7 @@ internal static class MapperClassInfoExtensions extension(MapperClassInfo) { internal static DiagnosticResult<(MapperClassInfo, ITypeSymbol)> CreateAndResolveModelType( - INamedTypeSymbol classSymbol, - GeneratorContext context + INamedTypeSymbol classSymbol, GeneratorContext context ) { context.ThrowIfCancellationRequested(); @@ -47,46 +48,38 @@ GeneratorContext context // If there's an error in POCO type matching, propagate it return EnsurePocoTypesMatch(toItemMethod, fromItemMethod, classSymbol) - .Bind(modelType => - { - var classSignature = GetClassSignature(classSymbol); - var toItemSignature = toItemMethod?.Map(GetMethodSignature); - var fromItemSignature = fromItemMethod?.Map(GetMethodSignature); - var namespaceStatement = classSymbol.ContainingNamespace - is { IsGlobalNamespace: false } ns - ? $"namespace {ns.ToDisplayString()};" - : string.Empty; - - toItemMethod - ?.Parameters.FirstOrDefault() - ?.Name.Tap(name => context.MapperOptions.ToMethodParameterName = name); - fromItemMethod - ?.Parameters.FirstOrDefault() - ?.Name.Tap(name => context.MapperOptions.FromMethodParameterName = name); - - var fromItemParameterName = fromItemMethod?.Parameters.FirstOrDefault()?.Name; - - return DiagnosticResult<(MapperClassInfo, ITypeSymbol)>.Success( - ( - new MapperClassInfo( - classSymbol.Name, - namespaceStatement, - classSignature, - toItemSignature, - fromItemSignature, - fromItemParameterName, - context.TargetNode.CreateLocationInfo() - ), - modelType - ) - ); - }); + .Bind( + modelType => + { + var classSignature = GetClassSignature(classSymbol); + var toItemSignature = toItemMethod?.Map(GetMethodSignature); + var fromItemSignature = fromItemMethod?.Map(GetMethodSignature); + var namespaceStatement = + classSymbol.ContainingNamespace is { IsGlobalNamespace: false } ns + ? $"namespace {ns.ToDisplayString()};" + : string.Empty; + + toItemMethod?.Parameters.FirstOrDefault() + ?.Name.Tap(name => context.MapperOptions.ToMethodParameterName = name); + fromItemMethod?.Parameters.FirstOrDefault() + ?.Name.Tap( + name => context.MapperOptions.FromMethodParameterName = name + ); + + var fromItemParameterName = + fromItemMethod?.Parameters.FirstOrDefault()?.Name; + + return DiagnosticResult<(MapperClassInfo, ITypeSymbol)>.Success( + (new MapperClassInfo(classSymbol.Name, namespaceStatement, classSignature, toItemSignature, fromItemSignature, fromItemParameterName, context.TargetNode.CreateLocationInfo(), new EquatableArray()), + modelType) + ); + } + ); } } private static DiagnosticResult EnsurePocoTypesMatch( - IMethodSymbol? toItemMethod, - IMethodSymbol? fromItemMethod, + IMethodSymbol? toItemMethod, IMethodSymbol? fromItemMethod, INamedTypeSymbol mapperClassSymbol ) { @@ -100,11 +93,8 @@ INamedTypeSymbol mapperClassSymbol var toItemPocoType = toItemMethod?.Parameters[0].Type; var fromItemPocoType = fromItemMethod?.ReturnType; - if ( - toItemPocoType is not null - && fromItemPocoType is not null - && !SymbolEqualityComparer.Default.Equals(toItemPocoType, fromItemPocoType) - ) + if (toItemPocoType is not null && fromItemPocoType is not null && + !SymbolEqualityComparer.Default.Equals(toItemPocoType, fromItemPocoType)) return DiagnosticResult.Failure( DiagnosticDescriptors.MismatchedPocoTypes, toItemMethod?.CreateLocationInfo(), @@ -125,10 +115,10 @@ toItemPocoType is not null /// /// private static bool IsToMethod(IMethodSymbol method, GeneratorContext context) => - method.Name.StartsWith(ToMethodPrefix) - && method - is { IsPartialDefinition: true, PartialImplementationPart: null, Parameters.Length: 1 } - && IsAttributeValueDictionary(method.ReturnType, context); + method.Name.StartsWith(ToMethodPrefix) && method is + { + IsPartialDefinition: true, PartialImplementationPart: null, Parameters.Length: 1, + } && IsAttributeValueDictionary(method.ReturnType, context); /// /// From method must be: @@ -138,20 +128,18 @@ private static bool IsToMethod(IMethodSymbol method, GeneratorContext context) = /// /// private static bool IsFromMethod(IMethodSymbol method, GeneratorContext context) => - method.Name.StartsWith(FromMethodPrefix) - && method - is { IsPartialDefinition: true, PartialImplementationPart: null, Parameters.Length: 1 } - && IsAttributeValueDictionary(method.Parameters[0].Type, context); + method.Name.StartsWith(FromMethodPrefix) && method is + { + IsPartialDefinition: true, PartialImplementationPart: null, Parameters.Length: 1, + } && IsAttributeValueDictionary(method.Parameters[0].Type, context); private static bool IsAttributeValueDictionary(ITypeSymbol type, GeneratorContext context) => - type is INamedTypeSymbol { IsGenericType: true } namedType - && context.WellKnownTypes.IsType( + type is INamedTypeSymbol { IsGenericType: true } namedType && context.WellKnownTypes.IsType( namedType.ConstructedFrom, WellKnownType.System_Collections_Generic_Dictionary_2 - ) - && namedType.TypeArguments.Length == 2 - && context.WellKnownTypes.IsType(namedType.TypeArguments[0], WellKnownType.System_String) - && context.WellKnownTypes.IsType( + ) && namedType.TypeArguments.Length == 2 && + context.WellKnownTypes.IsType(namedType.TypeArguments[0], WellKnownType.System_String) && + context.WellKnownTypes.IsType( namedType.TypeArguments[1], WellKnownType.Amazon_DynamoDBv2_Model_AttributeValue ); @@ -174,6 +162,7 @@ private static string GetMethodSignature(IMethodSymbol method) var modifiers = method.IsStatic ? "static " : string.Empty; var extensionMethod = method.IsExtensionMethod ? "this " : string.Empty; - return $"{accessibility} {modifiers}partial {returnType} {method.Name}({extensionMethod}{parameterType} {parameterName})"; + return + $"{accessibility} {modifiers}partial {returnType} {method.Name}({extensionMethod}{parameterType} {parameterName})"; } } diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs index 6dc394d..24283f6 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs @@ -7,7 +7,8 @@ namespace DynamoMapper.Generator.Models; internal sealed record MapperInfo( MapperClassInfo? MapperClass, ModelClassInfo? ModelClass, - EquatableArray Diagnostics + EquatableArray Diagnostics, + GeneratorContext? Context ); internal static class MapperInfoExtensions @@ -23,7 +24,7 @@ internal static MapperInfo Create(INamedTypeSymbol classSymbol, GeneratorContext // If there's an error creating the mapper class info, return a MapperInfo with the // error if (!mapperResult.IsSuccess) - return MapperInfo.CreateWithDiagnostics([mapperResult.Error!]); + return MapperInfo.CreateWithDiagnostics([mapperResult.Error!], context); var (mapperClassInfo, modelTypeSymbol) = mapperResult.Value; @@ -31,16 +32,25 @@ internal static MapperInfo Create(INamedTypeSymbol classSymbol, GeneratorContext context.HasToItemMethod = mapperClassInfo.ToItemSignature != null; context.HasFromItemMethod = mapperClassInfo.FromItemSignature != null; - var (modelClassInfo, diagnosticInfos) = ModelClassInfo.Create( - modelTypeSymbol, - mapperClassInfo.FromItemParameterName, - context - ); + var (modelClassInfo, diagnosticInfos, helperMethods) = + ModelClassInfo.Create( + modelTypeSymbol, + mapperClassInfo.FromItemParameterName, + context + ); + + // Add helper methods to mapper class info + var updatedMapperClassInfo = + mapperClassInfo with + { + HelperMethods = new EquatableArray(helperMethods), + }; return new MapperInfo( - mapperClassInfo, + updatedMapperClassInfo, modelClassInfo, - diagnosticInfos.ToEquatableArray() + diagnosticInfos.ToEquatableArray(), + context ); } @@ -48,7 +58,8 @@ internal static MapperInfo Create(INamedTypeSymbol classSymbol, GeneratorContext /// Creates a MapperInfo containing only error diagnostics. Used when an exception prevents /// normal analysis. /// - private static MapperInfo CreateWithDiagnostics(IEnumerable diagnostics) => - new(null, null, diagnostics.ToEquatableArray()); + private static MapperInfo CreateWithDiagnostics( + IEnumerable diagnostics, GeneratorContext? context = null + ) => new(null, null, diagnostics.ToEquatableArray(), context); } } diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs index abf3cb7..cba501a 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs @@ -18,10 +18,8 @@ internal static class ModelClassInfoExtensions { extension(ModelClassInfo) { - internal static (ModelClassInfo?, DiagnosticInfo[]) Create( - ITypeSymbol modelTypeSymbol, - string? fromItemParameterName, - GeneratorContext context + internal static (ModelClassInfo?, DiagnosticInfo[], HelperMethodInfo[]) Create( + ITypeSymbol modelTypeSymbol, string? fromItemParameterName, GeneratorContext context ) { context.ThrowIfCancellationRequested(); @@ -30,14 +28,11 @@ GeneratorContext context var varName = GetModelVarName(modelTypeSymbol, fromItemParameterName, context); - var constructorSelectionResult = SelectConstructorIfNeeded( - modelTypeSymbol, - properties, - context - ); + var constructorSelectionResult = + SelectConstructorIfNeeded(modelTypeSymbol, properties, context); if (!constructorSelectionResult.IsSuccess) - return (null, [constructorSelectionResult.Error!]); + return (null, [constructorSelectionResult.Error!], []); var selectedConstructor = constructorSelectionResult.Value; @@ -47,39 +42,47 @@ GeneratorContext context // Validate dot-notation paths in field options var dotNotationDiagnostics = ValidateDotNotationPaths(modelTypeSymbol, context); if (dotNotationDiagnostics.Length > 0) - return (null, dotNotationDiagnostics); - - var (propertyInfos, propertyInfosByIndex, propertyDiagnostics) = CreatePropertyInfos( - properties, - varName, - selectedConstructor, - context - ); + return (null, dotNotationDiagnostics, []); + + // Create helper registry for tracking nested object helper methods + var helperRegistry = new HelperMethodRegistry(); + + var (propertyInfos, propertyInfosByIndex, propertyDiagnostics) = + CreatePropertyInfos( + properties, + varName, + selectedConstructor, + context, + helperRegistry + ); - var constructorInfo = selectedConstructor is null - ? null - : CreateConstructorInfo(selectedConstructor, properties, propertyInfosByIndex); + var constructorInfo = + selectedConstructor is null + ? null + : CreateConstructorInfo(selectedConstructor, properties, propertyInfosByIndex); + + var modelClassInfo = + new ModelClassInfo( + modelTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + varName, + new EquatableArray(propertyInfos), + constructorInfo + ); - var modelClassInfo = new ModelClassInfo( - modelTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - varName, - new EquatableArray(propertyInfos), - constructorInfo - ); + // Extract helper methods from the registry + var helperMethods = helperRegistry.GetAllHelpers(); - return (modelClassInfo, propertyDiagnostics); + return (modelClassInfo, propertyDiagnostics, helperMethods); } } private static IPropertySymbol[] GetModelProperties( - ITypeSymbol modelTypeSymbol, - GeneratorContext context + ITypeSymbol modelTypeSymbol, GeneratorContext context ) { if (modelTypeSymbol is not INamedTypeSymbol namedType) { - return modelTypeSymbol - .GetMembers() + return modelTypeSymbol.GetMembers() .OfType() .Where(p => IsMappableProperty(p, modelTypeSymbol)) .ToArray(); @@ -96,9 +99,7 @@ private static bool IsMappableProperty(IPropertySymbol property, ITypeSymbol dec !property.IsStatic && !(declaringType.IsRecord && property.Name == "EqualityContract"); private static string GetModelVarName( - ITypeSymbol modelTypeSymbol, - string? fromItemParameterName, - GeneratorContext context + ITypeSymbol modelTypeSymbol, string? fromItemParameterName, GeneratorContext context ) { var varName = context.MapperOptions.KeyNamingConventionConverter(modelTypeSymbol.Name); @@ -106,32 +107,26 @@ GeneratorContext context } private static DiagnosticResult SelectConstructorIfNeeded( - ITypeSymbol modelTypeSymbol, - IPropertySymbol[] properties, - GeneratorContext context - ) => - context.HasFromItemMethod - ? ConstructorSelector.Select(modelTypeSymbol, properties, context) - : DiagnosticResult.Success(null); - - private static ( - PropertyInfo[] PropertyInfos, - PropertyInfo?[] PropertyInfosByIndex, - DiagnosticInfo[] Diagnostics - ) CreatePropertyInfos( - IPropertySymbol[] properties, - string modelVarName, - ConstructorSelectionResult? selectedConstructor, - GeneratorContext context - ) + ITypeSymbol modelTypeSymbol, IPropertySymbol[] properties, GeneratorContext context + ) => context.HasFromItemMethod + ? ConstructorSelector.Select(modelTypeSymbol, properties, context) + : DiagnosticResult.Success(null); + + private static ( PropertyInfo[] PropertyInfos, PropertyInfo?[] PropertyInfosByIndex, + DiagnosticInfo[] Diagnostics ) CreatePropertyInfos( + IPropertySymbol[] properties, string modelVarName, + ConstructorSelectionResult? selectedConstructor, GeneratorContext context, + HelperMethodRegistry helperRegistry + ) { - var initMethodsByPropertyName = selectedConstructor is null - ? null - : selectedConstructor.PropertyModes.ToDictionary( - pm => pm.PropertyName, - pm => pm.Method, - StringComparer.Ordinal - ); + var initMethodsByPropertyName = + selectedConstructor is null + ? null + : selectedConstructor.PropertyModes.ToDictionary( + pm => pm.PropertyName, + pm => pm.Method, + StringComparer.Ordinal + ); var propertyDiagnosticsList = new List(); var propertyInfosList = new List(properties.Length); @@ -143,19 +138,12 @@ GeneratorContext context var property = properties[i]; var initMethod = InitializationMethod.InitSyntax; - if ( - initMethodsByPropertyName is not null - && initMethodsByPropertyName.TryGetValue(property.Name, out var initMethod2) - ) + if (initMethodsByPropertyName is not null && + initMethodsByPropertyName.TryGetValue(property.Name, out var initMethod2)) initMethod = initMethod2; - var propertyInfoResult = PropertyInfo.Create( - property, - modelVarName, - i, - initMethod, - context - ); + var propertyInfoResult = + PropertyInfo.Create(property, modelVarName, i, initMethod, context, helperRegistry); if (!propertyInfoResult.IsSuccess) { @@ -167,24 +155,21 @@ initMethodsByPropertyName is not null propertyInfosByIndex[i] = propertyInfoResult.Value!; } - return ( - propertyInfosList.ToArray(), - propertyInfosByIndex, - propertyDiagnosticsList.ToArray() - ); + return (propertyInfosList.ToArray(), propertyInfosByIndex, + propertyDiagnosticsList.ToArray()); } private static ConstructorInfo CreateConstructorInfo( - ConstructorSelectionResult selectedConstructor, - IPropertySymbol[] properties, + ConstructorSelectionResult selectedConstructor, IPropertySymbol[] properties, PropertyInfo?[] propertyInfosByIndex ) { var propertyIndexBySymbol = CreatePropertyIndexBySymbol(properties); - var parameterInfosList = new List( - selectedConstructor.Constructor.Constructor.Parameters.Length - ); + var parameterInfosList = + new List( + selectedConstructor.Constructor.Constructor.Parameters.Length + ); foreach (var paramAnalysis in selectedConstructor.Constructor.Parameters) { @@ -214,10 +199,8 @@ private static Dictionary CreatePropertyIndexBySymbol( IPropertySymbol[] properties ) { - var propertyIndexBySymbol = new Dictionary( - properties.Length, - SymbolEqualityComparer.Default - ); + var propertyIndexBySymbol = + new Dictionary(properties.Length, SymbolEqualityComparer.Default); for (var i = 0; i < properties.Length; i++) propertyIndexBySymbol[properties[i]] = i; @@ -230,8 +213,7 @@ IPropertySymbol[] properties /// refer to valid property paths on the model type. /// private static DiagnosticInfo[] ValidateDotNotationPaths( - ITypeSymbol modelTypeSymbol, - GeneratorContext context + ITypeSymbol modelTypeSymbol, GeneratorContext context ) { var diagnostics = new List(); @@ -265,9 +247,7 @@ GeneratorContext context /// Validates a dot-notation path against the model type hierarchy. /// private static DiagnosticInfo? ValidatePath( - string path, - ITypeSymbol rootType, - GeneratorContext context + string path, ITypeSymbol rootType, GeneratorContext context ) { var segments = path.Split('.'); @@ -278,11 +258,12 @@ GeneratorContext context var segment = segments[i]; // Find the property on the current type - var property = PropertySymbolLookup.FindPropertyByName( - currentType, - segment, - context.MapperOptions.IncludeBaseClassProperties - ); + var property = + PropertySymbolLookup.FindPropertyByName( + currentType, + segment, + context.MapperOptions.IncludeBaseClassProperties + ); if (property == null) { @@ -300,8 +281,10 @@ GeneratorContext context { // Unwrap nullable if needed var propertyType = property.Type; - if (propertyType is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableType - && nullableType.TypeArguments.Length == 1) + if (propertyType is INamedTypeSymbol + { + OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, + } nullableType && nullableType.TypeArguments.Length == 1) { propertyType = nullableType.TypeArguments[0]; } diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/PropertyInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/PropertyInfo.cs index 38d519a..c4a93fe 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/PropertyInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/PropertyInfo.cs @@ -26,38 +26,34 @@ internal static class PropertyInfoExtensions /// Short-circuits when strategy is null (property won't be used in any method). /// internal static DiagnosticResult Create( - IPropertySymbol propertySymbol, - string modelVarName, - int index, - InitializationMethod initMethod, - GeneratorContext context - ) => - PropertyAnalyzer - .Analyze(propertySymbol, context) - .Bind(analysis => - TypeMappingStrategyResolver - .Resolve(analysis, context) + IPropertySymbol propertySymbol, string modelVarName, int index, + InitializationMethod initMethod, GeneratorContext context, + HelperMethodRegistry helperRegistry + ) => PropertyAnalyzer.Analyze(propertySymbol, context) + .Bind( + analysis => + TypeMappingStrategyResolver.Resolve(analysis, context) .Map(strategy => (analysis, strategy)) - ) - .Bind(tuple => + ) + .Bind( + tuple => // Short-circuit if property won't be used AND has no custom methods - tuple.strategy - is null - && tuple.analysis.FieldOptions?.ToMethod is null - && tuple.analysis.FieldOptions?.FromMethod is null + tuple.strategy is null && tuple.analysis.FieldOptions?.ToMethod is null && + tuple.analysis.FieldOptions?.FromMethod is null ? PropertyInfo.None - : PropertyMappingSpecBuilder - .Build(tuple.analysis, tuple.strategy, context) - .Map(spec => - PropertyMappingCodeRenderer.Render( - spec, - tuple.analysis, - modelVarName, - index, - initMethod, - context - ) + : PropertyMappingSpecBuilder.Build(tuple.analysis, tuple.strategy, context) + .Map( + spec => + PropertyMappingCodeRenderer.Render( + spec, + tuple.analysis, + modelVarName, + index, + initMethod, + context, + helperRegistry + ) ) - ); + ); } } diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs new file mode 100644 index 0000000..599c29f --- /dev/null +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs @@ -0,0 +1,87 @@ +using DynamoMapper.Generator.Models; +using DynamoMapper.Generator.PropertyMapping.Models; + +namespace DynamoMapper.Generator.PropertyMapping; + +/// +/// Registry for tracking helper methods during mapper generation. Ensures that the same +/// nested type reuses the same helper method. +/// +internal sealed class HelperMethodRegistry +{ + private readonly Dictionary _toItemHelpers = new(); + private readonly Dictionary _fromItemHelpers = new(); + + /// + /// Gets or registers a ToItem helper method for a nested type. Returns the method name to use + /// in the call site. + /// + public string GetOrRegisterToItemHelper( + string modelFullyQualifiedType, NestedInlineInfo inlineInfo + ) + { + if (_toItemHelpers.TryGetValue(modelFullyQualifiedType, out var existing)) + return existing.MethodName; + + var methodName = GenerateToItemHelperName(modelFullyQualifiedType); + var helperInfo = + new HelperMethodInfo( + methodName, + modelFullyQualifiedType, + inlineInfo, + HelperMethodDirection.ToItem + ); + + _toItemHelpers[modelFullyQualifiedType] = helperInfo; + return methodName; + } + + /// + /// Gets or registers a FromItem helper method for a nested type. Returns the method name to + /// use in the call site. + /// + public string GetOrRegisterFromItemHelper( + string modelFullyQualifiedType, NestedInlineInfo inlineInfo + ) + { + if (_fromItemHelpers.TryGetValue(modelFullyQualifiedType, out var existing)) + return existing.MethodName; + + var methodName = GenerateFromItemHelperName(modelFullyQualifiedType); + var helperInfo = + new HelperMethodInfo( + methodName, + modelFullyQualifiedType, + inlineInfo, + HelperMethodDirection.FromItem + ); + + _fromItemHelpers[modelFullyQualifiedType] = helperInfo; + return methodName; + } + + /// Returns all registered helper methods for emission. + public HelperMethodInfo[] GetAllHelpers() => + _toItemHelpers.Values.Concat(_fromItemHelpers.Values).ToArray(); + + private static string GenerateToItemHelperName(string modelFullyQualifiedType) + { + // Extract simple type name from fully qualified name + // "global::MyNamespace.Address" -> "Address" + var typeName = modelFullyQualifiedType.Split('.').Last(); + + // Sanitize generic types: Result
-> Result_Address + typeName = typeName.Replace("<", "_").Replace(">", "").Replace(",", "_").Replace(" ", ""); + + return $"ToItem_{typeName}"; + } + + private static string GenerateFromItemHelperName(string modelFullyQualifiedType) + { + var typeName = modelFullyQualifiedType.Split('.').Last(); + + typeName = typeName.Replace("<", "_").Replace(">", "").Replace(",", "_").Replace(" ", ""); + + return $"FromItem_{typeName}"; + } +} diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs index 8f6b64a..ba9355e 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs @@ -15,37 +15,39 @@ internal static class PropertyMappingCodeRenderer { /// Renders a property mapping specification into PropertyInfo. internal static PropertyInfo Render( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - string modelVarName, - int index, - InitializationMethod initMethod, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, string modelVarName, int index, + InitializationMethod initMethod, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { // Check if this is a nested object - requires special handling - var isNestedObject = spec.TypeStrategy?.NestedMapping is not null - && spec.TypeStrategy.CollectionInfo is null; + var isNestedObject = + spec.TypeStrategy?.NestedMapping is not null && + spec.TypeStrategy.CollectionInfo is null; // Check if this is a nested collection (List or Dictionary) - var isNestedCollection = spec.TypeStrategy?.CollectionInfo?.ElementNestedMapping is not null; + var isNestedCollection = + spec.TypeStrategy?.CollectionInfo?.ElementNestedMapping is not null; // If this property is a constructor parameter, render as constructor argument if (initMethod == InitializationMethod.ConstructorParameter && context.HasFromItemMethod) { // Nested objects/collections in constructors are not yet supported - var constructorArg = spec.FromItemMethod is not null && !isNestedObject && !isNestedCollection - ? RenderConstructorArgument(spec, analysis, context) - : null; + var constructorArg = + spec.FromItemMethod is not null && !isNestedObject && !isNestedCollection + ? RenderConstructorArgument(spec, analysis, context) + : null; // Constructor parameters still need ToAssignments for serialization string? toAssignment = null; if (context.HasToItemMethod && analysis.HasGetter && spec.ToItemMethod is not null) { if (isNestedObject) - toAssignment = RenderNestedObjectToAssignment(spec, analysis, context); + toAssignment = + RenderNestedObjectToAssignment(spec, analysis, context, helperRegistry); else if (isNestedCollection) - toAssignment = RenderNestedCollectionToAssignment(spec, analysis, context); + toAssignment = + RenderNestedCollectionToAssignment(spec, analysis, context, helperRegistry); else toAssignment = RenderToAssignment(spec); } @@ -56,38 +58,47 @@ GeneratorContext context // Handle nested collections specially (List
, Dictionary) if (isNestedCollection) { - return RenderNestedCollectionProperty(spec, analysis, modelVarName, initMethod, context); + return RenderNestedCollectionProperty( + spec, + analysis, + modelVarName, + initMethod, + context, + helperRegistry + ); } // Handle nested objects specially if (isNestedObject) { - return RenderNestedObjectProperty(spec, analysis, modelVarName, initMethod, context); + return RenderNestedObjectProperty( + spec, + analysis, + modelVarName, + initMethod, + context, + helperRegistry + ); } // Determine if we should use regular assignment vs init assignment var useRegularAssignment = - spec.FromItemMethod is { IsCustomMethod: false } - && analysis is { IsRequired: false, IsInitOnly: false, HasDefaultValue: true }; + spec.FromItemMethod is { IsCustomMethod: false } && analysis is + { IsRequired: false, IsInitOnly: false, HasDefaultValue: true }; // Use init syntax for InitSyntax mode, regular assignment for PostConstruction var useInitSyntax = initMethod == InitializationMethod.InitSyntax; // FromItem requires both: setter on property AND FromItem method exists var fromAssignment = - context.HasFromItemMethod - && analysis.HasSetter - && spec.FromItemMethod is not null - && (!useInitSyntax || useRegularAssignment) + context.HasFromItemMethod && analysis.HasSetter && spec.FromItemMethod is not null && + (!useInitSyntax || useRegularAssignment) ? RenderFromAssignment(spec, modelVarName, analysis, index, context) : null; var fromInitAssignment = - context.HasFromItemMethod - && analysis.HasSetter - && spec.FromItemMethod is not null - && useInitSyntax - && !useRegularAssignment + context.HasFromItemMethod && analysis.HasSetter && spec.FromItemMethod is not null && + useInitSyntax && !useRegularAssignment ? RenderFromInitAssignment(spec, analysis, context) : null; @@ -104,11 +115,9 @@ GeneratorContext context /// Renders a nested object property into PropertyInfo. /// private static PropertyInfo RenderNestedObjectProperty( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - string modelVarName, - InitializationMethod initMethod, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, string modelVarName, + InitializationMethod initMethod, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { // Use init syntax for InitSyntax mode, regular assignment for PostConstruction @@ -116,25 +125,27 @@ GeneratorContext context // FromItem for nested objects var fromAssignment = - context.HasFromItemMethod - && analysis.HasSetter - && spec.FromItemMethod is not null - && !useInitSyntax - ? RenderNestedObjectFromAssignment(spec, modelVarName, analysis, context) + context.HasFromItemMethod && analysis.HasSetter && spec.FromItemMethod is not null && + !useInitSyntax + ? RenderNestedObjectFromAssignment( + spec, + modelVarName, + analysis, + context, + helperRegistry + ) : null; var fromInitAssignment = - context.HasFromItemMethod - && analysis.HasSetter - && spec.FromItemMethod is not null - && useInitSyntax - ? RenderNestedObjectFromInitAssignment(spec, analysis, context) + context.HasFromItemMethod && analysis.HasSetter && spec.FromItemMethod is not null && + useInitSyntax + ? RenderNestedObjectFromInitAssignment(spec, analysis, context, helperRegistry) : null; // ToItem for nested objects var toAssignments = context.HasToItemMethod && analysis.HasGetter && spec.ToItemMethod is not null - ? RenderNestedObjectToAssignment(spec, analysis, context) + ? RenderNestedObjectToAssignment(spec, analysis, context, helperRegistry) : null; return new PropertyInfo(fromAssignment, fromInitAssignment, toAssignments, null); @@ -146,9 +157,7 @@ GeneratorContext context /// custom methods /// private static string RenderFromInitAssignment( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, GeneratorContext context ) { Debug.Assert(spec.FromItemMethod is not null, "FromItemMethod should not be null"); @@ -159,9 +168,10 @@ GeneratorContext context var args = string.Join(", ", spec.FromItemMethod.Arguments.Select(a => a.Value)); - var methodCall = spec.FromItemMethod.IsCustomMethod - ? $"{spec.FromItemMethod.MethodName}({args})" // Custom: MethodName(item) - : $"{context.MapperOptions.FromMethodParameterName}.{spec.FromItemMethod.MethodName}{spec.TypeStrategy!.GenericArgument}({args})"; // Standard: item.GetXxx(args) + var methodCall = + spec.FromItemMethod.IsCustomMethod + ? $"{spec.FromItemMethod.MethodName}({args})" // Custom: MethodName(item) + : $"{context.MapperOptions.FromMethodParameterName}.{spec.FromItemMethod.MethodName}{spec.TypeStrategy!.GenericArgument}({args})"; // Standard: item.GetXxx(args) // For array properties, append .ToArray() to convert the List to an array // GetList returns List, but if the property is T[], we need to convert it @@ -175,10 +185,7 @@ GeneratorContext context } private static string RenderFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - PropertyAnalysis analysis, - int index, + PropertyMappingSpec spec, string modelVarName, PropertyAnalysis analysis, int index, GeneratorContext context ) { @@ -194,9 +201,9 @@ GeneratorContext context // For non-nullable enums, out comes first (after key), then defaultValue, then format // For DateTime/DateTimeOffset/TimeSpan, out comes before format var isNullableEnumWithFormat = - spec.TypeStrategy?.TypeName == "Enum" - && spec.TypeStrategy?.NullableModifier == "Nullable" - && argsList.Any(a => a.Contains("format:")); + spec.TypeStrategy?.TypeName == "Enum" && + spec.TypeStrategy?.NullableModifier == "Nullable" && + argsList.Any(a => a.Contains("format:")); var outPosition = isNullableEnumWithFormat ? 2 : 1; argsList.Insert(outPosition, $"out var var{index}"); @@ -211,7 +218,8 @@ GeneratorContext context var toArray = isArrayProperty && !spec.FromItemMethod.IsCustomMethod ? ".ToArray()" : string.Empty; - return $"if ({context.MapperOptions.FromMethodParameterName}.{methodCall}) {modelVarName}.{spec.PropertyName} = var{index}!{toArray};"; + return + $"if ({context.MapperOptions.FromMethodParameterName}.{methodCall}) {modelVarName}.{spec.PropertyName} = var{index}!{toArray};"; } /// @@ -228,10 +236,11 @@ private static string RenderToAssignment(PropertyMappingSpec spec) var args = string.Join(", ", spec.ToItemMethod.Arguments.Select(a => a.Value)); - var methodCall = spec.ToItemMethod.IsCustomMethod - ? $".{spec.ToItemMethod.MethodName}({args})" // Custom: .Set("key", - // CustomMethod(source)) - : $".{spec.ToItemMethod.MethodName}{spec.TypeStrategy!.GenericArgument}({args})"; // Standard: .SetXxx(args) + var methodCall = + spec.ToItemMethod.IsCustomMethod + ? $".{spec.ToItemMethod.MethodName}({args})" // Custom: .Set("key", + // CustomMethod(source)) + : $".{spec.ToItemMethod.MethodName}{spec.TypeStrategy!.GenericArgument}({args})"; // Standard: .SetXxx(args) return methodCall; } @@ -241,9 +250,7 @@ private static string RenderToAssignment(PropertyMappingSpec spec) /// without the "PropertyName = " prefix, as it's used as a constructor parameter value. /// private static string RenderConstructorArgument( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, GeneratorContext context ) { Debug.Assert(spec.FromItemMethod is not null, "FromItemMethod should not be null"); @@ -254,9 +261,10 @@ GeneratorContext context var args = string.Join(", ", spec.FromItemMethod.Arguments.Select(a => a.Value)); - var methodCall = spec.FromItemMethod.IsCustomMethod - ? $"{spec.FromItemMethod.MethodName}({args})" // Custom: MethodName(item) - : $"{context.MapperOptions.FromMethodParameterName}.{spec.FromItemMethod.MethodName}{spec.TypeStrategy!.GenericArgument}({args})"; // Standard: item.GetXxx(args) + var methodCall = + spec.FromItemMethod.IsCustomMethod + ? $"{spec.FromItemMethod.MethodName}({args})" // Custom: MethodName(item) + : $"{context.MapperOptions.FromMethodParameterName}.{spec.FromItemMethod.MethodName}{spec.TypeStrategy!.GenericArgument}({args})"; // Standard: item.GetXxx(args) // For array properties, append .ToArray() to convert the List to an array var isArrayProperty = analysis.PropertyType.TypeKind == TypeKind.Array; @@ -274,9 +282,8 @@ GeneratorContext context /// Renders the ToItem code for a nested object. /// private static string RenderNestedObjectToAssignment( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { var nestedMapping = spec.TypeStrategy!.NestedMapping!; @@ -286,12 +293,22 @@ GeneratorContext context return nestedMapping switch { MapperBasedNesting mapperBased => RenderMapperBasedToAssignment( - spec, paramName, isNullable, mapperBased.Mapper + spec, + paramName, + isNullable, + mapperBased.Mapper ), InlineNesting inline => RenderInlineToAssignment( - spec, paramName, isNullable, inline.Info, context + spec, + paramName, + isNullable, + inline.Info, + context, + helperRegistry + ), + _ => throw new InvalidOperationException( + $"Unknown nested mapping type: {nestedMapping.GetType()}" ), - _ => throw new InvalidOperationException($"Unknown nested mapping type: {nestedMapping.GetType()}") }; } @@ -300,52 +317,55 @@ GeneratorContext context /// Output: .Set("key", source.Prop is null ? new AttributeValue { NULL = true } : new AttributeValue { M = MapperName.ToItem(source.Prop) }) /// private static string RenderMapperBasedToAssignment( - PropertyMappingSpec spec, - string paramName, - bool isNullable, - MapperReference mapper + PropertyMappingSpec spec, string paramName, bool isNullable, MapperReference mapper ) { var propAccess = $"{paramName}.{spec.PropertyName}"; if (isNullable) { - return $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ M = {mapper.MapperFullyQualifiedName}.ToItem({propAccess}) }})"; + return + $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ M = {mapper.MapperFullyQualifiedName}.ToItem({propAccess}) }})"; } - return $".Set(\"{spec.Key}\", new AttributeValue {{ M = {mapper.MapperFullyQualifiedName}.ToItem({propAccess}) }})"; + return + $".Set(\"{spec.Key}\", new AttributeValue {{ M = {mapper.MapperFullyQualifiedName}.ToItem({propAccess}) }})"; } /// - /// Renders ToItem code for inline nested objects. - /// Output: .Set("key", source.Prop is null ? new AttributeValue { NULL = true } : new AttributeValue { M = new Dictionary...().SetXxx()... }) + /// Renders ToItem code for inline nested objects using helper methods. + /// Output: .Set("key", source.Prop is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Type(source.Prop) }) /// private static string RenderInlineToAssignment( - PropertyMappingSpec spec, - string paramName, - bool isNullable, - NestedInlineInfo inlineInfo, - GeneratorContext context + PropertyMappingSpec spec, string paramName, bool isNullable, NestedInlineInfo inlineInfo, + GeneratorContext context, HelperMethodRegistry helperRegistry ) { var propAccess = $"{paramName}.{spec.PropertyName}"; - var inlineCode = RenderInlineNestedToItem(propAccess, inlineInfo, context); + + // Register the helper method and get its name + var helperMethodName = + helperRegistry.GetOrRegisterToItemHelper( + inlineInfo.ModelFullyQualifiedType, + inlineInfo + ); if (isNullable) { - return $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ M = {inlineCode} }})"; + return + $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ M = {helperMethodName}({propAccess}) }})"; } - return $".Set(\"{spec.Key}\", new AttributeValue {{ M = {inlineCode} }})"; + return + $".Set(\"{spec.Key}\", new AttributeValue {{ M = {helperMethodName}({propAccess}) }})"; } /// /// Renders the inline Dictionary creation code for nested objects. + /// Made internal to allow reuse by HelperMethodEmitter. /// - private static string RenderInlineNestedToItem( - string sourcePrefix, - NestedInlineInfo inlineInfo, - GeneratorContext context + internal static string RenderInlineNestedToItem( + string sourcePrefix, NestedInlineInfo inlineInfo, GeneratorContext context ) { var sb = new StringBuilder(); @@ -357,15 +377,18 @@ GeneratorContext context { // Recursive nested object var nestedSourcePrefix = $"{sourcePrefix}.{prop.PropertyName}"; - var nestedCode = prop.NestedMapping switch - { - MapperBasedNesting mapperBased => - $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem({nestedSourcePrefix}) }}", - InlineNesting inline => - $"new AttributeValue {{ M = {RenderInlineNestedToItem(nestedSourcePrefix, inline.Info, context)} }}", - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; - sb.Append($".Set(\"{prop.DynamoKey}\", {nestedSourcePrefix} is null ? new AttributeValue {{ NULL = true }} : {nestedCode})"); + var nestedCode = + prop.NestedMapping switch + { + MapperBasedNesting mapperBased => + $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem({nestedSourcePrefix}) }}", + InlineNesting inline => + $"new AttributeValue {{ M = {RenderInlineNestedToItem(nestedSourcePrefix, inline.Info, context)} }}", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; + sb.Append( + $".Set(\"{prop.DynamoKey}\", {nestedSourcePrefix} is null ? new AttributeValue {{ NULL = true }} : {nestedCode})" + ); } else if (prop.Strategy is not null) { @@ -375,15 +398,19 @@ GeneratorContext context var propValue = $"{sourcePrefix}.{prop.PropertyName}"; // Build type-specific args - var typeArgs = prop.Strategy.ToTypeSpecificArgs.Length > 0 - ? ", " + string.Join(", ", prop.Strategy.ToTypeSpecificArgs) - : ""; + var typeArgs = + prop.Strategy.ToTypeSpecificArgs.Length > 0 + ? ", " + string.Join(", ", prop.Strategy.ToTypeSpecificArgs) + : ""; // Omit flags - use mapper defaults - var omitEmpty = context.MapperOptions.OmitEmptyStrings.ToString().ToLowerInvariant(); + var omitEmpty = + context.MapperOptions.OmitEmptyStrings.ToString().ToLowerInvariant(); var omitNull = context.MapperOptions.OmitNullStrings.ToString().ToLowerInvariant(); - sb.Append($".{setMethod}{genericArg}(\"{prop.DynamoKey}\", {propValue}{typeArgs}, {omitEmpty}, {omitNull})"); + sb.Append( + $".{setMethod}{genericArg}(\"{prop.DynamoKey}\", {propValue}{typeArgs}, {omitEmpty}, {omitNull})" + ); } } @@ -394,9 +421,8 @@ GeneratorContext context /// Renders the FromItem code for a nested object (init-style assignment). /// private static string RenderNestedObjectFromInitAssignment( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { var nestedMapping = spec.TypeStrategy!.NestedMapping!; @@ -408,23 +434,36 @@ GeneratorContext context return nestedMapping switch { MapperBasedNesting mapperBased => RenderMapperBasedFromInitAssignment( - spec, itemParam, mapperBased.Mapper, fallback + spec, + itemParam, + mapperBased.Mapper, + fallback ), InlineNesting inline => RenderInlineFromInitAssignment( - spec, itemParam, inline.Info, context, fallback + spec, + itemParam, + inline.Info, + context, + fallback, + helperRegistry + ), + _ => throw new InvalidOperationException( + $"Unknown nested mapping type: {nestedMapping.GetType()}" ), - _ => throw new InvalidOperationException($"Unknown nested mapping type: {nestedMapping.GetType()}") }; } /// /// Determines the fallback expression for a nested object when not found in DynamoDB. /// - private static string GetNestedObjectFallback(PropertyMappingSpec spec, PropertyAnalysis analysis) + private static string GetNestedObjectFallback( + PropertyMappingSpec spec, PropertyAnalysis analysis + ) { // If property has 'required' keyword, throw on missing if (analysis.IsRequired) - return $"throw new System.InvalidOperationException(\"Required attribute '{spec.Key}' not found.\")"; + return + $"throw new System.InvalidOperationException(\"Required attribute '{spec.Key}' not found.\")"; // For nullable types, return null if (analysis.Nullability.IsNullableType) @@ -440,40 +479,41 @@ private static string GetNestedObjectFallback(PropertyMappingSpec spec, Property /// Output: PropertyName = item.TryGetValue("key", out var attr) && attr.M is { } map ? MapperName.FromItem(map) : fallback, /// private static string RenderMapperBasedFromInitAssignment( - PropertyMappingSpec spec, - string itemParam, - MapperReference mapper, - string fallback + PropertyMappingSpec spec, string itemParam, MapperReference mapper, string fallback ) { var varName = spec.PropertyName.ToLowerInvariant(); - return $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map ? {mapper.MapperFullyQualifiedName}.FromItem({varName}Map) : {fallback},"; + return + $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map ? {mapper.MapperFullyQualifiedName}.FromItem({varName}Map) : {fallback},"; } /// /// Renders FromItem init-style code for inline nested objects. /// private static string RenderInlineFromInitAssignment( - PropertyMappingSpec spec, - string itemParam, - NestedInlineInfo inlineInfo, - GeneratorContext context, - string fallback + PropertyMappingSpec spec, string itemParam, NestedInlineInfo inlineInfo, + GeneratorContext context, string fallback, HelperMethodRegistry helperRegistry ) { var varName = spec.PropertyName.ToLowerInvariant(); - var initCode = RenderInlineNestedFromItem(varName + "Map", inlineInfo, context); - return $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map ? {initCode} : {fallback},"; + // Register the helper method and get its name + var helperMethodName = + helperRegistry.GetOrRegisterFromItemHelper( + inlineInfo.ModelFullyQualifiedType, + inlineInfo + ); + + return + $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map ? {helperMethodName}({varName}Map) : {fallback},"; } /// /// Renders the inline object initializer for nested objects. + /// Made internal to allow reuse by HelperMethodEmitter. /// - private static string RenderInlineNestedFromItem( - string mapVarName, - NestedInlineInfo inlineInfo, - GeneratorContext context + internal static string RenderInlineNestedFromItem( + string mapVarName, NestedInlineInfo inlineInfo, GeneratorContext context ) { var sb = new StringBuilder(); @@ -489,14 +529,15 @@ GeneratorContext context { // Recursive nested object var nestedVarName = $"{mapVarName}_{prop.PropertyName.ToLowerInvariant()}"; - var nestedCode = prop.NestedMapping switch - { - MapperBasedNesting mapperBased => - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : null", - InlineNesting inline => - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {RenderInlineNestedFromItem(nestedVarName, inline.Info, context)} : null", - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var nestedCode = + prop.NestedMapping switch + { + MapperBasedNesting mapperBased => + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : null", + InlineNesting inline => + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {RenderInlineNestedFromItem(nestedVarName, inline.Info, context)} : null", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; sb.Append($"{prop.PropertyName} = {nestedCode},"); } else if (prop.Strategy is not null) @@ -506,12 +547,19 @@ GeneratorContext context var genericArg = prop.Strategy.GenericArgument; // Build type-specific args - var typeArgs = prop.Strategy.FromTypeSpecificArgs.Length > 0 - ? string.Join(", ", prop.Strategy.FromTypeSpecificArgs.Select(a => - a.StartsWith("\"") ? $"format: {a}" : a)) + ", " - : ""; - - sb.Append($"{prop.PropertyName} = {mapVarName}.{getMethod}{genericArg}(\"{prop.DynamoKey}\", {typeArgs}Requiredness.Optional),"); + var typeArgs = + prop.Strategy.FromTypeSpecificArgs.Length > 0 + ? string.Join( + ", ", + prop.Strategy.FromTypeSpecificArgs.Select( + a => a.StartsWith("\"") ? $"format: {a}" : a + ) + ) + ", " + : ""; + + sb.Append( + $"{prop.PropertyName} = {mapVarName}.{getMethod}{genericArg}(\"{prop.DynamoKey}\", {typeArgs}Requiredness.Optional)," + ); } } @@ -523,10 +571,8 @@ GeneratorContext context /// Renders the FromItem code for a nested object (regular assignment style). /// private static string RenderNestedObjectFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, string modelVarName, PropertyAnalysis analysis, + GeneratorContext context, HelperMethodRegistry helperRegistry ) { var nestedMapping = spec.TypeStrategy!.NestedMapping!; @@ -535,12 +581,22 @@ GeneratorContext context return nestedMapping switch { MapperBasedNesting mapperBased => RenderMapperBasedFromAssignment( - spec, modelVarName, itemParam, mapperBased.Mapper + spec, + modelVarName, + itemParam, + mapperBased.Mapper ), InlineNesting inline => RenderInlineFromAssignment( - spec, modelVarName, itemParam, inline.Info, context + spec, + modelVarName, + itemParam, + inline.Info, + context, + helperRegistry + ), + _ => throw new InvalidOperationException( + $"Unknown nested mapping type: {nestedMapping.GetType()}" ), - _ => throw new InvalidOperationException($"Unknown nested mapping type: {nestedMapping.GetType()}") }; } @@ -548,30 +604,33 @@ GeneratorContext context /// Renders FromItem regular assignment code for mapper-based nested objects. /// private static string RenderMapperBasedFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - string itemParam, - MapperReference mapper + PropertyMappingSpec spec, string modelVarName, string itemParam, MapperReference mapper ) { var varName = spec.PropertyName.ToLowerInvariant(); - return $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map) {modelVarName}.{spec.PropertyName} = {mapper.MapperFullyQualifiedName}.FromItem({varName}Map);"; + return + $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map) {modelVarName}.{spec.PropertyName} = {mapper.MapperFullyQualifiedName}.FromItem({varName}Map);"; } /// /// Renders FromItem regular assignment code for inline nested objects. /// private static string RenderInlineFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - string itemParam, - NestedInlineInfo inlineInfo, - GeneratorContext context + PropertyMappingSpec spec, string modelVarName, string itemParam, + NestedInlineInfo inlineInfo, GeneratorContext context, HelperMethodRegistry helperRegistry ) { var varName = spec.PropertyName.ToLowerInvariant(); - var initCode = RenderInlineNestedFromItem(varName + "Map", inlineInfo, context); - return $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map) {modelVarName}.{spec.PropertyName} = {initCode};"; + + // Register the helper method and get its name + var helperMethodName = + helperRegistry.GetOrRegisterFromItemHelper( + inlineInfo.ModelFullyQualifiedType, + inlineInfo + ); + + return + $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map) {modelVarName}.{spec.PropertyName} = {helperMethodName}({varName}Map);"; } #endregion @@ -582,11 +641,9 @@ GeneratorContext context /// Renders a nested collection property (List<NestedType> or Dictionary<string, NestedType>) into PropertyInfo. /// private static PropertyInfo RenderNestedCollectionProperty( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - string modelVarName, - InitializationMethod initMethod, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, string modelVarName, + InitializationMethod initMethod, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { // Use init syntax for InitSyntax mode, regular assignment for PostConstruction @@ -594,25 +651,27 @@ GeneratorContext context // FromItem for nested collections var fromAssignment = - context.HasFromItemMethod - && analysis.HasSetter - && spec.FromItemMethod is not null - && !useInitSyntax - ? RenderNestedCollectionFromAssignment(spec, modelVarName, analysis, context) + context.HasFromItemMethod && analysis.HasSetter && spec.FromItemMethod is not null && + !useInitSyntax + ? RenderNestedCollectionFromAssignment( + spec, + modelVarName, + analysis, + context, + helperRegistry + ) : null; var fromInitAssignment = - context.HasFromItemMethod - && analysis.HasSetter - && spec.FromItemMethod is not null - && useInitSyntax - ? RenderNestedCollectionFromInitAssignment(spec, analysis, context) + context.HasFromItemMethod && analysis.HasSetter && spec.FromItemMethod is not null && + useInitSyntax + ? RenderNestedCollectionFromInitAssignment(spec, analysis, context, helperRegistry) : null; // ToItem for nested collections var toAssignments = context.HasToItemMethod && analysis.HasGetter && spec.ToItemMethod is not null - ? RenderNestedCollectionToAssignment(spec, analysis, context) + ? RenderNestedCollectionToAssignment(spec, analysis, context, helperRegistry) : null; return new PropertyInfo(fromAssignment, fromInitAssignment, toAssignments, null); @@ -622,9 +681,8 @@ GeneratorContext context /// Renders the ToItem code for a nested collection. /// private static string RenderNestedCollectionToAssignment( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { var collectionInfo = spec.TypeStrategy!.CollectionInfo!; @@ -636,12 +694,25 @@ GeneratorContext context return collectionInfo.Category switch { CollectionCategory.List => RenderNestedListToAssignment( - spec, propAccess, isNullable, elementMapping, collectionInfo, context + spec, + propAccess, + isNullable, + elementMapping, + collectionInfo, + context, + helperRegistry ), CollectionCategory.Map => RenderNestedMapToAssignment( - spec, propAccess, isNullable, elementMapping, context + spec, + propAccess, + isNullable, + elementMapping, + context, + helperRegistry + ), + _ => throw new InvalidOperationException( + $"Unexpected collection category for nested collection: {collectionInfo.Category}" ), - _ => throw new InvalidOperationException($"Unexpected collection category for nested collection: {collectionInfo.Category}") }; } @@ -650,28 +721,28 @@ GeneratorContext context /// Output: .Set("key", source.Prop?.Select(x => new AttributeValue { M = ... }).ToList()) /// private static string RenderNestedListToAssignment( - PropertyMappingSpec spec, - string propAccess, - bool isNullable, - NestedMappingInfo elementMapping, - CollectionInfo collectionInfo, - GeneratorContext context + PropertyMappingSpec spec, string propAccess, bool isNullable, + NestedMappingInfo elementMapping, CollectionInfo collectionInfo, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { - var elementConverter = elementMapping switch - { - MapperBasedNesting mapperBased => - $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem(x) }}", - InlineNesting inline => - $"new AttributeValue {{ M = {RenderInlineNestedToItem("x", inline.Info, context)} }}", - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var elementConverter = + elementMapping switch + { + MapperBasedNesting mapperBased => + $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem(x) }}", + InlineNesting inline => + $"new AttributeValue {{ M = {helperRegistry.GetOrRegisterToItemHelper(inline.Info.ModelFullyQualifiedType, inline.Info)}(x) }}", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; - var selectExpr = $"{propAccess}{(isNullable ? "?" : "")}.Select(x => {elementConverter}).ToList()"; + var selectExpr = + $"{propAccess}{(isNullable ? "?" : "")}.Select(x => {elementConverter}).ToList()"; if (isNullable) { - return $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ L = {selectExpr} }})"; + return + $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ L = {selectExpr} }})"; } return $".Set(\"{spec.Key}\", new AttributeValue {{ L = {selectExpr} }})"; @@ -682,27 +753,28 @@ GeneratorContext context /// Output: .Set("key", source.Prop?.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { M = ... })) /// private static string RenderNestedMapToAssignment( - PropertyMappingSpec spec, - string propAccess, - bool isNullable, - NestedMappingInfo elementMapping, - GeneratorContext context + PropertyMappingSpec spec, string propAccess, bool isNullable, + NestedMappingInfo elementMapping, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { - var valueConverter = elementMapping switch - { - MapperBasedNesting mapperBased => - $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem(kvp.Value) }}", - InlineNesting inline => - $"new AttributeValue {{ M = {RenderInlineNestedToItem("kvp.Value", inline.Info, context)} }}", - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var valueConverter = + elementMapping switch + { + MapperBasedNesting mapperBased => + $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem(kvp.Value) }}", + InlineNesting inline => + $"new AttributeValue {{ M = {helperRegistry.GetOrRegisterToItemHelper(inline.Info.ModelFullyQualifiedType, inline.Info)}(kvp.Value) }}", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; - var toDictExpr = $"{propAccess}{(isNullable ? "?" : "")}.ToDictionary(kvp => kvp.Key, kvp => {valueConverter})"; + var toDictExpr = + $"{propAccess}{(isNullable ? "?" : "")}.ToDictionary(kvp => kvp.Key, kvp => {valueConverter})"; if (isNullable) { - return $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ M = {toDictExpr} }})"; + return + $".Set(\"{spec.Key}\", {propAccess} is null ? new AttributeValue {{ NULL = true }} : new AttributeValue {{ M = {toDictExpr} }})"; } return $".Set(\"{spec.Key}\", new AttributeValue {{ M = {toDictExpr} }})"; @@ -712,9 +784,8 @@ GeneratorContext context /// Renders the FromItem code for a nested collection (init-style assignment). /// private static string RenderNestedCollectionFromInitAssignment( - PropertyMappingSpec spec, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, PropertyAnalysis analysis, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { var collectionInfo = spec.TypeStrategy!.CollectionInfo!; @@ -728,23 +799,41 @@ GeneratorContext context return collectionInfo.Category switch { CollectionCategory.List => RenderNestedListFromInitAssignment( - spec, itemParam, varName, elementMapping, collectionInfo, context, fallback + spec, + itemParam, + varName, + elementMapping, + collectionInfo, + context, + fallback, + helperRegistry ), CollectionCategory.Map => RenderNestedMapFromInitAssignment( - spec, itemParam, varName, elementMapping, context, fallback + spec, + itemParam, + varName, + elementMapping, + context, + fallback, + helperRegistry + ), + _ => throw new InvalidOperationException( + $"Unexpected collection category for nested collection: {collectionInfo.Category}" ), - _ => throw new InvalidOperationException($"Unexpected collection category for nested collection: {collectionInfo.Category}") }; } /// /// Determines the fallback expression for a nested collection when not found in DynamoDB. /// - private static string GetNestedCollectionFallback(PropertyMappingSpec spec, PropertyAnalysis analysis) + private static string GetNestedCollectionFallback( + PropertyMappingSpec spec, PropertyAnalysis analysis + ) { // If property has 'required' keyword, throw on missing if (analysis.IsRequired) - return $"throw new System.InvalidOperationException(\"Required attribute '{spec.Key}' not found.\")"; + return + $"throw new System.InvalidOperationException(\"Required attribute '{spec.Key}' not found.\")"; // For nullable types, return null if (analysis.Nullability.IsNullableType) @@ -759,66 +848,61 @@ private static string GetNestedCollectionFallback(PropertyMappingSpec spec, Prop /// Renders FromItem init-style code for List<NestedType>. /// private static string RenderNestedListFromInitAssignment( - PropertyMappingSpec spec, - string itemParam, - string varName, - NestedMappingInfo elementMapping, - CollectionInfo collectionInfo, - GeneratorContext context, - string fallback + PropertyMappingSpec spec, string itemParam, string varName, + NestedMappingInfo elementMapping, CollectionInfo collectionInfo, GeneratorContext context, + string fallback, HelperMethodRegistry helperRegistry ) { - var elementConverter = elementMapping switch - { - MapperBasedNesting mapperBased => - $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(av.M)", - InlineNesting inline => - RenderInlineNestedFromItem("av.M", inline.Info, context), - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var elementConverter = + elementMapping switch + { + MapperBasedNesting mapperBased => + $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(av.M)", + InlineNesting inline => + $"{helperRegistry.GetOrRegisterFromItemHelper(inline.Info.ModelFullyQualifiedType, inline.Info)}(av.M)", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; var selectExpr = $"{varName}List.Select(av => {elementConverter}).ToList()"; // For arrays, add .ToArray() var toArray = collectionInfo.IsArray ? ".ToArray()" : ""; - return $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.L is {{ }} {varName}List ? {selectExpr}{toArray} : {fallback},"; + return + $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.L is {{ }} {varName}List ? {selectExpr}{toArray} : {fallback},"; } /// /// Renders FromItem init-style code for Dictionary<string, NestedType>. /// private static string RenderNestedMapFromInitAssignment( - PropertyMappingSpec spec, - string itemParam, - string varName, - NestedMappingInfo elementMapping, - GeneratorContext context, - string fallback + PropertyMappingSpec spec, string itemParam, string varName, + NestedMappingInfo elementMapping, GeneratorContext context, string fallback, + HelperMethodRegistry helperRegistry ) { - var valueConverter = elementMapping switch - { - MapperBasedNesting mapperBased => - $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(kvp.Value.M)", - InlineNesting inline => - RenderInlineNestedFromItem("kvp.Value.M", inline.Info, context), - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var valueConverter = + elementMapping switch + { + MapperBasedNesting mapperBased => + $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(kvp.Value.M)", + InlineNesting inline => + $"{helperRegistry.GetOrRegisterFromItemHelper(inline.Info.ModelFullyQualifiedType, inline.Info)}(kvp.Value.M)", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; var toDictExpr = $"{varName}Map.ToDictionary(kvp => kvp.Key, kvp => {valueConverter})"; - return $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map ? {toDictExpr} : {fallback},"; + return + $"{spec.PropertyName} = {itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map ? {toDictExpr} : {fallback},"; } /// /// Renders the FromItem code for a nested collection (regular assignment style). /// private static string RenderNestedCollectionFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - PropertyAnalysis analysis, - GeneratorContext context + PropertyMappingSpec spec, string modelVarName, PropertyAnalysis analysis, + GeneratorContext context, HelperMethodRegistry helperRegistry ) { var collectionInfo = spec.TypeStrategy!.CollectionInfo!; @@ -829,12 +913,27 @@ GeneratorContext context return collectionInfo.Category switch { CollectionCategory.List => RenderNestedListFromAssignment( - spec, modelVarName, itemParam, varName, elementMapping, collectionInfo, context + spec, + modelVarName, + itemParam, + varName, + elementMapping, + collectionInfo, + context, + helperRegistry ), CollectionCategory.Map => RenderNestedMapFromAssignment( - spec, modelVarName, itemParam, varName, elementMapping, context + spec, + modelVarName, + itemParam, + varName, + elementMapping, + context, + helperRegistry + ), + _ => throw new InvalidOperationException( + $"Unexpected collection category for nested collection: {collectionInfo.Category}" ), - _ => throw new InvalidOperationException($"Unexpected collection category for nested collection: {collectionInfo.Category}") }; } @@ -842,56 +941,53 @@ GeneratorContext context /// Renders FromItem regular assignment code for List<NestedType>. /// private static string RenderNestedListFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - string itemParam, - string varName, - NestedMappingInfo elementMapping, - CollectionInfo collectionInfo, - GeneratorContext context + PropertyMappingSpec spec, string modelVarName, string itemParam, string varName, + NestedMappingInfo elementMapping, CollectionInfo collectionInfo, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { - var elementConverter = elementMapping switch - { - MapperBasedNesting mapperBased => - $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(av.M)", - InlineNesting inline => - RenderInlineNestedFromItem("av.M", inline.Info, context), - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var elementConverter = + elementMapping switch + { + MapperBasedNesting mapperBased => + $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(av.M)", + InlineNesting inline => + $"{helperRegistry.GetOrRegisterFromItemHelper(inline.Info.ModelFullyQualifiedType, inline.Info)}(av.M)", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; var selectExpr = $"{varName}List.Select(av => {elementConverter}).ToList()"; // For arrays, add .ToArray() var toArray = collectionInfo.IsArray ? ".ToArray()" : ""; - return $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.L is {{ }} {varName}List) {modelVarName}.{spec.PropertyName} = {selectExpr}{toArray};"; + return + $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.L is {{ }} {varName}List) {modelVarName}.{spec.PropertyName} = {selectExpr}{toArray};"; } /// /// Renders FromItem regular assignment code for Dictionary<string, NestedType>. /// private static string RenderNestedMapFromAssignment( - PropertyMappingSpec spec, - string modelVarName, - string itemParam, - string varName, - NestedMappingInfo elementMapping, - GeneratorContext context + PropertyMappingSpec spec, string modelVarName, string itemParam, string varName, + NestedMappingInfo elementMapping, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { - var valueConverter = elementMapping switch - { - MapperBasedNesting mapperBased => - $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(kvp.Value.M)", - InlineNesting inline => - RenderInlineNestedFromItem("kvp.Value.M", inline.Info, context), - _ => throw new InvalidOperationException($"Unknown nested mapping type") - }; + var valueConverter = + elementMapping switch + { + MapperBasedNesting mapperBased => + $"{mapperBased.Mapper.MapperFullyQualifiedName}.FromItem(kvp.Value.M)", + InlineNesting inline => + $"{helperRegistry.GetOrRegisterFromItemHelper(inline.Info.ModelFullyQualifiedType, inline.Info)}(kvp.Value.M)", + _ => throw new InvalidOperationException("Unknown nested mapping type"), + }; var toDictExpr = $"{varName}Map.ToDictionary(kvp => kvp.Key, kvp => {valueConverter})"; - return $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map) {modelVarName}.{spec.PropertyName} = {toDictExpr};"; + return + $"if ({itemParam}.TryGetValue(\"{spec.Key}\", out var {varName}Attr) && {varName}Attr.M is {{ }} {varName}Map) {modelVarName}.{spec.PropertyName} = {toDictExpr};"; } #endregion diff --git a/src/LayeredCraft.DynamoMapper.Generators/Templates/Mapper.scriban b/src/LayeredCraft.DynamoMapper.Generators/Templates/Mapper.scriban index f4c33b7..283bed2 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Templates/Mapper.scriban +++ b/src/LayeredCraft.DynamoMapper.Generators/Templates/Mapper.scriban @@ -74,4 +74,13 @@ using Amazon.DynamoDBv2.Model; return {{ model_var_name }}; } {{~ end ~}} +{{~ # Helper methods for nested object mapping ~}} +{{~ if helper_methods.size > 0 ~}} + + // Helper methods for nested object mapping + {{~ for helper in helper_methods ~}} + + {{ helper }} + {{~ end ~}} +{{~ end ~}} } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs index c32cbc6..dfefe0e 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs @@ -22,15 +22,33 @@ public static partial class ExampleEntityMapper [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Entity source) => new Dictionary(1) - .Set("items", new AttributeValue { L = source.Items.Select(x => new AttributeValue { M = new Dictionary().SetString("name", x.Name, false, true) }).ToList() }); + .Set("items", new AttributeValue { L = source.Items.Select(x => new AttributeValue { M = ToItem_CustomClass(x) }).ToList() }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Entity FromItem(global::System.Collections.Generic.Dictionary item) { var entity = new global::MyNamespace.Entity { - Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => new global::MyNamespace.CustomClass { Name = av.M.GetString("name", Requiredness.Optional), }).ToList() : [], + Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => FromItem_CustomClass(av.M)).ToList() : [], }; return entity; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_CustomClass(global::MyNamespace.CustomClass customclass) + { + return new Dictionary() + .SetString("name", customclass.Name, false, true); + } + + + private static global::MyNamespace.CustomClass FromItem_CustomClass(Dictionary map) + { + return new global::MyNamespace.CustomClass + { + Name = map.GetString("name", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs index 4176c78..7eb3f11 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs @@ -22,15 +22,35 @@ internal static partial class OrderMapper [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] internal static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(1) - .Set("address", new AttributeValue { M = new Dictionary().SetString("city", source.Address.City, false, true).SetString("line_1", source.Address.Line1, false, true) }); + .Set("address", new AttributeValue { M = ToItem_Address(source.Address) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] internal static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) { var order = new global::MyNamespace.Order { - Address = item.TryGetValue("address", out var addressAttr) && addressAttr.M is { } addressMap ? new global::MyNamespace.Address { City = addressMap.GetString("city", Requiredness.Optional), Line1 = addressMap.GetString("line_1", Requiredness.Optional), } : null, + Address = item.TryGetValue("address", out var addressAttr) && addressAttr.M is { } addressMap ? FromItem_Address(addressMap) : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) + { + return new Dictionary() + .SetString("city", address.City, false, true) + .SetString("line_1", address.Line1, false, true); + } + + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + City = map.GetString("city", Requiredness.Optional), + Line1 = map.GetString("line_1", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs index 23290ab..9768484 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("items", new AttributeValue { L = source.Items.Select(x => new AttributeValue { M = new Dictionary().SetString("productId", x.ProductId, false, true).SetInt("quantity", x.Quantity, false, true) }).ToList() }); + .Set("items", new AttributeValue { L = source.Items.Select(x => new AttributeValue { M = ToItem_LineItem(x) }).ToList() }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,28 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => new global::MyNamespace.LineItem { ProductId = av.M.GetString("productId", Requiredness.Optional), Quantity = av.M.GetInt("quantity", Requiredness.Optional), }).ToList().ToArray() : [], + Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => FromItem_LineItem(av.M)).ToList().ToArray() : [], }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) + { + return new Dictionary() + .SetString("productId", lineitem.ProductId, false, true) + .SetInt("quantity", lineitem.Quantity, false, true); + } + + + private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) + { + return new global::MyNamespace.LineItem + { + ProductId = map.GetString("productId", Requiredness.Optional), + Quantity = map.GetInt("quantity", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs index 0b1b692..73e76fa 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class CatalogMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Catalog source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("products", new AttributeValue { M = source.Products.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { M = new Dictionary().SetString("name", kvp.Value.Name, false, true).SetDecimal("price", kvp.Value.Price, false, true) }) }); + .Set("products", new AttributeValue { M = source.Products.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { M = ToItem_Product(kvp.Value) }) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Catalog FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,28 @@ public static partial class CatalogMapper var catalog = new global::MyNamespace.Catalog { Id = item.GetString("id", Requiredness.InferFromNullability), - Products = item.TryGetValue("products", out var productsAttr) && productsAttr.M is { } productsMap ? productsMap.ToDictionary(kvp => kvp.Key, kvp => new global::MyNamespace.Product { Name = kvp.Value.M.GetString("name", Requiredness.Optional), Price = kvp.Value.M.GetDecimal("price", Requiredness.Optional), }) : [], + Products = item.TryGetValue("products", out var productsAttr) && productsAttr.M is { } productsMap ? productsMap.ToDictionary(kvp => kvp.Key, kvp => FromItem_Product(kvp.Value.M)) : [], }; return catalog; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Product(global::MyNamespace.Product product) + { + return new Dictionary() + .SetString("name", product.Name, false, true) + .SetDecimal("price", product.Price, false, true); + } + + + private static global::MyNamespace.Product FromItem_Product(Dictionary map) + { + return new global::MyNamespace.Product + { + Name = map.GetString("name", Requiredness.Optional), + Price = map.GetDecimal("price", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs index 2c0054e..8c69f99 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("items", new AttributeValue { L = source.Items.Select(x => new AttributeValue { M = new Dictionary().SetString("productId", x.ProductId, false, true).SetInt("quantity", x.Quantity, false, true).SetDecimal("price", x.Price, false, true) }).ToList() }); + .Set("items", new AttributeValue { L = source.Items.Select(x => new AttributeValue { M = ToItem_LineItem(x) }).ToList() }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,30 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => new global::MyNamespace.LineItem { ProductId = av.M.GetString("productId", Requiredness.Optional), Quantity = av.M.GetInt("quantity", Requiredness.Optional), Price = av.M.GetDecimal("price", Requiredness.Optional), }).ToList() : [], + Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => FromItem_LineItem(av.M)).ToList() : [], }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) + { + return new Dictionary() + .SetString("productId", lineitem.ProductId, false, true) + .SetInt("quantity", lineitem.Quantity, false, true) + .SetDecimal("price", lineitem.Price, false, true); + } + + + private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) + { + return new global::MyNamespace.LineItem + { + ProductId = map.GetString("productId", Requiredness.Optional), + Quantity = map.GetInt("quantity", Requiredness.Optional), + Price = map.GetDecimal("price", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs index 95427a7..ea20447 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class EventLogMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.EventLog source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("entries", new AttributeValue { L = source.Entries.Select(x => new AttributeValue { M = new Dictionary().SetDateTime("timestamp", x.Timestamp, "O", false, true).SetString("message", x.Message, false, true).SetInt("severity", x.Severity, false, true).SetBool("isError", x.IsError, false, true) }).ToList() }); + .Set("entries", new AttributeValue { L = source.Entries.Select(x => new AttributeValue { M = ToItem_LogEntry(x) }).ToList() }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.EventLog FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,32 @@ public static partial class EventLogMapper var eventLog = new global::MyNamespace.EventLog { Id = item.GetString("id", Requiredness.InferFromNullability), - Entries = item.TryGetValue("entries", out var entriesAttr) && entriesAttr.L is { } entriesList ? entriesList.Select(av => new global::MyNamespace.LogEntry { Timestamp = av.M.GetDateTime("timestamp", format: "O", Requiredness.Optional), Message = av.M.GetString("message", Requiredness.Optional), Severity = av.M.GetInt("severity", Requiredness.Optional), IsError = av.M.GetBool("isError", Requiredness.Optional), }).ToList() : [], + Entries = item.TryGetValue("entries", out var entriesAttr) && entriesAttr.L is { } entriesList ? entriesList.Select(av => FromItem_LogEntry(av.M)).ToList() : [], }; return eventLog; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_LogEntry(global::MyNamespace.LogEntry logentry) + { + return new Dictionary() + .SetDateTime("timestamp", logentry.Timestamp, "O", false, true) + .SetString("message", logentry.Message, false, true) + .SetInt("severity", logentry.Severity, false, true) + .SetBool("isError", logentry.IsError, false, true); + } + + + private static global::MyNamespace.LogEntry FromItem_LogEntry(Dictionary map) + { + return new global::MyNamespace.LogEntry + { + Timestamp = map.GetDateTime("timestamp", format: "O", Requiredness.Optional), + Message = map.GetString("message", Requiredness.Optional), + Severity = map.GetInt("severity", Requiredness.Optional), + IsError = map.GetBool("isError", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs index 0844b8c..efe9d4a 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class CatalogMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Catalog source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("products", source.Products is null ? new AttributeValue { NULL = true } : new AttributeValue { M = source.Products?.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { M = new Dictionary().SetString("name", kvp.Value.Name, false, true).SetDecimal("price", kvp.Value.Price, false, true) }) }); + .Set("products", source.Products is null ? new AttributeValue { NULL = true } : new AttributeValue { M = source.Products?.ToDictionary(kvp => kvp.Key, kvp => new AttributeValue { M = ToItem_Product(kvp.Value) }) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Catalog FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,28 @@ public static partial class CatalogMapper var catalog = new global::MyNamespace.Catalog { Id = item.GetString("id", Requiredness.InferFromNullability), - Products = item.TryGetValue("products", out var productsAttr) && productsAttr.M is { } productsMap ? productsMap.ToDictionary(kvp => kvp.Key, kvp => new global::MyNamespace.Product { Name = kvp.Value.M.GetString("name", Requiredness.Optional), Price = kvp.Value.M.GetDecimal("price", Requiredness.Optional), }) : null, + Products = item.TryGetValue("products", out var productsAttr) && productsAttr.M is { } productsMap ? productsMap.ToDictionary(kvp => kvp.Key, kvp => FromItem_Product(kvp.Value.M)) : null, }; return catalog; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Product(global::MyNamespace.Product product) + { + return new Dictionary() + .SetString("name", product.Name, false, true) + .SetDecimal("price", product.Price, false, true); + } + + + private static global::MyNamespace.Product FromItem_Product(Dictionary map) + { + return new global::MyNamespace.Product + { + Name = map.GetString("name", Requiredness.Optional), + Price = map.GetDecimal("price", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs index 11b870c..75a6b15 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("items", source.Items is null ? new AttributeValue { NULL = true } : new AttributeValue { L = source.Items?.Select(x => new AttributeValue { M = new Dictionary().SetString("productId", x.ProductId, false, true).SetInt("quantity", x.Quantity, false, true) }).ToList() }); + .Set("items", source.Items is null ? new AttributeValue { NULL = true } : new AttributeValue { L = source.Items?.Select(x => new AttributeValue { M = ToItem_LineItem(x) }).ToList() }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,28 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => new global::MyNamespace.LineItem { ProductId = av.M.GetString("productId", Requiredness.Optional), Quantity = av.M.GetInt("quantity", Requiredness.Optional), }).ToList() : null, + Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => FromItem_LineItem(av.M)).ToList() : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) + { + return new Dictionary() + .SetString("productId", lineitem.ProductId, false, true) + .SetInt("quantity", lineitem.Quantity, false, true); + } + + + private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) + { + return new global::MyNamespace.LineItem + { + ProductId = map.GetString("productId", Requiredness.Optional), + Quantity = map.GetInt("quantity", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs index 11b870c..75a6b15 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("items", source.Items is null ? new AttributeValue { NULL = true } : new AttributeValue { L = source.Items?.Select(x => new AttributeValue { M = new Dictionary().SetString("productId", x.ProductId, false, true).SetInt("quantity", x.Quantity, false, true) }).ToList() }); + .Set("items", source.Items is null ? new AttributeValue { NULL = true } : new AttributeValue { L = source.Items?.Select(x => new AttributeValue { M = ToItem_LineItem(x) }).ToList() }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,28 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => new global::MyNamespace.LineItem { ProductId = av.M.GetString("productId", Requiredness.Optional), Quantity = av.M.GetInt("quantity", Requiredness.Optional), }).ToList() : null, + Items = item.TryGetValue("items", out var itemsAttr) && itemsAttr.L is { } itemsList ? itemsList.Select(av => FromItem_LineItem(av.M)).ToList() : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) + { + return new Dictionary() + .SetString("productId", lineitem.ProductId, false, true) + .SetInt("quantity", lineitem.Quantity, false, true); + } + + + private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) + { + return new global::MyNamespace.LineItem + { + ProductId = map.GetString("productId", Requiredness.Optional), + Quantity = map.GetInt("quantity", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs index 96e6a4d..b0a0543 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("shippingAddress", new AttributeValue { M = new Dictionary().SetString("line1", source.ShippingAddress.Line1, false, true).SetString("city", source.ShippingAddress.City, false, true).SetString("postalCode", source.ShippingAddress.PostalCode, false, true) }); + .Set("shippingAddress", new AttributeValue { M = ToItem_Address(source.ShippingAddress) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,30 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? new global::MyNamespace.Address { Line1 = shippingaddressMap.GetString("line1", Requiredness.Optional), City = shippingaddressMap.GetString("city", Requiredness.Optional), PostalCode = shippingaddressMap.GetString("postalCode", Requiredness.Optional), } : null, + ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? FromItem_Address(shippingaddressMap) : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) + { + return new Dictionary() + .SetString("line1", address.Line1, false, true) + .SetString("city", address.City, false, true) + .SetString("postalCode", address.PostalCode, false, true); + } + + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + Line1 = map.GetString("line1", Requiredness.Optional), + City = map.GetString("city", Requiredness.Optional), + PostalCode = map.GetString("postalCode", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs index 01f81e7..49f60c2 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("customer", new AttributeValue { M = new Dictionary().SetString("name", source.Customer.Name, false, true).Set("address", source.Customer.Address is null ? new AttributeValue { NULL = true } : new AttributeValue { M = new Dictionary().SetString("line1", source.Customer.Address.Line1, false, true).SetString("city", source.Customer.Address.City, false, true) }) }); + .Set("customer", new AttributeValue { M = ToItem_Customer(source.Customer) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,30 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - Customer = item.TryGetValue("customer", out var customerAttr) && customerAttr.M is { } customerMap ? new global::MyNamespace.Customer { Name = customerMap.GetString("name", Requiredness.Optional), Address = customerMap.TryGetValue("address", out var customerMap_addressAttr) && customerMap_addressAttr.M is { } customerMap_address ? new global::MyNamespace.Address { Line1 = customerMap_address.GetString("line1", Requiredness.Optional), City = customerMap_address.GetString("city", Requiredness.Optional), } : null, } : null, + Customer = item.TryGetValue("customer", out var customerAttr) && customerAttr.M is { } customerMap ? FromItem_Customer(customerMap) : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Customer(global::MyNamespace.Customer customer) + { + return new Dictionary() + .SetString("name", customer.Name, false, true) + .Set("address", customer.Address is null ? new AttributeValue { NULL = true } : new AttributeValue { M = new Dictionary().SetString("line1", customer.Address.Line1, false, true).SetString("city", customer.Address.City, false, true) }); + } + + + private static global::MyNamespace.Customer FromItem_Customer(Dictionary map) + { + return new global::MyNamespace.Customer + { + Name = map.GetString("name", Requiredness.Optional), + Address = map.TryGetValue("address", out var map_addressAttr) && map_addressAttr.M is { } map_address ? new global::MyNamespace.Address { Line1 = map_address.GetString("line1", Requiredness.Optional), + City = map_address.GetString("city", Requiredness.Optional), + } : null, + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs index f824608..4496237 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("billingAddress", source.BillingAddress is null ? new AttributeValue { NULL = true } : new AttributeValue { M = new Dictionary().SetString("line1", source.BillingAddress.Line1, false, true).SetString("city", source.BillingAddress.City, false, true) }); + .Set("billingAddress", source.BillingAddress is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Address(source.BillingAddress) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,28 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - BillingAddress = item.TryGetValue("billingAddress", out var billingaddressAttr) && billingaddressAttr.M is { } billingaddressMap ? new global::MyNamespace.Address { Line1 = billingaddressMap.GetString("line1", Requiredness.Optional), City = billingaddressMap.GetString("city", Requiredness.Optional), } : null, + BillingAddress = item.TryGetValue("billingAddress", out var billingaddressAttr) && billingaddressAttr.M is { } billingaddressMap ? FromItem_Address(billingaddressMap) : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) + { + return new Dictionary() + .SetString("line1", address.Line1, false, true) + .SetString("city", address.City, false, true); + } + + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + Line1 = map.GetString("line1", Requiredness.Optional), + City = map.GetString("city", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs index 96e6a4d..b0a0543 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("shippingAddress", new AttributeValue { M = new Dictionary().SetString("line1", source.ShippingAddress.Line1, false, true).SetString("city", source.ShippingAddress.City, false, true).SetString("postalCode", source.ShippingAddress.PostalCode, false, true) }); + .Set("shippingAddress", new AttributeValue { M = ToItem_Address(source.ShippingAddress) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,30 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? new global::MyNamespace.Address { Line1 = shippingaddressMap.GetString("line1", Requiredness.Optional), City = shippingaddressMap.GetString("city", Requiredness.Optional), PostalCode = shippingaddressMap.GetString("postalCode", Requiredness.Optional), } : null, + ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? FromItem_Address(shippingaddressMap) : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) + { + return new Dictionary() + .SetString("line1", address.Line1, false, true) + .SetString("city", address.City, false, true) + .SetString("postalCode", address.PostalCode, false, true); + } + + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + Line1 = map.GetString("line1", Requiredness.Optional), + City = map.GetString("city", Requiredness.Optional), + PostalCode = map.GetString("postalCode", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs index dc0941e..172bd15 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs @@ -23,7 +23,7 @@ public static partial class OrderMapper public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => new Dictionary(2) .SetString("id", source.Id, false, true) - .Set("shippingAddress", new AttributeValue { M = new Dictionary().SetString("addr_line1", source.ShippingAddress.Line1, false, true).SetString("addr_city", source.ShippingAddress.City, false, true).SetString("postalCode", source.ShippingAddress.PostalCode, false, true) }); + .Set("shippingAddress", new AttributeValue { M = ToItem_Address(source.ShippingAddress) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) @@ -31,8 +31,30 @@ public static partial class OrderMapper var order = new global::MyNamespace.Order { Id = item.GetString("id", Requiredness.InferFromNullability), - ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? new global::MyNamespace.Address { Line1 = shippingaddressMap.GetString("addr_line1", Requiredness.Optional), City = shippingaddressMap.GetString("addr_city", Requiredness.Optional), PostalCode = shippingaddressMap.GetString("postalCode", Requiredness.Optional), } : null, + ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? FromItem_Address(shippingaddressMap) : null, }; return order; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) + { + return new Dictionary() + .SetString("addr_line1", address.Line1, false, true) + .SetString("addr_city", address.City, false, true) + .SetString("postalCode", address.PostalCode, false, true); + } + + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + Line1 = map.GetString("addr_line1", Requiredness.Optional), + City = map.GetString("addr_city", Requiredness.Optional), + PostalCode = map.GetString("postalCode", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs index a317469..5df1975 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs @@ -24,7 +24,7 @@ public static partial class ProductMapper new Dictionary(3) .SetString("id", source.Id, false, true) .SetString("name", source.Name, false, true) - .Set("details", new AttributeValue { M = new Dictionary().SetDecimal("price", source.Details.Price, false, true).SetInt("stockCount", source.Details.StockCount, false, true).SetDateTime("lastUpdated", source.Details.LastUpdated, "O", false, true).SetBool("isActive", source.Details.IsActive, false, true) }); + .Set("details", new AttributeValue { M = ToItem_ProductDetails(source.Details) }); [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] public static partial global::MyNamespace.Product FromItem(global::System.Collections.Generic.Dictionary item) @@ -33,8 +33,32 @@ public static partial class ProductMapper { Id = item.GetString("id", Requiredness.InferFromNullability), Name = item.GetString("name", Requiredness.InferFromNullability), - Details = item.TryGetValue("details", out var detailsAttr) && detailsAttr.M is { } detailsMap ? new global::MyNamespace.ProductDetails { Price = detailsMap.GetDecimal("price", Requiredness.Optional), StockCount = detailsMap.GetInt("stockCount", Requiredness.Optional), LastUpdated = detailsMap.GetDateTime("lastUpdated", format: "O", Requiredness.Optional), IsActive = detailsMap.GetBool("isActive", Requiredness.Optional), } : null, + Details = item.TryGetValue("details", out var detailsAttr) && detailsAttr.M is { } detailsMap ? FromItem_ProductDetails(detailsMap) : null, }; return product; } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_ProductDetails(global::MyNamespace.ProductDetails productdetails) + { + return new Dictionary() + .SetDecimal("price", productdetails.Price, false, true) + .SetInt("stockCount", productdetails.StockCount, false, true) + .SetDateTime("lastUpdated", productdetails.LastUpdated, "O", false, true) + .SetBool("isActive", productdetails.IsActive, false, true); + } + + + private static global::MyNamespace.ProductDetails FromItem_ProductDetails(Dictionary map) + { + return new global::MyNamespace.ProductDetails + { + Price = map.GetDecimal("price", Requiredness.Optional), + StockCount = map.GetInt("stockCount", Requiredness.Optional), + LastUpdated = map.GetDateTime("lastUpdated", format: "O", Requiredness.Optional), + IsActive = map.GetBool("isActive", Requiredness.Optional), + }; + } + } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes.DotNet10_0#ProductMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes.DotNet10_0#ProductMapper.g.verified.cs new file mode 100644 index 0000000..5df1975 --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes.DotNet10_0#ProductMapper.g.verified.cs @@ -0,0 +1,64 @@ +//HintName: ProductMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +public static partial class ProductMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Product source) => + new Dictionary(3) + .SetString("id", source.Id, false, true) + .SetString("name", source.Name, false, true) + .Set("details", new AttributeValue { M = ToItem_ProductDetails(source.Details) }); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + public static partial global::MyNamespace.Product FromItem(global::System.Collections.Generic.Dictionary item) + { + var product = new global::MyNamespace.Product + { + Id = item.GetString("id", Requiredness.InferFromNullability), + Name = item.GetString("name", Requiredness.InferFromNullability), + Details = item.TryGetValue("details", out var detailsAttr) && detailsAttr.M is { } detailsMap ? FromItem_ProductDetails(detailsMap) : null, + }; + return product; + } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_ProductDetails(global::MyNamespace.ProductDetails productdetails) + { + return new Dictionary() + .SetDecimal("price", productdetails.Price, false, true) + .SetInt("stockCount", productdetails.StockCount, false, true) + .SetDateTime("lastUpdated", productdetails.LastUpdated, "O", false, true) + .SetBool("isActive", productdetails.IsActive, false, true); + } + + + private static global::MyNamespace.ProductDetails FromItem_ProductDetails(Dictionary map) + { + return new global::MyNamespace.ProductDetails + { + Price = map.GetDecimal("price", Requiredness.Optional), + StockCount = map.GetInt("stockCount", Requiredness.Optional), + LastUpdated = map.GetDateTime("lastUpdated", format: "O", Requiredness.Optional), + IsActive = map.GetBool("isActive", Requiredness.Optional), + }; + } + +} From 4fa03d432a86db3b81eff406590d57f427578ccb Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 11:01:12 -0500 Subject: [PATCH 02/17] feat(emitters): optimize dictionary initialization with calculated capacity - Updated `FormatToItemChainedCalls` to preallocate dictionary capacity based on `.Set*` method calls. - Introduced `CountSetMethodCalls` to calculate the number of `.Set*` calls for capacity estimation. - Adjusted generated `ToItem` methods in test snapshots to use preallocated dictionary capacity. --- .../Emitters/HelperMethodEmitter.cs | 22 +++++++++++++++++-- ...dSucceed#ExampleEntityMapper.g.verified.cs | 2 +- ...thOptIn_Succeeds#OrderMapper.g.verified.cs | 2 +- ...yOfNestedObjects#OrderMapper.g.verified.cs | 2 +- ...Objects_Inline#CatalogMapper.g.verified.cs | 2 +- ...edObjects_Inline#OrderMapper.g.verified.cs | 2 +- ...ngScalarTypes#EventLogMapper.g.verified.cs | 2 +- ...Objects_Inline#CatalogMapper.g.verified.cs | 2 +- ...tOfNestedObjects#OrderMapper.g.verified.cs | 2 +- ...edObjects_Inline#OrderMapper.g.verified.cs | 2 +- ...allsBackToInline#OrderMapper.g.verified.cs | 2 +- ...bject_MultiLevel#OrderMapper.g.verified.cs | 2 +- ...t_NullableInline#OrderMapper.g.verified.cs | 2 +- ...ect_SimpleInline#OrderMapper.g.verified.cs | 2 +- ...NotationOverride#OrderMapper.g.verified.cs | 2 +- ...ithScalarTypes#ProductMapper.g.verified.cs | 2 +- 16 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index d908c1e..efb9441 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -73,12 +73,15 @@ public static string RenderFromItemHelper(HelperMethodInfo helper, GeneratorCont /// /// Formats ToItem chained method calls on separate lines. Input: new Dictionary<string, /// AttributeValue>().SetString(...).SetDecimal(...) Output: new Dictionary<string, - /// AttributeValue>() .SetString(...) .SetDecimal(...) + /// AttributeValue>(capacity) .SetString(...) .SetDecimal(...) /// private static string FormatToItemChainedCalls(string bodyCode) { + // Count how many .Set* method calls there are for capacity pre-allocation + var capacity = CountSetMethodCalls(bodyCode); + var sb = new StringBuilder(); - sb.Append("new Dictionary()"); + sb.Append($"new Dictionary({capacity})"); // Find each .Set* method call and put it on a new line var startIndex = bodyCode.IndexOf(".Set", StringComparison.Ordinal); @@ -169,6 +172,21 @@ private static string FormatFromItemObjectInitializer(string bodyCode) return sb.ToString(); } + /// Counts the number of .Set* method calls in the body code for dictionary capacity. + private static int CountSetMethodCalls(string bodyCode) + { + var count = 0; + var index = 0; + + while ((index = bodyCode.IndexOf(".Set", index, StringComparison.Ordinal)) >= 0) + { + count++; + index += 4; // Move past ".Set" + } + + return count; + } + /// Splits property assignments by comma, respecting nested parentheses. private static string[] SplitPropertiesRespectingNesting(string content) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs index dfefe0e..acfdede 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs @@ -38,7 +38,7 @@ public static partial class ExampleEntityMapper private static Dictionary ToItem_CustomClass(global::MyNamespace.CustomClass customclass) { - return new Dictionary() + return new Dictionary(1) .SetString("name", customclass.Name, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs index 7eb3f11..b4b3a35 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs @@ -38,7 +38,7 @@ internal static partial class OrderMapper private static Dictionary ToItem_Address(global::MyNamespace.Address address) { - return new Dictionary() + return new Dictionary(2) .SetString("city", address.City, false, true) .SetString("line_1", address.Line1, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs index 9768484..4ebd226 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) { - return new Dictionary() + return new Dictionary(2) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs index 73e76fa..140aa71 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class CatalogMapper private static Dictionary ToItem_Product(global::MyNamespace.Product product) { - return new Dictionary() + return new Dictionary(2) .SetString("name", product.Name, false, true) .SetDecimal("price", product.Price, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs index 8c69f99..05a0aa0 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) { - return new Dictionary() + return new Dictionary(3) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true) .SetDecimal("price", lineitem.Price, false, true); diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs index ea20447..808198e 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class EventLogMapper private static Dictionary ToItem_LogEntry(global::MyNamespace.LogEntry logentry) { - return new Dictionary() + return new Dictionary(4) .SetDateTime("timestamp", logentry.Timestamp, "O", false, true) .SetString("message", logentry.Message, false, true) .SetInt("severity", logentry.Severity, false, true) diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs index efe9d4a..1877fbc 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class CatalogMapper private static Dictionary ToItem_Product(global::MyNamespace.Product product) { - return new Dictionary() + return new Dictionary(2) .SetString("name", product.Name, false, true) .SetDecimal("price", product.Price, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs index 75a6b15..a3da341 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) { - return new Dictionary() + return new Dictionary(2) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs index 75a6b15..a3da341 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) { - return new Dictionary() + return new Dictionary(2) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs index b0a0543..ef51ee8 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_Address(global::MyNamespace.Address address) { - return new Dictionary() + return new Dictionary(3) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true) .SetString("postalCode", address.PostalCode, false, true); diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs index 49f60c2..3cabee1 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_Customer(global::MyNamespace.Customer customer) { - return new Dictionary() + return new Dictionary(4) .SetString("name", customer.Name, false, true) .Set("address", customer.Address is null ? new AttributeValue { NULL = true } : new AttributeValue { M = new Dictionary().SetString("line1", customer.Address.Line1, false, true).SetString("city", customer.Address.City, false, true) }); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs index 4496237..2ed1a31 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_Address(global::MyNamespace.Address address) { - return new Dictionary() + return new Dictionary(2) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs index b0a0543..ef51ee8 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_Address(global::MyNamespace.Address address) { - return new Dictionary() + return new Dictionary(3) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true) .SetString("postalCode", address.PostalCode, false, true); diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs index 172bd15..4b4304f 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs @@ -40,7 +40,7 @@ public static partial class OrderMapper private static Dictionary ToItem_Address(global::MyNamespace.Address address) { - return new Dictionary() + return new Dictionary(3) .SetString("addr_line1", address.Line1, false, true) .SetString("addr_city", address.City, false, true) .SetString("postalCode", address.PostalCode, false, true); diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs index 5df1975..bd45ef6 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs @@ -42,7 +42,7 @@ public static partial class ProductMapper private static Dictionary ToItem_ProductDetails(global::MyNamespace.ProductDetails productdetails) { - return new Dictionary() + return new Dictionary(4) .SetDecimal("price", productdetails.Price, false, true) .SetInt("stockCount", productdetails.StockCount, false, true) .SetDateTime("lastUpdated", productdetails.LastUpdated, "O", false, true) From 8f7d5846053dd10b9a64d7ad34481dcdafe1e97c Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 11:07:10 -0500 Subject: [PATCH 03/17] feat(tests): add test for multi-level nested object mappings - Introduced `NestedObject_MultipleLevels` test to verify mappings for deeply nested objects. - Added sample classes (`Level1` to `Level4`) to test `ToItem` and `FromItem` methods. - Updated test assertions to validate mappings across multiple levels. --- .../NestedObjectVerifyTests.cs | 1106 +++++++++-------- 1 file changed, 587 insertions(+), 519 deletions(-) diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs index 5bf5ad9..b6d0fe5 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs @@ -3,11 +3,11 @@ namespace LayeredCraft.DynamoMapper.Generators.Tests; public class NestedObjectVerifyTests { [Fact] - public async Task NestedObject_SimpleInline() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_SimpleInline() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -35,16 +35,16 @@ public class Address public string PostalCode { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] - public async Task NestedObject_NullableInline() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_NullableInline() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -71,16 +71,16 @@ public class Address public string City { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] - public async Task NestedObject_NullableMapperBased() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_NullableMapperBased() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -115,16 +115,16 @@ public class Address public string City { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] - public async Task NestedObject_MapperBased() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_MapperBased() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -160,59 +160,60 @@ public class Address public string PostalCode { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] public async Task NestedObject_MapperBased_MissingFromMethod_FallsBackToInline() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class AddressMapper - { - public static partial Dictionary ToItem(Address source); - } - - [DynamoMapper] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public Address ShippingAddress { get; set; } - } - - public class Address - { - public string Line1 { get; set; } - public string City { get; set; } - public string PostalCode { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class AddressMapper + { + public static partial Dictionary ToItem(Address source); + } + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public Address ShippingAddress { get; set; } + } + + public class Address + { + public string Line1 { get; set; } + public string City { get; set; } + public string PostalCode { get; set; } + } + """, }, TestContext.Current.CancellationToken ); [Fact] - public async Task NestedObject_MultiLevel() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_MultiLevel() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -245,16 +246,16 @@ public class Address public string City { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] - public async Task NestedObject_WithScalarTypes() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_WithScalarTypes() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System; using System.Collections.Generic; using Amazon.DynamoDBv2.Model; @@ -285,16 +286,16 @@ public class ProductDetails public bool IsActive { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] - public async Task NestedObject_WithDotNotationOverride() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedObject_WithDotNotationOverride() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -324,9 +325,9 @@ public class Address public string PostalCode { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); // ==================== NESTED COLLECTION TESTS ==================== @@ -335,34 +336,35 @@ public async Task NestedCollection_ListOfNestedObjects_Inline() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public List Items { get; set; } - } - - public class LineItem - { - public string ProductId { get; set; } - public int Quantity { get; set; } - public decimal Price { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public List Items { get; set; } + } + + public class LineItem + { + public string ProductId { get; set; } + public int Quantity { get; set; } + public decimal Price { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -372,42 +374,43 @@ public async Task NestedCollection_ListOfNestedObjects_MapperBased() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class LineItemMapper - { - public static partial Dictionary ToItem(LineItem source); - - public static partial LineItem FromItem(Dictionary item); - } - - [DynamoMapper] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public List Items { get; set; } - } - - public class LineItem - { - public string ProductId { get; set; } - public int Quantity { get; set; } - public decimal Price { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class LineItemMapper + { + public static partial Dictionary ToItem(LineItem source); + + public static partial LineItem FromItem(Dictionary item); + } + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public List Items { get; set; } + } + + public class LineItem + { + public string ProductId { get; set; } + public int Quantity { get; set; } + public decimal Price { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -417,43 +420,44 @@ public async Task NestedCollection_NullableListOfNestedObjects() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public List? Items { get; set; } - } - - public class LineItem - { - public string ProductId { get; set; } - public int Quantity { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public List? Items { get; set; } + } + + public class LineItem + { + public string ProductId { get; set; } + public int Quantity { get; set; } + } + """, }, TestContext.Current.CancellationToken ); [Fact] - public async Task NestedCollection_ArrayOfNestedObjects() => - await GeneratorTestHelpers.Verify( - new VerifyTestOptions - { - SourceCode = """ + public async Task NestedCollection_ArrayOfNestedObjects() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; @@ -480,42 +484,43 @@ public class LineItem public int Quantity { get; set; } } """, - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); [Fact] public async Task NestedCollection_DictionaryOfNestedObjects_Inline() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class CatalogMapper - { - public static partial Dictionary ToItem(Catalog source); - - public static partial Catalog FromItem(Dictionary item); - } - - public class Catalog - { - public string Id { get; set; } - public Dictionary Products { get; set; } - } - - public class Product - { - public string Name { get; set; } - public decimal Price { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class CatalogMapper + { + public static partial Dictionary ToItem(Catalog source); + + public static partial Catalog FromItem(Dictionary item); + } + + public class Catalog + { + public string Id { get; set; } + public Dictionary Products { get; set; } + } + + public class Product + { + public string Name { get; set; } + public decimal Price { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -525,41 +530,42 @@ public async Task NestedCollection_DictionaryOfNestedObjects_MapperBased() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class ProductMapper - { - public static partial Dictionary ToItem(Product source); - - public static partial Product FromItem(Dictionary item); - } - - [DynamoMapper] - public static partial class CatalogMapper - { - public static partial Dictionary ToItem(Catalog source); - - public static partial Catalog FromItem(Dictionary item); - } - - public class Catalog - { - public string Id { get; set; } - public Dictionary Products { get; set; } - } - - public class Product - { - public string Name { get; set; } - public decimal Price { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class ProductMapper + { + public static partial Dictionary ToItem(Product source); + + public static partial Product FromItem(Dictionary item); + } + + [DynamoMapper] + public static partial class CatalogMapper + { + public static partial Dictionary ToItem(Catalog source); + + public static partial Catalog FromItem(Dictionary item); + } + + public class Catalog + { + public string Id { get; set; } + public Dictionary Products { get; set; } + } + + public class Product + { + public string Name { get; set; } + public decimal Price { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -569,36 +575,37 @@ public async Task NestedCollection_ListWithNestedObjectContainingScalarTypes() = await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System; - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class EventLogMapper - { - public static partial Dictionary ToItem(EventLog source); - - public static partial EventLog FromItem(Dictionary item); - } - - public class EventLog - { - public string Id { get; set; } - public List Entries { get; set; } - } - - public class LogEntry - { - public DateTime Timestamp { get; set; } - public string Message { get; set; } - public int Severity { get; set; } - public bool IsError { get; set; } - } - """, + SourceCode = + """ + using System; + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class EventLogMapper + { + public static partial Dictionary ToItem(EventLog source); + + public static partial EventLog FromItem(Dictionary item); + } + + public class EventLog + { + public string Id { get; set; } + public List Entries { get; set; } + } + + public class LogEntry + { + public DateTime Timestamp { get; set; } + public string Message { get; set; } + public int Severity { get; set; } + public bool IsError { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -610,33 +617,34 @@ public async Task NestedCollection_NullableListOfNestedObjects_Inline() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public List? Items { get; set; } - } - - public class LineItem - { - public string ProductId { get; set; } - public int Quantity { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public List? Items { get; set; } + } + + public class LineItem + { + public string ProductId { get; set; } + public int Quantity { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -646,41 +654,42 @@ public async Task NestedCollection_NullableListOfNestedObjects_MapperBased() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class LineItemMapper - { - public static partial Dictionary ToItem(LineItem source); - - public static partial LineItem FromItem(Dictionary item); - } - - [DynamoMapper] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public List? Items { get; set; } - } - - public class LineItem - { - public string ProductId { get; set; } - public int Quantity { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class LineItemMapper + { + public static partial Dictionary ToItem(LineItem source); + + public static partial LineItem FromItem(Dictionary item); + } + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public List? Items { get; set; } + } + + public class LineItem + { + public string ProductId { get; set; } + public int Quantity { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -690,33 +699,34 @@ public async Task NestedCollection_NullableDictionaryOfNestedObjects_Inline() => await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class CatalogMapper - { - public static partial Dictionary ToItem(Catalog source); - - public static partial Catalog FromItem(Dictionary item); - } - - public class Catalog - { - public string Id { get; set; } - public Dictionary? Products { get; set; } - } - - public class Product - { - public string Name { get; set; } - public decimal Price { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class CatalogMapper + { + public static partial Dictionary ToItem(Catalog source); + + public static partial Catalog FromItem(Dictionary item); + } + + public class Catalog + { + public string Id { get; set; } + public Dictionary? Products { get; set; } + } + + public class Product + { + public string Name { get; set; } + public decimal Price { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -726,41 +736,42 @@ public async Task NestedCollection_NullableDictionaryOfNestedObjects_MapperBased await GeneratorTestHelpers.Verify( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class ProductMapper - { - public static partial Dictionary ToItem(Product source); - - public static partial Product FromItem(Dictionary item); - } - - [DynamoMapper] - public static partial class CatalogMapper - { - public static partial Dictionary ToItem(Catalog source); - - public static partial Catalog FromItem(Dictionary item); - } - - public class Catalog - { - public string Id { get; set; } - public Dictionary? Products { get; set; } - } - - public class Product - { - public string Name { get; set; } - public decimal Price { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class ProductMapper + { + public static partial Dictionary ToItem(Product source); + + public static partial Product FromItem(Dictionary item); + } + + [DynamoMapper] + public static partial class CatalogMapper + { + public static partial Dictionary ToItem(Catalog source); + + public static partial Catalog FromItem(Dictionary item); + } + + public class Catalog + { + public string Id { get; set; } + public Dictionary? Products { get; set; } + } + + public class Product + { + public string Name { get; set; } + public decimal Price { get; set; } + } + """, }, TestContext.Current.CancellationToken ); @@ -772,27 +783,28 @@ public async Task NestedObject_CycleDetected_ShouldFail_DM0006() => await GeneratorTestHelpers.VerifyFailure( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class PersonMapper - { - public static partial Dictionary ToItem(Person source); - - public static partial Person FromItem(Dictionary item); - } - - public class Person - { - public string Name { get; set; } - public Person Parent { get; set; } // Self-referencing cycle - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class PersonMapper + { + public static partial Dictionary ToItem(Person source); + + public static partial Person FromItem(Dictionary item); + } + + public class Person + { + public string Name { get; set; } + public Person Parent { get; set; } // Self-referencing cycle + } + """, ExpectedDiagnosticId = "DM0006", }, TestContext.Current.CancellationToken @@ -803,33 +815,34 @@ public async Task NestedObject_IndirectCycleDetected_ShouldFail_DM0006() => await GeneratorTestHelpers.VerifyFailure( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class NodeAMapper - { - public static partial Dictionary ToItem(NodeA source); - - public static partial NodeA FromItem(Dictionary item); - } - - public class NodeA - { - public string Id { get; set; } - public NodeB Child { get; set; } // A -> B -> A cycle - } - - public class NodeB - { - public string Id { get; set; } - public NodeA Parent { get; set; } // Back reference creates cycle - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class NodeAMapper + { + public static partial Dictionary ToItem(NodeA source); + + public static partial NodeA FromItem(Dictionary item); + } + + public class NodeA + { + public string Id { get; set; } + public NodeB Child { get; set; } // A -> B -> A cycle + } + + public class NodeB + { + public string Id { get; set; } + public NodeA Parent { get; set; } // Back reference creates cycle + } + """, ExpectedDiagnosticId = "DM0006", }, TestContext.Current.CancellationToken @@ -840,27 +853,28 @@ public async Task NestedCollection_CycleDetected_ShouldFail_DM0006() => await GeneratorTestHelpers.VerifyFailure( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - public static partial class NodeMapper - { - public static partial Dictionary ToItem(Node source); - - public static partial Node FromItem(Dictionary item); - } - - public class Node - { - public string Name { get; set; } - public List Children { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + public static partial class NodeMapper + { + public static partial Dictionary ToItem(Node source); + + public static partial Node FromItem(Dictionary item); + } + + public class Node + { + public string Name { get; set; } + public List Children { get; set; } + } + """, ExpectedDiagnosticId = "DM0006", }, TestContext.Current.CancellationToken @@ -871,34 +885,35 @@ public async Task NestedObject_InvalidDotNotationPath_ShouldFail_DM0008() => await GeneratorTestHelpers.VerifyFailure( new VerifyTestOptions { - SourceCode = """ - using System.Collections.Generic; - using Amazon.DynamoDBv2.Model; - using DynamoMapper.Runtime; - - namespace MyNamespace; - - [DynamoMapper] - [DynamoField("Address.NonExistentProperty", AttributeName = "bad_path")] - public static partial class OrderMapper - { - public static partial Dictionary ToItem(Order source); - - public static partial Order FromItem(Dictionary item); - } - - public class Order - { - public string Id { get; set; } - public Address Address { get; set; } - } - - public class Address - { - public string Line1 { get; set; } - public string City { get; set; } - } - """, + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + [DynamoField("Address.NonExistentProperty", AttributeName = "bad_path")] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public Address Address { get; set; } + } + + public class Address + { + public string Line1 { get; set; } + public string City { get; set; } + } + """, ExpectedDiagnosticId = "DM0008", }, TestContext.Current.CancellationToken @@ -909,36 +924,89 @@ public async Task NestedObject_InvalidDotNotationPath_NonExistentProperty_Should await GeneratorTestHelpers.VerifyFailure( new VerifyTestOptions { - SourceCode = """ + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + [DynamoMapper] + [DynamoField("NonExistentProperty.Line1", AttributeName = "bad_path")] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + + public static partial Order FromItem(Dictionary item); + } + + public class Order + { + public string Id { get; set; } + public Address Address { get; set; } + } + + public class Address + { + public string Line1 { get; set; } + public string City { get; set; } + } + """, + ExpectedDiagnosticId = "DM0008", + }, + TestContext.Current.CancellationToken + ); + + [Fact] + public async Task NestedObject_MultipleLevels() => await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ + namespace MyNamespace; + using System.Collections.Generic; using Amazon.DynamoDBv2.Model; using DynamoMapper.Runtime; - namespace MyNamespace; + public class Level1 + { + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public Level2? Level2Data { get; set; } + } - [DynamoMapper] - [DynamoField("NonExistentProperty.Line1", AttributeName = "bad_path")] - public static partial class OrderMapper + public class Level2 { - public static partial Dictionary ToItem(Order source); + public string Id { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public Level3? Level3Data { get; set; } + } - public static partial Order FromItem(Dictionary item); + public class Level3 + { + public string Id { get; set; } = string.Empty; + public int Value { get; set; } + public Level4? Level4Data { get; set; } } - public class Order + public class Level4 { - public string Id { get; set; } - public Address Address { get; set; } + public string Id { get; set; } = string.Empty; + public bool IsActive { get; set; } + public decimal Price { get; set; } } - public class Address + [DynamoMapper] + public static partial class Level1Mapper { - public string Line1 { get; set; } - public string City { get; set; } + public static partial Dictionary ToItem(Level1 source); + + public static partial Level1 FromItem(Dictionary item); } """, - ExpectedDiagnosticId = "DM0008", - }, - TestContext.Current.CancellationToken - ); + }, + TestContext.Current.CancellationToken + ); } From d2a684df46940e8b3866c0fdf3e18fcd75ad04ec Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 12:52:10 -0500 Subject: [PATCH 04/17] feat(emitters): refactor to support reusable helper methods for nested mappings - Extended `HelperMethodRegistry` to manage registration of reusable helper methods. - Refactored `RenderInlineNestedToItem` and `RenderInlineNestedFromItem` to utilize the helper registry. - Enhanced support for iterative rendering of helper methods in `MapperEmitter`. - Updated snapshots to align with new helper methods for deeply nested mapping scenarios. --- .../Emitters/HelperMethodEmitter.cs | 14 ++- .../Emitters/MapperEmitter.cs | 53 ++++++--- .../Models/MapperInfo.cs | 18 +++- .../Models/ModelClassInfo.cs | 6 +- .../PropertyMappingCodeRenderer.cs | 70 ++++++++---- ..._MultipleLevels#Level1Mapper.g.verified.cs | 102 ++++++++++++++++++ 6 files changed, 215 insertions(+), 48 deletions(-) create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index efb9441..829e969 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -8,7 +8,9 @@ namespace DynamoMapper.Generator.Emitters; internal static class HelperMethodEmitter { /// Renders a ToItem helper method. - public static string RenderToItemHelper(HelperMethodInfo helper, GeneratorContext context) + public static string RenderToItemHelper( + HelperMethodInfo helper, GeneratorContext context, HelperMethodRegistry helperRegistry + ) { var sb = new StringBuilder(); var typeName = ExtractSimpleTypeName(helper.ModelFullyQualifiedType); @@ -27,7 +29,8 @@ public static string RenderToItemHelper(HelperMethodInfo helper, GeneratorContex PropertyMappingCodeRenderer.RenderInlineNestedToItem( paramName, helper.InlineInfo, - context + context, + helperRegistry ); // Format chained method calls on separate lines @@ -40,7 +43,9 @@ public static string RenderToItemHelper(HelperMethodInfo helper, GeneratorContex } /// Renders a FromItem helper method. - public static string RenderFromItemHelper(HelperMethodInfo helper, GeneratorContext context) + public static string RenderFromItemHelper( + HelperMethodInfo helper, GeneratorContext context, HelperMethodRegistry helperRegistry + ) { var sb = new StringBuilder(); var mapParamName = "map"; @@ -58,7 +63,8 @@ public static string RenderFromItemHelper(HelperMethodInfo helper, GeneratorCont PropertyMappingCodeRenderer.RenderInlineNestedFromItem( mapParamName, helper.InlineInfo, - context + context, + helperRegistry ); // Format object initializer on separate lines diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs index 1cae88d..61868d0 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs @@ -43,23 +43,46 @@ internal static void Generate(SourceProductionContext context, MapperInfo mapper .ToArray(); // Render helper methods for nested objects + // We need to render iteratively because rendering a helper might register new helpers var helperMethods = Array.Empty(); if (mapperInfo.MapperClass is not null && mapperInfo.Context is not null && - mapperInfo.MapperClass.HelperMethods.Any()) - helperMethods = - mapperInfo.MapperClass.HelperMethods.Select( - helper => - helper.Direction == HelperMethodDirection.ToItem - ? HelperMethodEmitter.RenderToItemHelper( - helper, - mapperInfo.Context! - ) - : HelperMethodEmitter.RenderFromItemHelper( - helper, - mapperInfo.Context! - ) - ) - .ToArray(); + mapperInfo.HelperRegistry is not null && mapperInfo.MapperClass.HelperMethods.Any()) + { + var renderedHelpers = new HashSet(); + var helperList = new List(); + + // Keep rendering until all helpers are processed + while (true) + { + var allHelpers = mapperInfo.HelperRegistry.GetAllHelpers(); + var newHelpers = + allHelpers.Where(h => !renderedHelpers.Contains(h.MethodName)).ToArray(); + + if (newHelpers.Length == 0) + break; + + foreach (var helper in newHelpers) + { + var rendered = + helper.Direction == HelperMethodDirection.ToItem + ? HelperMethodEmitter.RenderToItemHelper( + helper, + mapperInfo.Context!, + mapperInfo.HelperRegistry! + ) + : HelperMethodEmitter.RenderFromItemHelper( + helper, + mapperInfo.Context!, + mapperInfo.HelperRegistry! + ); + + helperList.Add(rendered); + renderedHelpers.Add(helper.MethodName); + } + } + + helperMethods = helperList.ToArray(); + } var model = new diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs index 24283f6..7fedcf7 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs @@ -1,4 +1,5 @@ using DynamoMapper.Generator.Diagnostics; +using DynamoMapper.Generator.PropertyMapping; using LayeredCraft.SourceGeneratorTools.Types; using Microsoft.CodeAnalysis; @@ -8,7 +9,8 @@ internal sealed record MapperInfo( MapperClassInfo? MapperClass, ModelClassInfo? ModelClass, EquatableArray Diagnostics, - GeneratorContext? Context + GeneratorContext? Context, + HelperMethodRegistry? HelperRegistry ); internal static class MapperInfoExtensions @@ -32,11 +34,15 @@ internal static MapperInfo Create(INamedTypeSymbol classSymbol, GeneratorContext context.HasToItemMethod = mapperClassInfo.ToItemSignature != null; context.HasFromItemMethod = mapperClassInfo.FromItemSignature != null; + // Create registry to track helper methods for nested objects + var helperRegistry = new HelperMethodRegistry(); + var (modelClassInfo, diagnosticInfos, helperMethods) = ModelClassInfo.Create( modelTypeSymbol, mapperClassInfo.FromItemParameterName, - context + context, + helperRegistry ); // Add helper methods to mapper class info @@ -50,7 +56,8 @@ mapperClassInfo with updatedMapperClassInfo, modelClassInfo, diagnosticInfos.ToEquatableArray(), - context + context, + helperRegistry ); } @@ -59,7 +66,8 @@ mapperClassInfo with /// normal analysis. /// private static MapperInfo CreateWithDiagnostics( - IEnumerable diagnostics, GeneratorContext? context = null - ) => new(null, null, diagnostics.ToEquatableArray(), context); + IEnumerable diagnostics, GeneratorContext? context = null, + HelperMethodRegistry? helperRegistry = null + ) => new(null, null, diagnostics.ToEquatableArray(), context, helperRegistry); } } diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs index cba501a..e74e94d 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/ModelClassInfo.cs @@ -19,7 +19,8 @@ internal static class ModelClassInfoExtensions extension(ModelClassInfo) { internal static (ModelClassInfo?, DiagnosticInfo[], HelperMethodInfo[]) Create( - ITypeSymbol modelTypeSymbol, string? fromItemParameterName, GeneratorContext context + ITypeSymbol modelTypeSymbol, string? fromItemParameterName, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { context.ThrowIfCancellationRequested(); @@ -44,9 +45,6 @@ internal static (ModelClassInfo?, DiagnosticInfo[], HelperMethodInfo[]) Create( if (dotNotationDiagnostics.Length > 0) return (null, dotNotationDiagnostics, []); - // Create helper registry for tracking nested object helper methods - var helperRegistry = new HelperMethodRegistry(); - var (propertyInfos, propertyInfosByIndex, propertyDiagnostics) = CreatePropertyInfos( properties, diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs index ba9355e..922a379 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs @@ -365,7 +365,8 @@ private static string RenderInlineToAssignment( /// Made internal to allow reuse by HelperMethodEmitter. /// internal static string RenderInlineNestedToItem( - string sourcePrefix, NestedInlineInfo inlineInfo, GeneratorContext context + string sourcePrefix, NestedInlineInfo inlineInfo, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { var sb = new StringBuilder(); @@ -377,15 +378,29 @@ internal static string RenderInlineNestedToItem( { // Recursive nested object var nestedSourcePrefix = $"{sourcePrefix}.{prop.PropertyName}"; - var nestedCode = - prop.NestedMapping switch - { - MapperBasedNesting mapperBased => - $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem({nestedSourcePrefix}) }}", - InlineNesting inline => - $"new AttributeValue {{ M = {RenderInlineNestedToItem(nestedSourcePrefix, inline.Info, context)} }}", - _ => throw new InvalidOperationException("Unknown nested mapping type"), - }; + + string nestedCode; + if (prop.NestedMapping is MapperBasedNesting mapperBased) + { + nestedCode = + $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem({nestedSourcePrefix}) }}"; + } + else if (prop.NestedMapping is InlineNesting inline) + { + // Register helper and generate call instead of inlining + var helperMethodName = + helperRegistry.GetOrRegisterToItemHelper( + inline.Info.ModelFullyQualifiedType, + inline.Info + ); + nestedCode = + $"new AttributeValue {{ M = {helperMethodName}({nestedSourcePrefix}) }}"; + } + else + { + throw new InvalidOperationException("Unknown nested mapping type"); + } + sb.Append( $".Set(\"{prop.DynamoKey}\", {nestedSourcePrefix} is null ? new AttributeValue {{ NULL = true }} : {nestedCode})" ); @@ -513,7 +528,8 @@ private static string RenderInlineFromInitAssignment( /// Made internal to allow reuse by HelperMethodEmitter. /// internal static string RenderInlineNestedFromItem( - string mapVarName, NestedInlineInfo inlineInfo, GeneratorContext context + string mapVarName, NestedInlineInfo inlineInfo, GeneratorContext context, + HelperMethodRegistry helperRegistry ) { var sb = new StringBuilder(); @@ -529,15 +545,29 @@ internal static string RenderInlineNestedFromItem( { // Recursive nested object var nestedVarName = $"{mapVarName}_{prop.PropertyName.ToLowerInvariant()}"; - var nestedCode = - prop.NestedMapping switch - { - MapperBasedNesting mapperBased => - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : null", - InlineNesting inline => - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {RenderInlineNestedFromItem(nestedVarName, inline.Info, context)} : null", - _ => throw new InvalidOperationException("Unknown nested mapping type"), - }; + + string nestedCode; + if (prop.NestedMapping is MapperBasedNesting mapperBased) + { + nestedCode = + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : null"; + } + else if (prop.NestedMapping is InlineNesting inline) + { + // Register helper and generate call instead of inlining + var helperMethodName = + helperRegistry.GetOrRegisterFromItemHelper( + inline.Info.ModelFullyQualifiedType, + inline.Info + ); + nestedCode = + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {helperMethodName}({nestedVarName}) : null"; + } + else + { + throw new InvalidOperationException("Unknown nested mapping type"); + } + sb.Append($"{prop.PropertyName} = {nestedCode},"); } else if (prop.Strategy is not null) diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs new file mode 100644 index 0000000..628f05c --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs @@ -0,0 +1,102 @@ +//HintName: Level1Mapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +public static partial class Level1Mapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Level1 source) => + new Dictionary(3) + .SetString("id", source.Id, false, true) + .SetString("name", source.Name, false, true) + .Set("level2Data", source.Level2Data is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Level2(source.Level2Data) }); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + public static partial global::MyNamespace.Level1 FromItem(global::System.Collections.Generic.Dictionary item) + { + var level1 = new global::MyNamespace.Level1 + { + Level2Data = item.TryGetValue("level2Data", out var level2dataAttr) && level2dataAttr.M is { } level2dataMap ? FromItem_Level2(level2dataMap) : null, + }; + if (item.TryGetString("id", out var var0, Requiredness.InferFromNullability)) level1.Id = var0!; + if (item.TryGetString("name", out var var1, Requiredness.InferFromNullability)) level1.Name = var1!; + return level1; + } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Level2(global::MyNamespace.Level2 level2) + { + return new Dictionary(3) + .SetString("id", level2.Id, false, true) + .SetString("description", level2.Description, false, true) + .Set("level3Data", level2.Level3Data is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Level3(level2.Level3Data) }); + } + + + private static global::MyNamespace.Level2 FromItem_Level2(Dictionary map) + { + return new global::MyNamespace.Level2 + { + Id = map.GetString("id", Requiredness.Optional), + Description = map.GetString("description", Requiredness.Optional), + Level3Data = map.TryGetValue("level3Data", out var map_level3dataAttr) && map_level3dataAttr.M is { } map_level3data ? FromItem_Level3(map_level3data) : null, + }; + } + + + private static Dictionary ToItem_Level3(global::MyNamespace.Level3 level3) + { + return new Dictionary(3) + .SetString("id", level3.Id, false, true) + .SetInt("value", level3.Value, false, true) + .Set("level4Data", level3.Level4Data is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Level4(level3.Level4Data) }); + } + + + private static global::MyNamespace.Level3 FromItem_Level3(Dictionary map) + { + return new global::MyNamespace.Level3 + { + Id = map.GetString("id", Requiredness.Optional), + Value = map.GetInt("value", Requiredness.Optional), + Level4Data = map.TryGetValue("level4Data", out var map_level4dataAttr) && map_level4dataAttr.M is { } map_level4data ? FromItem_Level4(map_level4data) : null, + }; + } + + + private static Dictionary ToItem_Level4(global::MyNamespace.Level4 level4) + { + return new Dictionary(3) + .SetString("id", level4.Id, false, true) + .SetBool("isActive", level4.IsActive, false, true) + .SetDecimal("price", level4.Price, false, true); + } + + + private static global::MyNamespace.Level4 FromItem_Level4(Dictionary map) + { + return new global::MyNamespace.Level4 + { + Id = map.GetString("id", Requiredness.Optional), + IsActive = map.GetBool("isActive", Requiredness.Optional), + Price = map.GetDecimal("price", Requiredness.Optional), + }; + } + +} From d9c49b6da82d28eac8edecfbe9a91509dbd13367 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 12:52:59 -0500 Subject: [PATCH 05/17] feat(tests): update snapshots to use helper methods for nested object address mappings - Refactored `ToItem_Customer` and `FromItem_Customer` to delegate address mappings to new helper methods. - Added `ToItem_Address` and `FromItem_Address` for better modularity and reuse. - Updated dictionary initialization to reflect more accurate capacity. --- ...bject_MultiLevel#OrderMapper.g.verified.cs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs index 3cabee1..585a01d 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs @@ -40,9 +40,9 @@ public static partial class OrderMapper private static Dictionary ToItem_Customer(global::MyNamespace.Customer customer) { - return new Dictionary(4) + return new Dictionary(2) .SetString("name", customer.Name, false, true) - .Set("address", customer.Address is null ? new AttributeValue { NULL = true } : new AttributeValue { M = new Dictionary().SetString("line1", customer.Address.Line1, false, true).SetString("city", customer.Address.City, false, true) }); + .Set("address", customer.Address is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Address(customer.Address) }); } @@ -51,9 +51,25 @@ private static Dictionary ToItem_Customer(global::MyName return new global::MyNamespace.Customer { Name = map.GetString("name", Requiredness.Optional), - Address = map.TryGetValue("address", out var map_addressAttr) && map_addressAttr.M is { } map_address ? new global::MyNamespace.Address { Line1 = map_address.GetString("line1", Requiredness.Optional), - City = map_address.GetString("city", Requiredness.Optional), - } : null, + Address = map.TryGetValue("address", out var map_addressAttr) && map_addressAttr.M is { } map_address ? FromItem_Address(map_address) : null, + }; + } + + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) + { + return new Dictionary(2) + .SetString("line1", address.Line1, false, true) + .SetString("city", address.City, false, true); + } + + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + Line1 = map.GetString("line1", Requiredness.Optional), + City = map.GetString("city", Requiredness.Optional), }; } From 7ee307845f6597520c8446e7abe48d45d2b8e948 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 13:02:23 -0500 Subject: [PATCH 06/17] refactor(emitters): switch helper methods to use expression-bodied members - Replaced block-bodied methods with arrow syntax for cleaner code. - Updated snapshots to align with the new method formatting. - Removed unnecessary braces and adjusted formatting accordingly. --- .../Emitters/HelperMethodEmitter.cs | 10 ++++----- ...dSucceed#ExampleEntityMapper.g.verified.cs | 7 ++----- ...thOptIn_Succeeds#OrderMapper.g.verified.cs | 7 ++----- ...yOfNestedObjects#OrderMapper.g.verified.cs | 7 ++----- ...Objects_Inline#CatalogMapper.g.verified.cs | 7 ++----- ...edObjects_Inline#OrderMapper.g.verified.cs | 7 ++----- ...ngScalarTypes#EventLogMapper.g.verified.cs | 7 ++----- ...Objects_Inline#CatalogMapper.g.verified.cs | 7 ++----- ...tOfNestedObjects#OrderMapper.g.verified.cs | 7 ++----- ...edObjects_Inline#OrderMapper.g.verified.cs | 7 ++----- ...allsBackToInline#OrderMapper.g.verified.cs | 7 ++----- ...bject_MultiLevel#OrderMapper.g.verified.cs | 14 ++++--------- ..._MultipleLevels#Level1Mapper.g.verified.cs | 21 ++++++------------- ...t_NullableInline#OrderMapper.g.verified.cs | 7 ++----- ...ect_SimpleInline#OrderMapper.g.verified.cs | 7 ++----- ...NotationOverride#OrderMapper.g.verified.cs | 7 ++----- ...ithScalarTypes#ProductMapper.g.verified.cs | 7 ++----- 17 files changed, 42 insertions(+), 101 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index 829e969..cd2a9f9 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -16,13 +16,12 @@ public static string RenderToItemHelper( var typeName = ExtractSimpleTypeName(helper.ModelFullyQualifiedType); var paramName = typeName.ToLowerInvariant(); - // Method signature with block syntax (no leading spaces - template handles base + // Method signature with arrow syntax (no leading spaces - template handles base // indentation) sb.AppendLine( - $"private static Dictionary {helper.MethodName}({helper.ModelFullyQualifiedType} {paramName})" + $"private static Dictionary {helper.MethodName}({helper.ModelFullyQualifiedType} {paramName}) =>" ); - sb.AppendLine("{"); - sb.Append(" return "); + sb.Append(" "); // Reuse existing RenderInlineNestedToItem logic var bodyCode = @@ -36,8 +35,7 @@ public static string RenderToItemHelper( // Format chained method calls on separate lines var formattedBody = FormatToItemChainedCalls(bodyCode); sb.Append(formattedBody); - sb.AppendLine(";"); - sb.AppendLine("}"); + sb.Append(";"); return sb.ToString(); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs index acfdede..2abc79c 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/CollectionVerifyTests.Collection_NestedObjectElementType_ShouldSucceed#ExampleEntityMapper.g.verified.cs @@ -36,12 +36,9 @@ public static partial class ExampleEntityMapper // Helper methods for nested object mapping - private static Dictionary ToItem_CustomClass(global::MyNamespace.CustomClass customclass) - { - return new Dictionary(1) + private static Dictionary ToItem_CustomClass(global::MyNamespace.CustomClass customclass) => + new Dictionary(1) .SetString("name", customclass.Name, false, true); - } - private static global::MyNamespace.CustomClass FromItem_CustomClass(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs index b4b3a35..da93612 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs @@ -36,13 +36,10 @@ internal static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Address(global::MyNamespace.Address address) - { - return new Dictionary(2) + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(2) .SetString("city", address.City, false, true) .SetString("line_1", address.Line1, false, true); - } - private static global::MyNamespace.Address FromItem_Address(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs index 4ebd226..9f2202f 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ArrayOfNestedObjects#OrderMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) - { - return new Dictionary(2) + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) => + new Dictionary(2) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true); - } - private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs index 140aa71..01ce6a7 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_DictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class CatalogMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Product(global::MyNamespace.Product product) - { - return new Dictionary(2) + private static Dictionary ToItem_Product(global::MyNamespace.Product product) => + new Dictionary(2) .SetString("name", product.Name, false, true) .SetDecimal("price", product.Price, false, true); - } - private static global::MyNamespace.Product FromItem_Product(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs index 05a0aa0..e249df1 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListOfNestedObjects_Inline#OrderMapper.g.verified.cs @@ -38,14 +38,11 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) - { - return new Dictionary(3) + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) => + new Dictionary(3) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true) .SetDecimal("price", lineitem.Price, false, true); - } - private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs index 808198e..c1358b1 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_ListWithNestedObjectContainingScalarTypes#EventLogMapper.g.verified.cs @@ -38,15 +38,12 @@ public static partial class EventLogMapper // Helper methods for nested object mapping - private static Dictionary ToItem_LogEntry(global::MyNamespace.LogEntry logentry) - { - return new Dictionary(4) + private static Dictionary ToItem_LogEntry(global::MyNamespace.LogEntry logentry) => + new Dictionary(4) .SetDateTime("timestamp", logentry.Timestamp, "O", false, true) .SetString("message", logentry.Message, false, true) .SetInt("severity", logentry.Severity, false, true) .SetBool("isError", logentry.IsError, false, true); - } - private static global::MyNamespace.LogEntry FromItem_LogEntry(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs index 1877fbc..b6f6e29 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableDictionaryOfNestedObjects_Inline#CatalogMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class CatalogMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Product(global::MyNamespace.Product product) - { - return new Dictionary(2) + private static Dictionary ToItem_Product(global::MyNamespace.Product product) => + new Dictionary(2) .SetString("name", product.Name, false, true) .SetDecimal("price", product.Price, false, true); - } - private static global::MyNamespace.Product FromItem_Product(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs index a3da341..cfe9f11 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects#OrderMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) - { - return new Dictionary(2) + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) => + new Dictionary(2) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true); - } - private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs index a3da341..cfe9f11 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedCollection_NullableListOfNestedObjects_Inline#OrderMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) - { - return new Dictionary(2) + private static Dictionary ToItem_LineItem(global::MyNamespace.LineItem lineitem) => + new Dictionary(2) .SetString("productId", lineitem.ProductId, false, true) .SetInt("quantity", lineitem.Quantity, false, true); - } - private static global::MyNamespace.LineItem FromItem_LineItem(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs index ef51ee8..6d62b24 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MapperBased_MissingFromMethod_FallsBackToInline#OrderMapper.g.verified.cs @@ -38,14 +38,11 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Address(global::MyNamespace.Address address) - { - return new Dictionary(3) + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(3) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true) .SetString("postalCode", address.PostalCode, false, true); - } - private static global::MyNamespace.Address FromItem_Address(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs index 585a01d..26a7c4a 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultiLevel#OrderMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Customer(global::MyNamespace.Customer customer) - { - return new Dictionary(2) + private static Dictionary ToItem_Customer(global::MyNamespace.Customer customer) => + new Dictionary(2) .SetString("name", customer.Name, false, true) .Set("address", customer.Address is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Address(customer.Address) }); - } - private static global::MyNamespace.Customer FromItem_Customer(Dictionary map) { @@ -56,13 +53,10 @@ private static Dictionary ToItem_Customer(global::MyName } - private static Dictionary ToItem_Address(global::MyNamespace.Address address) - { - return new Dictionary(2) + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(2) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true); - } - private static global::MyNamespace.Address FromItem_Address(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs index 628f05c..85fa9c7 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs @@ -40,14 +40,11 @@ public static partial class Level1Mapper // Helper methods for nested object mapping - private static Dictionary ToItem_Level2(global::MyNamespace.Level2 level2) - { - return new Dictionary(3) + private static Dictionary ToItem_Level2(global::MyNamespace.Level2 level2) => + new Dictionary(3) .SetString("id", level2.Id, false, true) .SetString("description", level2.Description, false, true) .Set("level3Data", level2.Level3Data is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Level3(level2.Level3Data) }); - } - private static global::MyNamespace.Level2 FromItem_Level2(Dictionary map) { @@ -60,14 +57,11 @@ private static Dictionary ToItem_Level2(global::MyNamesp } - private static Dictionary ToItem_Level3(global::MyNamespace.Level3 level3) - { - return new Dictionary(3) + private static Dictionary ToItem_Level3(global::MyNamespace.Level3 level3) => + new Dictionary(3) .SetString("id", level3.Id, false, true) .SetInt("value", level3.Value, false, true) .Set("level4Data", level3.Level4Data is null ? new AttributeValue { NULL = true } : new AttributeValue { M = ToItem_Level4(level3.Level4Data) }); - } - private static global::MyNamespace.Level3 FromItem_Level3(Dictionary map) { @@ -80,14 +74,11 @@ private static Dictionary ToItem_Level3(global::MyNamesp } - private static Dictionary ToItem_Level4(global::MyNamespace.Level4 level4) - { - return new Dictionary(3) + private static Dictionary ToItem_Level4(global::MyNamespace.Level4 level4) => + new Dictionary(3) .SetString("id", level4.Id, false, true) .SetBool("isActive", level4.IsActive, false, true) .SetDecimal("price", level4.Price, false, true); - } - private static global::MyNamespace.Level4 FromItem_Level4(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs index 2ed1a31..cffbf96 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_NullableInline#OrderMapper.g.verified.cs @@ -38,13 +38,10 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Address(global::MyNamespace.Address address) - { - return new Dictionary(2) + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(2) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true); - } - private static global::MyNamespace.Address FromItem_Address(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs index ef51ee8..6d62b24 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_SimpleInline#OrderMapper.g.verified.cs @@ -38,14 +38,11 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Address(global::MyNamespace.Address address) - { - return new Dictionary(3) + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(3) .SetString("line1", address.Line1, false, true) .SetString("city", address.City, false, true) .SetString("postalCode", address.PostalCode, false, true); - } - private static global::MyNamespace.Address FromItem_Address(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs index 4b4304f..2b07f13 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithDotNotationOverride#OrderMapper.g.verified.cs @@ -38,14 +38,11 @@ public static partial class OrderMapper // Helper methods for nested object mapping - private static Dictionary ToItem_Address(global::MyNamespace.Address address) - { - return new Dictionary(3) + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(3) .SetString("addr_line1", address.Line1, false, true) .SetString("addr_city", address.City, false, true) .SetString("postalCode", address.PostalCode, false, true); - } - private static global::MyNamespace.Address FromItem_Address(Dictionary map) { diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs index bd45ef6..1cdb12f 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithScalarTypes#ProductMapper.g.verified.cs @@ -40,15 +40,12 @@ public static partial class ProductMapper // Helper methods for nested object mapping - private static Dictionary ToItem_ProductDetails(global::MyNamespace.ProductDetails productdetails) - { - return new Dictionary(4) + private static Dictionary ToItem_ProductDetails(global::MyNamespace.ProductDetails productdetails) => + new Dictionary(4) .SetDecimal("price", productdetails.Price, false, true) .SetInt("stockCount", productdetails.StockCount, false, true) .SetDateTime("lastUpdated", productdetails.LastUpdated, "O", false, true) .SetBool("isActive", productdetails.IsActive, false, true); - } - private static global::MyNamespace.ProductDetails FromItem_ProductDetails(Dictionary map) { From a322237242acce06e6270e15d73c4449a50c44b8 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 20:21:10 -0500 Subject: [PATCH 07/17] feat(mappers): enhance nested object property analysis and add requiredness support - Integrated `PropertyAnalyzer` for detailed property analysis in `NestedObjectTypeAnalyzer`. - Added support to track nullability, requiredness, getters, setters, and default values. - Enhanced handling of requiredness in nested object mappings with fallback management. - Updated `NestedPropertySpec` to include new analysis properties. - Refactored code rendering logic to leverage enhanced property analysis. - Added a test case to validate correct requiredness handling for nested properties. - Updated test snapshots to reflect changes in generated mapping code. --- .../Models/NestedMappingInfo.cs | 12 + .../NestedObjectTypeAnalyzer.cs | 325 ++++++++++++------ .../PropertyMappingCodeRenderer.cs | 25 +- .../NestedObjectVerifyTests.cs | 46 +++ ...rectRequiredness#OrderMapper.g.verified.cs | 61 ++++ 5 files changed, 355 insertions(+), 114 deletions(-) create mode 100644 test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/Models/NestedMappingInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/Models/NestedMappingInfo.cs index df8354e..f561fec 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/Models/NestedMappingInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/Models/NestedMappingInfo.cs @@ -41,10 +41,22 @@ EquatableArray Properties /// The C# property name. /// The DynamoDB attribute name. /// The type mapping strategy for this property (can itself be nested). +/// Nullability information from property analysis. +/// Whether the property has an accessible getter. +/// Whether the property has an accessible setter. +/// Whether the property is marked as required. +/// Whether the property is init-only. +/// Whether the property has a default value. /// If the property is itself a nested object, its mapping info. internal sealed record NestedPropertySpec( string PropertyName, string DynamoKey, TypeMappingStrategy? Strategy, + PropertyNullabilityInfo Nullability, + bool HasGetter, + bool HasSetter, + bool IsRequired, + bool IsInitOnly, + bool HasDefaultValue, NestedMappingInfo? NestedMapping = null ); diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs index 1e84e52..198c4f9 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs @@ -1,6 +1,8 @@ using DynamoMapper.Generator.Diagnostics; using DynamoMapper.Generator.Models; using DynamoMapper.Generator.PropertyMapping.Models; +using DynamoMapper.Generator.WellKnownTypes; +using DynamoMapper.Runtime; using LayeredCraft.SourceGeneratorTools.Types; using Microsoft.CodeAnalysis; @@ -23,14 +25,13 @@ internal static class NestedObjectTypeAnalyzer /// or a failure for cycles/unsupported types. /// internal static DiagnosticResult Analyze( - ITypeSymbol type, - string propertyName, - NestedAnalysisContext nestedContext + ITypeSymbol type, string propertyName, NestedAnalysisContext nestedContext ) { nestedContext.Context.ThrowIfCancellationRequested(); - // Check if type is a class or struct with properties (not a primitive, enum, collection, etc.) + // Check if type is a class or struct with properties (not a primitive, enum, collection, + // etc.) if (!IsNestedObjectType(type, nestedContext.Context)) return DiagnosticResult.Success(null); @@ -54,15 +55,14 @@ NestedAnalysisContext nestedContext } // Step 3: Check if a mapper exists for this type and supports required directions - if (nestedContext.Registry.TryGetMapper(type, out var mapperReference) && mapperReference != null) + if (nestedContext.Registry.TryGetMapper(type, out var mapperReference) && + mapperReference != null) { var requiresTo = nestedContext.Context.HasToItemMethod; var requiresFrom = nestedContext.Context.HasFromItemMethod; - if ( - (!requiresTo || mapperReference.HasToItemMethod) - && (!requiresFrom || mapperReference.HasFromItemMethod) - ) + if ((!requiresTo || mapperReference.HasToItemMethod) && + (!requiresFrom || mapperReference.HasFromItemMethod)) { return DiagnosticResult.Success( new MapperBasedNesting(mapperReference) @@ -120,11 +120,11 @@ private static bool IsWellKnownNonNestedType(INamedTypeSymbol type, GeneratorCon // DateTimeOffset, TimeSpan, Guid are well-known scalar types, not nested objects var wellKnown = context.WellKnownTypes; - if (wellKnown.IsType(type, WellKnownTypes.WellKnownTypeData.WellKnownType.System_DateTimeOffset)) + if (wellKnown.IsType(type, WellKnownTypeData.WellKnownType.System_DateTimeOffset)) return true; - if (wellKnown.IsType(type, WellKnownTypes.WellKnownTypeData.WellKnownType.System_TimeSpan)) + if (wellKnown.IsType(type, WellKnownTypeData.WellKnownType.System_TimeSpan)) return true; - if (wellKnown.IsType(type, WellKnownTypes.WellKnownTypeData.WellKnownType.System_Guid)) + if (wellKnown.IsType(type, WellKnownTypeData.WellKnownType.System_Guid)) return true; return false; @@ -133,23 +133,22 @@ private static bool IsWellKnownNonNestedType(INamedTypeSymbol type, GeneratorCon /// /// Gets the mappable properties from a type. /// - private static IPropertySymbol[] GetMappableProperties(INamedTypeSymbol type, GeneratorContext context) => + private static IPropertySymbol[] GetMappableProperties( + INamedTypeSymbol type, GeneratorContext context + ) => PropertySymbolLookup.GetProperties( type, context.MapperOptions.IncludeBaseClassProperties, static (p, declaringType) => - !p.IsStatic - && !p.IsIndexer - && (p.GetMethod != null || p.SetMethod != null) - && !(declaringType.IsRecord && p.Name == "EqualityContract") + !p.IsStatic && !p.IsIndexer && (p.GetMethod != null || p.SetMethod != null) && + !(declaringType.IsRecord && p.Name == "EqualityContract") ); /// /// Analyzes a type for inline code generation, recursively building property specs. /// private static DiagnosticResult AnalyzeForInline( - ITypeSymbol type, - NestedAnalysisContext nestedContext + ITypeSymbol type, NestedAnalysisContext nestedContext ) { if (type is not INamedTypeSymbol namedType) @@ -168,41 +167,69 @@ NestedAnalysisContext nestedContext // Check if this property should be ignored var ignoreOptions = nestedContext.GetIgnoreOptionsForProperty(property.Name); - if (ignoreOptions?.Ignore is Runtime.IgnoreMapping.All) + if (ignoreOptions?.Ignore is IgnoreMapping.All) continue; // Get field options for this nested property var fieldOptions = nestedContext.GetFieldOptionsForProperty(property.Name); + // Analyze the property using PropertyAnalyzer + var propertyAnalysisResult = + PropertyAnalyzer.Analyze(property, contextWithAncestor.Context); + if (!propertyAnalysisResult.IsSuccess) + { + diagnostics.Add(propertyAnalysisResult.Error!); + continue; + } + + var propertyAnalysis = propertyAnalysisResult.Value!; + // Determine the DynamoDB attribute name - var dynamoKey = fieldOptions?.AttributeName - ?? nestedContext.Context.MapperOptions.KeyNamingConventionConverter(property.Name); + var dynamoKey = + fieldOptions?.AttributeName ?? + nestedContext.Context.MapperOptions.KeyNamingConventionConverter(property.Name); // Analyze the property type var propertyType = property.Type; var underlyingType = UnwrapNullable(propertyType); // Try to resolve as a scalar type first - var scalarStrategy = TryResolveScalarStrategy(underlyingType, propertyType, fieldOptions, nestedContext.Context); + var scalarStrategy = + TryResolveScalarStrategy( + underlyingType, + propertyType, + fieldOptions, + nestedContext.Context + ); if (scalarStrategy != null) { - propertySpecs.Add(new NestedPropertySpec( - property.Name, - dynamoKey, - scalarStrategy, - NestedMapping: null - )); + propertySpecs.Add( + new NestedPropertySpec( + property.Name, + dynamoKey, + scalarStrategy, + propertyAnalysis.Nullability, + propertyAnalysis.HasGetter, + propertyAnalysis.HasSetter, + propertyAnalysis.IsRequired, + propertyAnalysis.IsInitOnly, + propertyAnalysis.HasDefaultValue, + null + ) + ); continue; } // Check for collections - var collectionInfo = CollectionTypeAnalyzer.Analyze(underlyingType, nestedContext.Context); + var collectionInfo = + CollectionTypeAnalyzer.Analyze(underlyingType, nestedContext.Context); if (collectionInfo != null) { - var validation = CollectionTypeAnalyzer.ValidateElementType( - collectionInfo.ElementType, - contextWithAncestor - ); + var validation = + CollectionTypeAnalyzer.ValidateElementType( + collectionInfo.ElementType, + contextWithAncestor + ); if (validation.Error is not null) { @@ -212,29 +239,40 @@ NestedAnalysisContext nestedContext if (!validation.IsValid) { - diagnostics.Add(new DiagnosticInfo( - DiagnosticDescriptors.UnsupportedNestedMemberType, - type.Locations.FirstOrDefault()?.CreateLocationInfo(), - nestedContext.CurrentPath, - property.Name, - propertyType.ToDisplayString() - )); + diagnostics.Add( + new DiagnosticInfo( + DiagnosticDescriptors.UnsupportedNestedMemberType, + type.Locations.FirstOrDefault()?.CreateLocationInfo(), + nestedContext.CurrentPath, + property.Name, + propertyType.ToDisplayString() + ) + ); continue; } // Create collection strategy (simplified for nested objects) var collectionStrategy = CreateCollectionStrategy(collectionInfo, propertyType); - propertySpecs.Add(new NestedPropertySpec( - property.Name, - dynamoKey, - collectionStrategy, - NestedMapping: null - )); + propertySpecs.Add( + new NestedPropertySpec( + property.Name, + dynamoKey, + collectionStrategy, + propertyAnalysis.Nullability, + propertyAnalysis.HasGetter, + propertyAnalysis.HasSetter, + propertyAnalysis.IsRequired, + propertyAnalysis.IsInitOnly, + propertyAnalysis.HasDefaultValue, + null + ) + ); continue; } // Try to analyze as a nested object (recursive) - var nestedResult = Analyze(underlyingType, property.Name, contextWithAncestor.WithPath(property.Name)); + var nestedResult = + Analyze(underlyingType, property.Name, contextWithAncestor.WithPath(property.Name)); if (!nestedResult.IsSuccess) { diagnostics.Add(nestedResult.Error!); @@ -244,33 +282,44 @@ NestedAnalysisContext nestedContext if (nestedResult.Value != null) { // It's a nested object - propertySpecs.Add(new NestedPropertySpec( - property.Name, - dynamoKey, - Strategy: null, - NestedMapping: nestedResult.Value - )); + propertySpecs.Add( + new NestedPropertySpec( + property.Name, + dynamoKey, + null, + propertyAnalysis.Nullability, + propertyAnalysis.HasGetter, + propertyAnalysis.HasSetter, + propertyAnalysis.IsRequired, + propertyAnalysis.IsInitOnly, + propertyAnalysis.HasDefaultValue, + nestedResult.Value + ) + ); continue; } // Unsupported type - diagnostics.Add(new DiagnosticInfo( - DiagnosticDescriptors.UnsupportedNestedMemberType, - type.Locations.FirstOrDefault()?.CreateLocationInfo(), - nestedContext.CurrentPath, - property.Name, - propertyType.ToDisplayString() - )); + diagnostics.Add( + new DiagnosticInfo( + DiagnosticDescriptors.UnsupportedNestedMemberType, + type.Locations.FirstOrDefault()?.CreateLocationInfo(), + nestedContext.CurrentPath, + property.Name, + propertyType.ToDisplayString() + ) + ); } // If there were errors, return the first one if (diagnostics.Count > 0) return DiagnosticResult.Failure(diagnostics[0]); - var inlineInfo = new NestedInlineInfo( - type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - new EquatableArray(propertySpecs.ToArray()) - ); + var inlineInfo = + new NestedInlineInfo( + type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + new EquatableArray(propertySpecs.ToArray()) + ); return DiagnosticResult.Success(new InlineNesting(inlineInfo)); } @@ -279,67 +328,108 @@ NestedAnalysisContext nestedContext /// Tries to resolve a scalar type mapping strategy. /// private static TypeMappingStrategy? TryResolveScalarStrategy( - ITypeSymbol underlyingType, - ITypeSymbol originalType, - DynamoFieldOptions? fieldOptions, + ITypeSymbol underlyingType, ITypeSymbol originalType, DynamoFieldOptions? fieldOptions, GeneratorContext context ) { - var isNullable = !SymbolEqualityComparer.Default.Equals(underlyingType, originalType) - || originalType.NullableAnnotation == NullableAnnotation.Annotated; + var isNullable = + !SymbolEqualityComparer.Default.Equals(underlyingType, originalType) || + originalType.NullableAnnotation == NullableAnnotation.Annotated; var nullableModifier = isNullable ? "Nullable" : ""; return underlyingType switch { { SpecialType: SpecialType.System_String } => new TypeMappingStrategy( - "String", "", nullableModifier, [], [] + "String", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_Boolean } => new TypeMappingStrategy( - "Bool", "", nullableModifier, [], [] + "Bool", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_Int32 } => new TypeMappingStrategy( - "Int", "", nullableModifier, [], [] + "Int", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_Int64 } => new TypeMappingStrategy( - "Long", "", nullableModifier, [], [] + "Long", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_Single } => new TypeMappingStrategy( - "Float", "", nullableModifier, [], [] + "Float", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_Double } => new TypeMappingStrategy( - "Double", "", nullableModifier, [], [] + "Double", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_Decimal } => new TypeMappingStrategy( - "Decimal", "", nullableModifier, [], [] + "Decimal", + "", + nullableModifier, + [], + [] ), { SpecialType: SpecialType.System_DateTime } => new TypeMappingStrategy( - "DateTime", "", nullableModifier, + "DateTime", + "", + nullableModifier, [$"\"{fieldOptions?.Format ?? context.MapperOptions.DateTimeFormat}\""], [$"\"{fieldOptions?.Format ?? context.MapperOptions.DateTimeFormat}\""] ), INamedTypeSymbol t when context.WellKnownTypes.IsType( - t, WellKnownTypes.WellKnownTypeData.WellKnownType.System_DateTimeOffset + t, + WellKnownTypeData.WellKnownType.System_DateTimeOffset ) => new TypeMappingStrategy( - "DateTimeOffset", "", nullableModifier, + "DateTimeOffset", + "", + nullableModifier, [$"\"{fieldOptions?.Format ?? context.MapperOptions.DateTimeFormat}\""], [$"\"{fieldOptions?.Format ?? context.MapperOptions.DateTimeFormat}\""] ), INamedTypeSymbol t when context.WellKnownTypes.IsType( - t, WellKnownTypes.WellKnownTypeData.WellKnownType.System_Guid + t, + WellKnownTypeData.WellKnownType.System_Guid ) => new TypeMappingStrategy( - "Guid", "", nullableModifier, + "Guid", + "", + nullableModifier, [$"\"{fieldOptions?.Format ?? context.MapperOptions.GuidFormat}\""], [$"\"{fieldOptions?.Format ?? context.MapperOptions.GuidFormat}\""] ), INamedTypeSymbol t when context.WellKnownTypes.IsType( - t, WellKnownTypes.WellKnownTypeData.WellKnownType.System_TimeSpan + t, + WellKnownTypeData.WellKnownType.System_TimeSpan ) => new TypeMappingStrategy( - "TimeSpan", "", nullableModifier, + "TimeSpan", + "", + nullableModifier, [$"\"{fieldOptions?.Format ?? context.MapperOptions.TimeSpanFormat}\""], [$"\"{fieldOptions?.Format ?? context.MapperOptions.TimeSpanFormat}\""] ), INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType => CreateEnumStrategy( - enumType, isNullable, fieldOptions, context + enumType, + isNullable, + fieldOptions, + context ), _ => null }; @@ -349,9 +439,7 @@ INamedTypeSymbol t when context.WellKnownTypes.IsType( /// Creates an enum type mapping strategy. /// private static TypeMappingStrategy CreateEnumStrategy( - INamedTypeSymbol enumType, - bool isNullable, - DynamoFieldOptions? fieldOptions, + INamedTypeSymbol enumType, bool isNullable, DynamoFieldOptions? fieldOptions, GeneratorContext context ) { @@ -360,40 +448,56 @@ GeneratorContext context var enumFormat = $"\"{format}\""; var nullableModifier = isNullable ? "Nullable" : ""; - string[] fromArgs = isNullable - ? [enumFormat] - : [$"{enumName}.{enumType.MemberNames.First()}", enumFormat]; + string[] fromArgs = + isNullable ? [enumFormat] : [$"{enumName}.{enumType.MemberNames.First()}", enumFormat]; - return new TypeMappingStrategy("Enum", $"<{enumName}>", nullableModifier, fromArgs, [enumFormat]); + return new TypeMappingStrategy( + "Enum", + $"<{enumName}>", + nullableModifier, + fromArgs, + [enumFormat] + ); } /// /// Creates a collection type mapping strategy. /// private static TypeMappingStrategy CreateCollectionStrategy( - CollectionInfo collectionInfo, - ITypeSymbol originalType + CollectionInfo collectionInfo, ITypeSymbol originalType ) { var isNullable = originalType.NullableAnnotation == NullableAnnotation.Annotated; var nullableModifier = isNullable ? "Nullable" : ""; - var (typeName, genericArg) = collectionInfo.Category switch - { - CollectionCategory.List => ("List", $"<{collectionInfo.ElementType.ToDisplayString()}>"), - CollectionCategory.Map => ("Map", $"<{collectionInfo.ElementType.ToDisplayString()}>"), - CollectionCategory.Set => collectionInfo.TargetKind switch + var (typeName, genericArg) = + collectionInfo.Category switch { - Runtime.DynamoKind.SS => ("StringSet", ""), - Runtime.DynamoKind.NS => ("NumberSet", $"<{collectionInfo.ElementType.ToDisplayString()}>"), - Runtime.DynamoKind.BS => ("BinarySet", ""), - _ => throw new InvalidOperationException($"Unexpected set kind: {collectionInfo.TargetKind}") - }, - _ => throw new InvalidOperationException($"Unexpected category: {collectionInfo.Category}") - }; + CollectionCategory.List => ("List", + $"<{collectionInfo.ElementType.ToDisplayString()}>"), + CollectionCategory.Map => ("Map", + $"<{collectionInfo.ElementType.ToDisplayString()}>"), + CollectionCategory.Set => collectionInfo.TargetKind switch + { + DynamoKind.SS => ("StringSet", ""), + DynamoKind.NS => ("NumberSet", + $"<{collectionInfo.ElementType.ToDisplayString()}>"), + DynamoKind.BS => ("BinarySet", ""), + _ => throw new InvalidOperationException( + $"Unexpected set kind: {collectionInfo.TargetKind}" + ), + }, + _ => throw new InvalidOperationException( + $"Unexpected category: {collectionInfo.Category}" + ), + }; return new TypeMappingStrategy( - typeName, genericArg, nullableModifier, [], [], + typeName, + genericArg, + nullableModifier, + [], + [], KindOverride: collectionInfo.TargetKind ); } @@ -403,11 +507,14 @@ ITypeSymbol originalType /// private static ITypeSymbol UnwrapNullable(ITypeSymbol type) { - if (type is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullableType - && nullableType.TypeArguments.Length == 1) + if (type is INamedTypeSymbol + { + OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, + } nullableType && nullableType.TypeArguments.Length == 1) { return nullableType.TypeArguments[0]; } + return type; } } diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs index 922a379..65628b2 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs @@ -405,9 +405,9 @@ HelperMethodRegistry helperRegistry $".Set(\"{prop.DynamoKey}\", {nestedSourcePrefix} is null ? new AttributeValue {{ NULL = true }} : {nestedCode})" ); } - else if (prop.Strategy is not null) + else if (prop.Strategy is not null && prop.HasGetter) { - // Scalar property + // Scalar property - only render if it has a getter var setMethod = $"Set{prop.Strategy.TypeName}"; var genericArg = prop.Strategy.GenericArgument; var propValue = $"{sourcePrefix}.{prop.PropertyName}"; @@ -546,11 +546,22 @@ HelperMethodRegistry helperRegistry // Recursive nested object var nestedVarName = $"{mapVarName}_{prop.PropertyName.ToLowerInvariant()}"; + // Determine fallback based on required and nullability + string fallback; + if (prop.IsRequired) + fallback = + $"throw new System.InvalidOperationException(\"Required nested property '{prop.DynamoKey}' not found in DynamoDB item.\")"; + else if (prop.Nullability.IsNullableType) + fallback = "null"; + else + // Non-nullable, non-required - use null (design limitation) + fallback = "null"; + string nestedCode; if (prop.NestedMapping is MapperBasedNesting mapperBased) { nestedCode = - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : null"; + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : {fallback}"; } else if (prop.NestedMapping is InlineNesting inline) { @@ -561,7 +572,7 @@ HelperMethodRegistry helperRegistry inline.Info ); nestedCode = - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {helperMethodName}({nestedVarName}) : null"; + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {helperMethodName}({nestedVarName}) : {fallback}"; } else { @@ -587,8 +598,12 @@ HelperMethodRegistry helperRegistry ) + ", " : ""; + // Determine requiredness based on property analysis + var requiredness = + prop.IsRequired ? "Requiredness.Required" : "Requiredness.Optional"; + sb.Append( - $"{prop.PropertyName} = {mapVarName}.{getMethod}{genericArg}(\"{prop.DynamoKey}\", {typeArgs}Requiredness.Optional)," + $"{prop.PropertyName} = {mapVarName}.{getMethod}{genericArg}(\"{prop.DynamoKey}\", {typeArgs}{requiredness})," ); } } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs index b6d0fe5..4c6a40a 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/NestedObjectVerifyTests.cs @@ -1009,4 +1009,50 @@ public static partial class Level1Mapper }, TestContext.Current.CancellationToken ); + + [Fact] + public async Task NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = + """ + using System.Collections.Generic; + using Amazon.DynamoDBv2.Model; + using DynamoMapper.Runtime; + + namespace MyNamespace; + + public class Order + { + public string Id { get; set; } = string.Empty; + public Address ShippingAddress { get; set; } = null!; + } + + public class Address + { + // Required properties - should use Requiredness.Required + public required string Line1 { get; set; } + public required string City { get; set; } + + // Optional with default - should use Requiredness.Optional + public string Country { get; set; } = "USA"; + + // Nullable - should use Requiredness.Optional + public string? PostalCode { get; set; } + + // Init-only required - should be in object initializer + public required string State { get; init; } + } + + [DynamoMapper] + public static partial class OrderMapper + { + public static partial Dictionary ToItem(Order source); + public static partial Order FromItem(Dictionary item); + } + """, + }, + TestContext.Current.CancellationToken + ); } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs new file mode 100644 index 0000000..bb9d50f --- /dev/null +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs @@ -0,0 +1,61 @@ +//HintName: OrderMapper.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using DynamoMapper.Runtime; +using System.Collections.Generic; +using System.Linq; +using Amazon.DynamoDBv2.Model; + +namespace MyNamespace; + +public static partial class OrderMapper +{ + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + public static partial global::System.Collections.Generic.Dictionary ToItem(global::MyNamespace.Order source) => + new Dictionary(2) + .SetString("id", source.Id, false, true) + .Set("shippingAddress", new AttributeValue { M = ToItem_Address(source.ShippingAddress) }); + + [global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")] + public static partial global::MyNamespace.Order FromItem(global::System.Collections.Generic.Dictionary item) + { + var order = new global::MyNamespace.Order + { + ShippingAddress = item.TryGetValue("shippingAddress", out var shippingaddressAttr) && shippingaddressAttr.M is { } shippingaddressMap ? FromItem_Address(shippingaddressMap) : null, + }; + if (item.TryGetString("id", out var var0, Requiredness.InferFromNullability)) order.Id = var0!; + return order; + } + + // Helper methods for nested object mapping + + private static Dictionary ToItem_Address(global::MyNamespace.Address address) => + new Dictionary(5) + .SetString("line1", address.Line1, false, true) + .SetString("city", address.City, false, true) + .SetString("country", address.Country, false, true) + .SetString("postalCode", address.PostalCode, false, true) + .SetString("state", address.State, false, true); + + private static global::MyNamespace.Address FromItem_Address(Dictionary map) + { + return new global::MyNamespace.Address + { + Line1 = map.GetString("line1", Requiredness.Required), + City = map.GetString("city", Requiredness.Required), + Country = map.GetString("country", Requiredness.Optional), + PostalCode = map.GetNullableString("postalCode", Requiredness.Optional), + State = map.GetString("state", Requiredness.Required), + }; + } + +} From b3386dc4fb29af35157f9768bc8449d451449ec5 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Mon, 16 Feb 2026 21:21:45 -0500 Subject: [PATCH 08/17] feat(mappers): improve requiredness handling and support post-construction assignments - Updated nested object mappings to separate properties into initialization vs post-construction setup. - Enhanced handling of `Requiredness.InferFromNullability` for dynamic property assignment. - Refactored `RenderInlineNestedFromItem` to support conditional property assignments post-construction. - Adjusted helper method generation to align with new assignment logic. - Updated test snapshots to reflect changes in requiredness handling and assignment patterns. --- .../Emitters/HelperMethodEmitter.cs | 24 +- .../PropertyMappingCodeRenderer.cs | 240 +++++++++++++----- ...thOptIn_Succeeds#OrderMapper.g.verified.cs | 9 +- ..._MultipleLevels#Level1Mapper.g.verified.cs | 17 +- ...rectRequiredness#OrderMapper.g.verified.cs | 5 +- 5 files changed, 213 insertions(+), 82 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index cd2a9f9..4ef110a 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -54,7 +54,6 @@ public static string RenderFromItemHelper( $"private static {helper.ModelFullyQualifiedType} {helper.MethodName}(Dictionary {mapParamName})" ); sb.AppendLine("{"); - sb.Append(" return "); // Reuse existing RenderInlineNestedFromItem logic var bodyCode = @@ -65,10 +64,25 @@ public static string RenderFromItemHelper( helperRegistry ); - // Format object initializer on separate lines - var formattedBody = FormatFromItemObjectInitializer(bodyCode); - sb.Append(formattedBody); - sb.AppendLine(";"); + // Check if body is simple expression or multi-statement block + var isSimpleExpression = bodyCode.TrimStart().StartsWith("new "); + + if (isSimpleExpression) + { + // Simple expression: "new Type { ... }" + sb.Append(" return "); + var formattedBody = FormatFromItemObjectInitializer(bodyCode); + sb.Append(formattedBody); + sb.AppendLine(";"); + } + else + { + // Multi-statement block: "var x = new Type { ... }; if (...) ...; return x;" + // Body already contains proper indentation and return statement + sb.Append(bodyCode); + sb.AppendLine(); + } + sb.AppendLine("}"); return sb.ToString(); diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs index 65628b2..b0cfa7e 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs @@ -531,85 +531,199 @@ internal static string RenderInlineNestedFromItem( string mapVarName, NestedInlineInfo inlineInfo, GeneratorContext context, HelperMethodRegistry helperRegistry ) + { + // Split properties into object initializer vs post-construction + var initProperties = new List(); + var postConstructionProperties = new List(); + + foreach (var prop in inlineInfo.Properties) + { + // Use post-construction for optional, settable properties with default values + var usePostConstruction = + !prop.IsRequired && !prop.IsInitOnly && prop.HasDefaultValue && prop.HasSetter; + + if (usePostConstruction) + postConstructionProperties.Add(prop); + else + initProperties.Add(prop); + } + + // If no post-construction properties, use simple expression + if (postConstructionProperties.Count == 0) + return RenderSimpleNestedFromItem( + mapVarName, + inlineInfo.ModelFullyQualifiedType, + initProperties, + context, + helperRegistry + ); + + // Otherwise, use var + initializer + if statements pattern + var sb = new StringBuilder(); + var varName = ExtractSimpleVarName(inlineInfo.ModelFullyQualifiedType); + + // Variable declaration with object initializer for required/init properties + if (initProperties.Count == 0) + { + sb.AppendLine($" var {varName} = new {inlineInfo.ModelFullyQualifiedType}();"); + } + else + { + sb.AppendLine($" var {varName} = new {inlineInfo.ModelFullyQualifiedType}"); + sb.AppendLine(" {"); + foreach (var prop in initProperties) + { + sb.Append(" "); + RenderPropertyInitAssignment(prop, mapVarName, sb, context, helperRegistry); + sb.AppendLine(); + } + + sb.AppendLine(" };"); + } + + // Post-construction if statements for optional properties with defaults + var varIndex = 0; + foreach (var prop in postConstructionProperties) + { + sb.Append(" if ("); + sb.Append(RenderTryGetCondition(prop, mapVarName, $"var{varIndex}", context)); + sb.Append($") {varName}.{prop.PropertyName} = var{varIndex}!;"); + sb.AppendLine(); + varIndex++; + } + + sb.Append($" return {varName};"); + return sb.ToString(); + } + + /// Renders a simple single-expression nested object creation (all properties in initializer). + private static string RenderSimpleNestedFromItem( + string mapVarName, string modelType, List properties, + GeneratorContext context, HelperMethodRegistry helperRegistry + ) { var sb = new StringBuilder(); - sb.Append($"new {inlineInfo.ModelFullyQualifiedType} {{ "); + sb.Append($"new {modelType} {{ "); var first = true; - foreach (var prop in inlineInfo.Properties) + foreach (var prop in properties) { if (!first) sb.Append(" "); first = false; - if (prop.NestedMapping is not null) - { - // Recursive nested object - var nestedVarName = $"{mapVarName}_{prop.PropertyName.ToLowerInvariant()}"; - - // Determine fallback based on required and nullability - string fallback; - if (prop.IsRequired) - fallback = - $"throw new System.InvalidOperationException(\"Required nested property '{prop.DynamoKey}' not found in DynamoDB item.\")"; - else if (prop.Nullability.IsNullableType) - fallback = "null"; - else - // Non-nullable, non-required - use null (design limitation) - fallback = "null"; + RenderPropertyInitAssignment(prop, mapVarName, sb, context, helperRegistry); + } - string nestedCode; - if (prop.NestedMapping is MapperBasedNesting mapperBased) - { - nestedCode = - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : {fallback}"; - } - else if (prop.NestedMapping is InlineNesting inline) - { - // Register helper and generate call instead of inlining - var helperMethodName = - helperRegistry.GetOrRegisterFromItemHelper( - inline.Info.ModelFullyQualifiedType, - inline.Info - ); - nestedCode = - $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {helperMethodName}({nestedVarName}) : {fallback}"; - } - else - { - throw new InvalidOperationException("Unknown nested mapping type"); - } + sb.Append(" }"); + return sb.ToString(); + } - sb.Append($"{prop.PropertyName} = {nestedCode},"); + /// Renders a single property initialization assignment for object initializer. + private static void RenderPropertyInitAssignment( + NestedPropertySpec prop, string mapVarName, StringBuilder sb, GeneratorContext context, + HelperMethodRegistry helperRegistry + ) + { + if (prop.NestedMapping is not null) + { + // Recursive nested object + var nestedVarName = $"{mapVarName}_{prop.PropertyName.ToLowerInvariant()}"; + + // Determine fallback based on required and nullability + string fallback; + if (prop.IsRequired) + fallback = + $"throw new System.InvalidOperationException(\"Required nested property '{prop.DynamoKey}' not found in DynamoDB item.\")"; + else if (prop.Nullability.IsNullableType) + fallback = "null"; + else + // Non-nullable, non-required - use null (design limitation) + fallback = "null"; + + string nestedCode; + if (prop.NestedMapping is MapperBasedNesting mapperBased) + { + nestedCode = + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {mapperBased.Mapper.MapperFullyQualifiedName}.FromItem({nestedVarName}) : {fallback}"; } - else if (prop.Strategy is not null) + else if (prop.NestedMapping is InlineNesting inline) { - // Scalar property - var getMethod = $"Get{prop.Strategy.NullableModifier}{prop.Strategy.TypeName}"; - var genericArg = prop.Strategy.GenericArgument; + // Register helper and generate call instead of inlining + var helperMethodName = + helperRegistry.GetOrRegisterFromItemHelper( + inline.Info.ModelFullyQualifiedType, + inline.Info + ); + nestedCode = + $"{mapVarName}.TryGetValue(\"{prop.DynamoKey}\", out var {nestedVarName}Attr) && {nestedVarName}Attr.M is {{ }} {nestedVarName} ? {helperMethodName}({nestedVarName}) : {fallback}"; + } + else + { + throw new InvalidOperationException("Unknown nested mapping type"); + } - // Build type-specific args - var typeArgs = - prop.Strategy.FromTypeSpecificArgs.Length > 0 - ? string.Join( - ", ", - prop.Strategy.FromTypeSpecificArgs.Select( - a => a.StartsWith("\"") ? $"format: {a}" : a - ) - ) + ", " - : ""; + sb.Append($"{prop.PropertyName} = {nestedCode},"); + } + else if (prop.Strategy is not null) + { + // Scalar property + var getMethod = $"Get{prop.Strategy.NullableModifier}{prop.Strategy.TypeName}"; + var genericArg = prop.Strategy.GenericArgument; + + // Build type-specific args + var typeArgs = + prop.Strategy.FromTypeSpecificArgs.Length > 0 + ? string.Join( + ", ", + prop.Strategy.FromTypeSpecificArgs.Select( + a => a.StartsWith("\"") ? $"format: {a}" : a + ) + ) + ", " + : ""; + + // Determine requiredness based on property analysis + var requiredness = prop.IsRequired ? "Requiredness.Required" : "Requiredness.Optional"; + + sb.Append( + $"{prop.PropertyName} = {mapVarName}.{getMethod}{genericArg}(\"{prop.DynamoKey}\", {typeArgs}{requiredness})," + ); + } + } - // Determine requiredness based on property analysis - var requiredness = - prop.IsRequired ? "Requiredness.Required" : "Requiredness.Optional"; + /// Renders a TryGet condition for post-construction assignment. + private static string RenderTryGetCondition( + NestedPropertySpec prop, string mapVarName, string outVarName, GeneratorContext context + ) + { + if (prop.Strategy is null) + throw new InvalidOperationException( + "Post-construction only supported for scalar properties" + ); - sb.Append( - $"{prop.PropertyName} = {mapVarName}.{getMethod}{genericArg}(\"{prop.DynamoKey}\", {typeArgs}{requiredness})," - ); - } - } + var tryMethod = $"TryGet{prop.Strategy.NullableModifier}{prop.Strategy.TypeName}"; + var genericArg = prop.Strategy.GenericArgument; - sb.Append(" }"); - return sb.ToString(); + // Build type-specific args + var typeArgs = + prop.Strategy.FromTypeSpecificArgs.Length > 0 + ? string.Join( + ", ", + prop.Strategy.FromTypeSpecificArgs.Select( + a => a.StartsWith("\"") ? $"format: {a}" : a + ) + ) + ", " + : ""; + + return + $"{mapVarName}.{tryMethod}{genericArg}(\"{prop.DynamoKey}\", out var {outVarName}, {typeArgs}Requiredness.InferFromNullability)"; + } + + /// Extracts a simple variable name from a fully qualified type name. + private static string ExtractSimpleVarName(string fullyQualifiedType) + { + // Remove "global::" prefix and namespace, take last segment, lowercase first letter + var typeName = fullyQualifiedType.Replace("global::", "").Split('.').Last(); + return char.ToLowerInvariant(typeName[0]) + typeName.Substring(1); } /// diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs index da93612..8867dca 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/InheritanceVerifyTests.Inheritance_DotNotation_BaseProperty_WithOptIn_Succeeds#OrderMapper.g.verified.cs @@ -43,11 +43,10 @@ private static Dictionary ToItem_Address(global::MyNames private static global::MyNamespace.Address FromItem_Address(Dictionary map) { - return new global::MyNamespace.Address - { - City = map.GetString("city", Requiredness.Optional), - Line1 = map.GetString("line_1", Requiredness.Optional), - }; + var address = new global::MyNamespace.Address(); + if (map.TryGetString("city", out var var0, Requiredness.InferFromNullability)) address.City = var0!; + if (map.TryGetString("line_1", out var var1, Requiredness.InferFromNullability)) address.Line1 = var1!; + return address; } } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs index 85fa9c7..a7680a4 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_MultipleLevels#Level1Mapper.g.verified.cs @@ -48,12 +48,13 @@ private static Dictionary ToItem_Level2(global::MyNamesp private static global::MyNamespace.Level2 FromItem_Level2(Dictionary map) { - return new global::MyNamespace.Level2 + var level2 = new global::MyNamespace.Level2 { - Id = map.GetString("id", Requiredness.Optional), - Description = map.GetString("description", Requiredness.Optional), Level3Data = map.TryGetValue("level3Data", out var map_level3dataAttr) && map_level3dataAttr.M is { } map_level3data ? FromItem_Level3(map_level3data) : null, }; + if (map.TryGetString("id", out var var0, Requiredness.InferFromNullability)) level2.Id = var0!; + if (map.TryGetString("description", out var var1, Requiredness.InferFromNullability)) level2.Description = var1!; + return level2; } @@ -65,12 +66,13 @@ private static Dictionary ToItem_Level3(global::MyNamesp private static global::MyNamespace.Level3 FromItem_Level3(Dictionary map) { - return new global::MyNamespace.Level3 + var level3 = new global::MyNamespace.Level3 { - Id = map.GetString("id", Requiredness.Optional), Value = map.GetInt("value", Requiredness.Optional), Level4Data = map.TryGetValue("level4Data", out var map_level4dataAttr) && map_level4dataAttr.M is { } map_level4data ? FromItem_Level4(map_level4data) : null, }; + if (map.TryGetString("id", out var var0, Requiredness.InferFromNullability)) level3.Id = var0!; + return level3; } @@ -82,12 +84,13 @@ private static Dictionary ToItem_Level4(global::MyNamesp private static global::MyNamespace.Level4 FromItem_Level4(Dictionary map) { - return new global::MyNamespace.Level4 + var level4 = new global::MyNamespace.Level4 { - Id = map.GetString("id", Requiredness.Optional), IsActive = map.GetBool("isActive", Requiredness.Optional), Price = map.GetDecimal("price", Requiredness.Optional), }; + if (map.TryGetString("id", out var var0, Requiredness.InferFromNullability)) level4.Id = var0!; + return level4; } } diff --git a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs index bb9d50f..42b5596 100644 --- a/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs +++ b/test/LayeredCraft.DynamoMapper.Generators.Tests/Snapshots/NestedObjectVerifyTests.NestedObject_WithRequiredProperties_ShouldUseCorrectRequiredness#OrderMapper.g.verified.cs @@ -48,14 +48,15 @@ private static Dictionary ToItem_Address(global::MyNames private static global::MyNamespace.Address FromItem_Address(Dictionary map) { - return new global::MyNamespace.Address + var address = new global::MyNamespace.Address { Line1 = map.GetString("line1", Requiredness.Required), City = map.GetString("city", Requiredness.Required), - Country = map.GetString("country", Requiredness.Optional), PostalCode = map.GetNullableString("postalCode", Requiredness.Optional), State = map.GetString("state", Requiredness.Required), }; + if (map.TryGetString("country", out var var0, Requiredness.InferFromNullability)) address.Country = var0!; + return address; } } From bfec277e0266ec9b344f61820560fa086ba964aa Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 14:05:33 -0500 Subject: [PATCH 09/17] feat(models): enhance `MapperInfo` with equality and hash code implementations - Added `Equals` and `GetHashCode` methods to `MapperInfo` for enhanced comparison logic. - Made `MapperInfo` non-sealed to allow inheritance. --- .../Models/MapperInfo.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs index 7fedcf7..c80c213 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs @@ -5,13 +5,21 @@ namespace DynamoMapper.Generator.Models; -internal sealed record MapperInfo( +internal record MapperInfo( MapperClassInfo? MapperClass, ModelClassInfo? ModelClass, EquatableArray Diagnostics, GeneratorContext? Context, HelperMethodRegistry? HelperRegistry -); +) +{ + public virtual bool Equals(MapperInfo? other) => other is not null && + MapperClass == other.MapperClass && + ModelClass == other.ModelClass && + Diagnostics == other.Diagnostics; + + public override int GetHashCode() => HashCode.Combine(MapperClass, ModelClass, Diagnostics); +} internal static class MapperInfoExtensions { From 29854dd4eb3cf5dad74dc248197feb7faba0c885 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 14:29:53 -0500 Subject: [PATCH 10/17] feat(diagnostics): add diagnostic for helper rendering iteration limit - Introduced `DM0009` diagnostic to detect and report limit exceedance during helper method rendering. - Updated `MapperEmitter` to apply a safety bound of 1,000 iterations for helper rendering loops. - Modified the rendering logic to report `DM0009` when the iteration limit is reached. - Enhanced `DiagnosticDescriptors` with a descriptor for `HelperRenderingLimitExceeded`. --- .../AnalyzerReleases.Unshipped.md | 1 + .../Diagnostics/DiagnosticDescriptors.cs | 180 ++++++++++-------- .../Emitters/MapperEmitter.cs | 31 ++- 3 files changed, 130 insertions(+), 82 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.DynamoMapper.Generators/AnalyzerReleases.Unshipped.md index deeb294..0865b58 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/AnalyzerReleases.Unshipped.md +++ b/src/LayeredCraft.DynamoMapper.Generators/AnalyzerReleases.Unshipped.md @@ -12,6 +12,7 @@ DM0006 | DynamoMapper.Usage | Error | DiagnosticDescriptors DM0007 | DynamoMapper.Usage | Error | DiagnosticDescriptors DM0008 | DynamoMapper.Usage | Error | DiagnosticDescriptors + DM0009 | DynamoMapper.Usage | Error | DiagnosticDescriptors DM0101 | DynamoMapper.Usage | Error | DiagnosticDescriptors DM0102 | DynamoMapper.Usage | Error | DiagnosticDescriptors DM0103 | DynamoMapper.Usage | Error | DiagnosticDescriptors \ No newline at end of file diff --git a/src/LayeredCraft.DynamoMapper.Generators/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.DynamoMapper.Generators/Diagnostics/DiagnosticDescriptors.cs index 207fdfd..b959ccc 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Diagnostics/DiagnosticDescriptors.cs @@ -6,93 +6,113 @@ internal static class DiagnosticDescriptors { private const string UsageCategory = "DynamoMapper.Usage"; - internal static readonly DiagnosticDescriptor CannotConvertFromAttributeValue = new( - "DM0001", - "Type cannot be mapped to an AttributeValue", - "The property '{0}' of type '{1}' cannot be mapped to an AttributeValue", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor CannotConvertFromAttributeValue = + new( + "DM0001", + "Type cannot be mapped to an AttributeValue", + "The property '{0}' of type '{1}' cannot be mapped to an AttributeValue", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor UnsupportedCollectionElementType = new( - "DM0003", - "Collection element type not supported", - "The property '{0}' has element type '{1}' which is not supported. Only primitive types are supported as collection elements.", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor UnsupportedCollectionElementType = + new( + "DM0003", + "Collection element type not supported", + "The property '{0}' has element type '{1}' which is not supported. Only primitive types are supported as collection elements.", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor DictionaryKeyMustBeString = new( - "DM0004", - "Dictionary key must be string", - "The property '{0}' has key type '{1}' but dictionary keys must be of type 'string'", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor DictionaryKeyMustBeString = + new( + "DM0004", + "Dictionary key must be string", + "The property '{0}' has key type '{1}' but dictionary keys must be of type 'string'", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor IncompatibleKindOverride = new( - "DM0005", - "Incompatible DynamoKind override for collection", - "The property '{0}' has Kind override '{1}' which is incompatible with the inferred collection kind '{2}'", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor IncompatibleKindOverride = + new( + "DM0005", + "Incompatible DynamoKind override for collection", + "The property '{0}' has Kind override '{1}' which is incompatible with the inferred collection kind '{2}'", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor NoMapperMethodsFound = new( - "DM0101", - "No mapper methods found", - "The mapper class '{0}' must define at least one partial method starting with 'To' or 'From'", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor NoMapperMethodsFound = + new( + "DM0101", + "No mapper methods found", + "The mapper class '{0}' must define at least one partial method starting with 'To' or 'From'", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor MismatchedPocoTypes = new( - "DM0102", - "Mapper methods use different POCO types", - "The mapper methods '{0}' and '{1}' must use the same POCO type, but '{2}' and '{3}' were found", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor MismatchedPocoTypes = + new( + "DM0102", + "Mapper methods use different POCO types", + "The mapper methods '{0}' and '{1}' must use the same POCO type, but '{2}' and '{3}' were found", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor MultipleConstructorsWithAttribute = new( - "DM0103", - "Multiple constructors marked with [DynamoMapperConstructor]", - "The type '{0}' has multiple constructors marked with [DynamoMapperConstructor]. Only one constructor can be marked with this attribute.", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor MultipleConstructorsWithAttribute = + new( + "DM0103", + "Multiple constructors marked with [DynamoMapperConstructor]", + "The type '{0}' has multiple constructors marked with [DynamoMapperConstructor]. Only one constructor can be marked with this attribute.", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor CycleDetectedInNestedType = new( - "DM0006", - "Circular reference detected in nested type", - "The property '{0}' creates a circular reference with type '{1}'. Cycles are not supported in nested object mapping.", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor CycleDetectedInNestedType = + new( + "DM0006", + "Circular reference detected in nested type", + "The property '{0}' creates a circular reference with type '{1}'. Cycles are not supported in nested object mapping.", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor UnsupportedNestedMemberType = new( - "DM0007", - "Unsupported nested member type", - "The nested property '{0}.{1}' has type '{2}' which cannot be mapped", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor UnsupportedNestedMemberType = + new( + "DM0007", + "Unsupported nested member type", + "The nested property '{0}.{1}' has type '{2}' which cannot be mapped", + UsageCategory, + DiagnosticSeverity.Error, + true + ); - internal static readonly DiagnosticDescriptor InvalidDotNotationPath = new( - "DM0008", - "Invalid dot-notation path", - "The dot-notation path '{0}' is invalid. Property '{1}' not found on type '{2}'.", - UsageCategory, - DiagnosticSeverity.Error, - true - ); + internal static readonly DiagnosticDescriptor InvalidDotNotationPath = + new( + "DM0008", + "Invalid dot-notation path", + "The dot-notation path '{0}' is invalid. Property '{1}' not found on type '{2}'.", + UsageCategory, + DiagnosticSeverity.Error, + true + ); + + internal static readonly DiagnosticDescriptor HelperRenderingLimitExceeded = + new( + "DM0009", + "Helper method rendering limit exceeded", + "Mapper '{0}' exceeded the maximum of {1} helper-method rendering iterations. This indicates a bug in the DynamoMapper source generator — please file an issue.", + UsageCategory, + DiagnosticSeverity.Error, + true + ); } diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs index 61868d0..40f9467 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/MapperEmitter.cs @@ -1,4 +1,5 @@ using System.Reflection; +using DynamoMapper.Generator.Diagnostics; using DynamoMapper.Generator.Models; using Microsoft.CodeAnalysis; @@ -51,8 +52,16 @@ internal static void Generate(SourceProductionContext context, MapperInfo mapper var renderedHelpers = new HashSet(); var helperList = new List(); - // Keep rendering until all helpers are processed - while (true) + // Keep rendering until all helpers are processed. + // A safety bound prevents an infinite loop if a bug causes helpers to be + // registered under ever-changing names (e.g. non-deterministic type strings). + // Each iteration must render at least one helper, so the real limit is the + // number of distinct nested types; 1 000 is a generous upper bound. + const int maxIterations = 1_000; + var iterations = 0; + var limitExceeded = false; + + while (iterations++ < maxIterations) { var allHelpers = mapperInfo.HelperRegistry.GetAllHelpers(); var newHelpers = @@ -79,6 +88,24 @@ internal static void Generate(SourceProductionContext context, MapperInfo mapper helperList.Add(rendered); renderedHelpers.Add(helper.MethodName); } + + if (iterations >= maxIterations) + { + limitExceeded = true; + break; + } + } + + if (limitExceeded) + { + new DiagnosticInfo( + DiagnosticDescriptors.HelperRenderingLimitExceeded, + null, + mapperInfo.MapperClass!.Name, + maxIterations + ).ReportDiagnostic(context); + + return; } helperMethods = helperList.ToArray(); From 16ab080324b211c85a0c058decb84d69439df358 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 14:41:55 -0500 Subject: [PATCH 11/17] fix(emitters): handle braces in depth tracking for helper methods - Updated `HelperMethodEmitter` to account for `{` and `}` in depth calculations. - Prevented potential mismatches in helper method generation involving block delimiters. --- .../Emitters/HelperMethodEmitter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index 4ef110a..2de9d91 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -216,11 +216,11 @@ private static string[] SplitPropertiesRespectingNesting(string content) { var ch = content[i]; - if (ch == '(' || ch == '<') + if (ch == '(' || ch == '<' || ch == '{') { depth++; } - else if (ch == ')' || ch == '>') + else if (ch == ')' || ch == '>' || ch == '}') { depth--; } From fc22b6a3df9fe0cdcdc4a6aefeca532d8407ebe2 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 14:59:38 -0500 Subject: [PATCH 12/17] feat(emitters): refine `.Set*` method detection and extraction logic - Added `FindNextSetMethodCall` to streamline `.Set*` method call detection. - Replaced direct `.Set` index searches with the new utility method for consistency. - Enhanced support for generic type and nested method parsing in `.Set*` calls. --- .../Emitters/HelperMethodEmitter.cs | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index 2de9d91..d787be9 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -102,7 +102,7 @@ private static string FormatToItemChainedCalls(string bodyCode) sb.Append($"new Dictionary({capacity})"); // Find each .Set* method call and put it on a new line - var startIndex = bodyCode.IndexOf(".Set", StringComparison.Ordinal); + var startIndex = FindNextSetMethodCall(bodyCode, 0); while (startIndex >= 0) { @@ -136,7 +136,7 @@ private static string FormatToItemChainedCalls(string bodyCode) sb.Append(methodCall); // Find the next .Set call - startIndex = bodyCode.IndexOf(".Set", i, StringComparison.Ordinal); + startIndex = FindNextSetMethodCall(bodyCode, i); } return sb.ToString(); @@ -194,17 +194,57 @@ private static string FormatFromItemObjectInitializer(string bodyCode) private static int CountSetMethodCalls(string bodyCode) { var count = 0; - var index = 0; + var searchFrom = 0; - while ((index = bodyCode.IndexOf(".Set", index, StringComparison.Ordinal)) >= 0) + while ((searchFrom = FindNextSetMethodCall(bodyCode, searchFrom)) >= 0) { count++; - index += 4; // Move past ".Set" + searchFrom += 4; // Move past ".Set" } return count; } + /// + /// Finds the next .Set* method call (not a property access like source.Settings) starting at + /// startFrom. Returns the index of the leading '.' or -1 if none found. + /// + private static int FindNextSetMethodCall(string bodyCode, int startFrom) + { + var index = startFrom; + + while ((index = bodyCode.IndexOf(".Set", index, StringComparison.Ordinal)) >= 0) + { + // Advance past ".Set" and any remaining identifier chars (the method name suffix) + var nameEnd = index + 4; + while (nameEnd < bodyCode.Length && + (char.IsLetterOrDigit(bodyCode[nameEnd]) || bodyCode[nameEnd] == '_')) + nameEnd++; + + // Skip generic type parameters, e.g. in .SetList(...) + if (nameEnd < bodyCode.Length && bodyCode[nameEnd] == '<') + { + var depth = 1; + nameEnd++; + while (nameEnd < bodyCode.Length && depth > 0) + { + if (bodyCode[nameEnd] == '<') depth++; + else if (bodyCode[nameEnd] == '>') depth--; + nameEnd++; + } + } + + // A method call must be immediately followed by '(' + if (nameEnd < bodyCode.Length && bodyCode[nameEnd] == '(') + return index; + + // Not a method call (e.g. source.Settings); resume search after this position + index += 4; + } + + return -1; + } + /// Splits property assignments by comma, respecting nested parentheses. private static string[] SplitPropertiesRespectingNesting(string content) { From 1f53c931e4ccbe68279458f89d01bfd79c9b7687 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 15:37:36 -0500 Subject: [PATCH 13/17] feat(property-mapping): centralize type name extraction and sanitization logic - Introduced `TypeNameHelper` utility to extract and sanitize type names from fully qualified strings. - Replaced redundant extraction logic in `HelperMethodEmitter` and `PropertyMappingCodeRenderer`. - Unified `GenerateToItemHelperName` and `GenerateFromItemHelperName` methods into `GenerateHelperName`. - Improved maintainability by removing duplicate code for type sanitization. - Updated relevant method calls to leverage the new `TypeNameHelper` utility. --- .../Emitters/HelperMethodEmitter.cs | 7 +--- .../PropertyMapping/HelperMethodRegistry.cs | 26 +++----------- .../PropertyMappingCodeRenderer.cs | 10 +----- .../PropertyMapping/TypeNameHelper.cs | 34 +++++++++++++++++++ 4 files changed, 40 insertions(+), 37 deletions(-) create mode 100644 src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/TypeNameHelper.cs diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index d787be9..c2f8086 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -13,8 +13,7 @@ public static string RenderToItemHelper( ) { var sb = new StringBuilder(); - var typeName = ExtractSimpleTypeName(helper.ModelFullyQualifiedType); - var paramName = typeName.ToLowerInvariant(); + var paramName = TypeNameHelper.ToParameterName(helper.ModelFullyQualifiedType); // Method signature with arrow syntax (no leading spaces - template handles base // indentation) @@ -279,8 +278,4 @@ private static string[] SplitPropertiesRespectingNesting(string content) return result.ToArray(); } - - private static string ExtractSimpleTypeName(string fullyQualifiedType) => - // "global::MyNamespace.Address" -> "address" - fullyQualifiedType.Split('.').Last().ToLowerInvariant(); } diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs index 599c29f..98799bd 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/HelperMethodRegistry.cs @@ -23,7 +23,7 @@ public string GetOrRegisterToItemHelper( if (_toItemHelpers.TryGetValue(modelFullyQualifiedType, out var existing)) return existing.MethodName; - var methodName = GenerateToItemHelperName(modelFullyQualifiedType); + var methodName = GenerateHelperName(modelFullyQualifiedType, "ToItem"); var helperInfo = new HelperMethodInfo( methodName, @@ -47,7 +47,7 @@ public string GetOrRegisterFromItemHelper( if (_fromItemHelpers.TryGetValue(modelFullyQualifiedType, out var existing)) return existing.MethodName; - var methodName = GenerateFromItemHelperName(modelFullyQualifiedType); + var methodName = GenerateHelperName(modelFullyQualifiedType, "FromItem"); var helperInfo = new HelperMethodInfo( methodName, @@ -64,24 +64,6 @@ public string GetOrRegisterFromItemHelper( public HelperMethodInfo[] GetAllHelpers() => _toItemHelpers.Values.Concat(_fromItemHelpers.Values).ToArray(); - private static string GenerateToItemHelperName(string modelFullyQualifiedType) - { - // Extract simple type name from fully qualified name - // "global::MyNamespace.Address" -> "Address" - var typeName = modelFullyQualifiedType.Split('.').Last(); - - // Sanitize generic types: Result
-> Result_Address - typeName = typeName.Replace("<", "_").Replace(">", "").Replace(",", "_").Replace(" ", ""); - - return $"ToItem_{typeName}"; - } - - private static string GenerateFromItemHelperName(string modelFullyQualifiedType) - { - var typeName = modelFullyQualifiedType.Split('.').Last(); - - typeName = typeName.Replace("<", "_").Replace(">", "").Replace(",", "_").Replace(" ", ""); - - return $"FromItem_{typeName}"; - } + private static string GenerateHelperName(string modelFullyQualifiedType, string prefix) => + $"{prefix}_{TypeNameHelper.ExtractSanitized(modelFullyQualifiedType)}"; } diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs index b0cfa7e..83f156d 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs @@ -560,7 +560,7 @@ HelperMethodRegistry helperRegistry // Otherwise, use var + initializer + if statements pattern var sb = new StringBuilder(); - var varName = ExtractSimpleVarName(inlineInfo.ModelFullyQualifiedType); + var varName = TypeNameHelper.ToVariableName(inlineInfo.ModelFullyQualifiedType); // Variable declaration with object initializer for required/init properties if (initProperties.Count == 0) @@ -718,14 +718,6 @@ private static string RenderTryGetCondition( $"{mapVarName}.{tryMethod}{genericArg}(\"{prop.DynamoKey}\", out var {outVarName}, {typeArgs}Requiredness.InferFromNullability)"; } - /// Extracts a simple variable name from a fully qualified type name. - private static string ExtractSimpleVarName(string fullyQualifiedType) - { - // Remove "global::" prefix and namespace, take last segment, lowercase first letter - var typeName = fullyQualifiedType.Replace("global::", "").Split('.').Last(); - return char.ToLowerInvariant(typeName[0]) + typeName.Substring(1); - } - /// /// Renders the FromItem code for a nested object (regular assignment style). /// diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/TypeNameHelper.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/TypeNameHelper.cs new file mode 100644 index 0000000..2b2b9b0 --- /dev/null +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/TypeNameHelper.cs @@ -0,0 +1,34 @@ +namespace DynamoMapper.Generator.PropertyMapping; + +/// Utility for extracting and sanitizing type names from fully-qualified type strings. +internal static class TypeNameHelper +{ + /// + /// Extracts the simple type name from a fully-qualified type string, sanitized for use as a + /// C# identifier. Strips "global::" prefix, takes the last dot-delimited segment, and replaces + /// generic syntax characters. Example: "global::MyNamespace.Result<Address>" → + /// "Result_Address" + /// + public static string ExtractSanitized(string fullyQualifiedType) + { + var typeName = fullyQualifiedType.Replace("global::", "").Split('.').Last(); + return typeName.Replace("<", "_").Replace(">", "").Replace(",", "_").Replace(" ", ""); + } + + /// + /// Returns the sanitized type name as an all-lowercase string, suitable for use as a method + /// parameter name. Example: "global::MyNamespace.Address" → "address" + /// + public static string ToParameterName(string fullyQualifiedType) => + ExtractSanitized(fullyQualifiedType).ToLowerInvariant(); + + /// + /// Returns the sanitized type name with the first letter lowercased, suitable for use as a + /// local variable name. Example: "global::MyNamespace.Address" → "address" + /// + public static string ToVariableName(string fullyQualifiedType) + { + var name = ExtractSanitized(fullyQualifiedType); + return name.Length == 0 ? name : char.ToLowerInvariant(name[0]) + name.Substring(1); + } +} From 675e5fcdeb3e7c4ef3ff17a278f67921aff6edd0 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 15:57:41 -0500 Subject: [PATCH 14/17] feat(emitters): optimize property mapping rendering logic - Improved dictionary initialization with capacity pre-allocation in `HelperMethodEmitter`. - Simplified and modularized property mapping rendering logic with reusable helper methods. - Introduced `RenderToItemPropertyCall` and `RenderPropertyInitAssignment` as reusable utilities. - Removed redundant formatting methods (`FormatToItemChainedCalls` and `FormatFromItemObjectInitializer`). - Enhanced handling of nested and scalar property mappings with streamlined syntax. --- .../Emitters/HelperMethodEmitter.cs | 274 ++++-------------- .../PropertyMappingCodeRenderer.cs | 117 ++++---- 2 files changed, 120 insertions(+), 271 deletions(-) diff --git a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs index c2f8086..ebb68a6 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Emitters/HelperMethodEmitter.cs @@ -22,20 +22,32 @@ public static string RenderToItemHelper( ); sb.Append(" "); - // Reuse existing RenderInlineNestedToItem logic - var bodyCode = - PropertyMappingCodeRenderer.RenderInlineNestedToItem( - paramName, - helper.InlineInfo, - context, - helperRegistry + // Count properties that produce Set* calls for dictionary capacity pre-allocation + var capacity = + helper.InlineInfo.Properties.Count( + p => p.NestedMapping is not null || (p.Strategy is not null && p.HasGetter) ); + sb.Append($"new Dictionary({capacity})"); - // Format chained method calls on separate lines - var formattedBody = FormatToItemChainedCalls(bodyCode); - sb.Append(formattedBody); - sb.Append(";"); + // Render each property call on its own indented line directly from structured data + foreach (var prop in helper.InlineInfo.Properties) + { + var call = + PropertyMappingCodeRenderer.RenderToItemPropertyCall( + prop, + paramName, + context, + helperRegistry + ); + if (call is not null) + { + sb.AppendLine(); + sb.Append(" "); + sb.Append(call); + } + } + sb.Append(";"); return sb.ToString(); } @@ -54,228 +66,52 @@ public static string RenderFromItemHelper( ); sb.AppendLine("{"); - // Reuse existing RenderInlineNestedFromItem logic - var bodyCode = - PropertyMappingCodeRenderer.RenderInlineNestedFromItem( - mapParamName, - helper.InlineInfo, - context, - helperRegistry + // Determine rendering path from structured data, without an intermediate string + var hasPostConstructionProperties = + helper.InlineInfo.Properties.Any( + p => !p.IsRequired && !p.IsInitOnly && p.HasDefaultValue && p.HasSetter ); - // Check if body is simple expression or multi-statement block - var isSimpleExpression = bodyCode.TrimStart().StartsWith("new "); - - if (isSimpleExpression) + if (!hasPostConstructionProperties) { - // Simple expression: "new Type { ... }" + // Simple path: all properties go into the object initializer — build directly sb.Append(" return "); - var formattedBody = FormatFromItemObjectInitializer(bodyCode); - sb.Append(formattedBody); - sb.AppendLine(";"); - } - else - { - // Multi-statement block: "var x = new Type { ... }; if (...) ...; return x;" - // Body already contains proper indentation and return statement - sb.Append(bodyCode); - sb.AppendLine(); - } - - sb.AppendLine("}"); - - return sb.ToString(); - } - - /// - /// Formats ToItem chained method calls on separate lines. Input: new Dictionary<string, - /// AttributeValue>().SetString(...).SetDecimal(...) Output: new Dictionary<string, - /// AttributeValue>(capacity) .SetString(...) .SetDecimal(...) - /// - private static string FormatToItemChainedCalls(string bodyCode) - { - // Count how many .Set* method calls there are for capacity pre-allocation - var capacity = CountSetMethodCalls(bodyCode); - - var sb = new StringBuilder(); - sb.Append($"new Dictionary({capacity})"); - - // Find each .Set* method call and put it on a new line - var startIndex = FindNextSetMethodCall(bodyCode, 0); - - while (startIndex >= 0) - { - // Find the matching closing parenthesis for this method call - var i = startIndex + 1; // skip the '.' - - // Find the opening parenthesis - while (i < bodyCode.Length && bodyCode[i] != '(') - i++; - - if (i >= bodyCode.Length) - break; + sb.AppendLine($"new {helper.ModelFullyQualifiedType}"); + sb.AppendLine(" {"); - i++; // skip the '(' - var parenCount = 1; - - // Find the matching closing parenthesis - while (i < bodyCode.Length && parenCount > 0) - { - if (bodyCode[i] == '(') - parenCount++; - else if (bodyCode[i] == ')') - parenCount--; - i++; - } - - // Extract the method call - var methodCall = bodyCode.Substring(startIndex, i - startIndex); - sb.AppendLine(); - sb.Append(" "); - sb.Append(methodCall); - - // Find the next .Set call - startIndex = FindNextSetMethodCall(bodyCode, i); - } - - return sb.ToString(); - } - - /// - /// Formats FromItem object initializer on separate lines. Input: new MyType { Prop1 = val1, - /// Prop2 = val2, } Output: new MyType { Prop1 = val1, Prop2 = val2, } - /// - private static string FormatFromItemObjectInitializer(string bodyCode) - { - // Find the opening brace - var braceIndex = bodyCode.IndexOf('{'); - if (braceIndex < 0) - return bodyCode; // No object initializer, return as-is - - var sb = new StringBuilder(); - - // Type name before the brace - var typePart = bodyCode.Substring(0, braceIndex).TrimEnd(); - sb.AppendLine(typePart); - sb.AppendLine(" {"); - - // Extract the properties inside the braces - var propsStart = braceIndex + 1; - var propsEnd = bodyCode.LastIndexOf('}'); - if (propsEnd < 0) - return bodyCode; // Malformed, return as-is - - var propsContent = bodyCode.Substring(propsStart, propsEnd - propsStart).Trim(); - - // Split on commas, but need to be careful about nested commas - var properties = SplitPropertiesRespectingNesting(propsContent); - - for (var i = 0; i < properties.Length; i++) - { - var trimmed = properties[i].Trim(); - if (!string.IsNullOrEmpty(trimmed)) + foreach (var prop in helper.InlineInfo.Properties) { sb.Append(" "); - sb.Append(trimmed); - // Add comma after each property (including the last one for trailing comma style) - if (!trimmed.EndsWith(",")) - sb.Append(","); + PropertyMappingCodeRenderer.RenderPropertyInitAssignment( + prop, + mapParamName, + sb, + context, + helperRegistry + ); sb.AppendLine(); } - } - - sb.Append(" }"); - - return sb.ToString(); - } - /// Counts the number of .Set* method calls in the body code for dictionary capacity. - private static int CountSetMethodCalls(string bodyCode) - { - var count = 0; - var searchFrom = 0; - - while ((searchFrom = FindNextSetMethodCall(bodyCode, searchFrom)) >= 0) - { - count++; - searchFrom += 4; // Move past ".Set" - } - - return count; - } - - /// - /// Finds the next .Set* method call (not a property access like source.Settings) starting at - /// startFrom. Returns the index of the leading '.' or -1 if none found. - /// - private static int FindNextSetMethodCall(string bodyCode, int startFrom) - { - var index = startFrom; - - while ((index = bodyCode.IndexOf(".Set", index, StringComparison.Ordinal)) >= 0) - { - // Advance past ".Set" and any remaining identifier chars (the method name suffix) - var nameEnd = index + 4; - while (nameEnd < bodyCode.Length && - (char.IsLetterOrDigit(bodyCode[nameEnd]) || bodyCode[nameEnd] == '_')) - nameEnd++; - - // Skip generic type parameters, e.g. in .SetList(...) - if (nameEnd < bodyCode.Length && bodyCode[nameEnd] == '<') - { - var depth = 1; - nameEnd++; - while (nameEnd < bodyCode.Length && depth > 0) - { - if (bodyCode[nameEnd] == '<') depth++; - else if (bodyCode[nameEnd] == '>') depth--; - nameEnd++; - } - } - - // A method call must be immediately followed by '(' - if (nameEnd < bodyCode.Length && bodyCode[nameEnd] == '(') - return index; - - // Not a method call (e.g. source.Settings); resume search after this position - index += 4; + sb.Append(" }"); + sb.AppendLine(";"); } - - return -1; - } - - /// Splits property assignments by comma, respecting nested parentheses. - private static string[] SplitPropertiesRespectingNesting(string content) - { - var result = new List(); - var current = new StringBuilder(); - var depth = 0; - - for (var i = 0; i < content.Length; i++) + else { - var ch = content[i]; - - if (ch == '(' || ch == '<' || ch == '{') - { - depth++; - } - else if (ch == ')' || ch == '>' || ch == '}') - { - depth--; - } - else if (ch == ',' && depth == 0) - { - result.Add(current.ToString()); - current.Clear(); - continue; - } - - current.Append(ch); + // Complex path: mix of init and post-construction assignments. + // RenderInlineNestedFromItem already emits properly indented multi-line code. + var bodyCode = + PropertyMappingCodeRenderer.RenderInlineNestedFromItem( + mapParamName, + helper.InlineInfo, + context, + helperRegistry + ); + sb.Append(bodyCode); + sb.AppendLine(); } - if (current.Length > 0) - result.Add(current.ToString()); + sb.AppendLine("}"); - return result.ToArray(); + return sb.ToString(); } } diff --git a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs index 83f156d..27a0530 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/PropertyMappingCodeRenderer.cs @@ -374,62 +374,72 @@ HelperMethodRegistry helperRegistry foreach (var prop in inlineInfo.Properties) { - if (prop.NestedMapping is not null) - { - // Recursive nested object - var nestedSourcePrefix = $"{sourcePrefix}.{prop.PropertyName}"; - - string nestedCode; - if (prop.NestedMapping is MapperBasedNesting mapperBased) - { - nestedCode = - $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem({nestedSourcePrefix}) }}"; - } - else if (prop.NestedMapping is InlineNesting inline) - { - // Register helper and generate call instead of inlining - var helperMethodName = - helperRegistry.GetOrRegisterToItemHelper( - inline.Info.ModelFullyQualifiedType, - inline.Info - ); - nestedCode = - $"new AttributeValue {{ M = {helperMethodName}({nestedSourcePrefix}) }}"; - } - else - { - throw new InvalidOperationException("Unknown nested mapping type"); - } + var call = RenderToItemPropertyCall(prop, sourcePrefix, context, helperRegistry); + if (call is not null) + sb.Append(call); + } + + return sb.ToString(); + } + + /// + /// Renders a single property's .Set*(...) call fragment for ToItem mapping. Returns null if + /// the property produces no call (no getter, no strategy, no nested mapping). Made internal to + /// allow reuse by HelperMethodEmitter for formatted multi-line rendering. + /// + internal static string? RenderToItemPropertyCall( + NestedPropertySpec prop, string sourcePrefix, GeneratorContext context, + HelperMethodRegistry helperRegistry + ) + { + if (prop.NestedMapping is not null) + { + var nestedSourcePrefix = $"{sourcePrefix}.{prop.PropertyName}"; - sb.Append( - $".Set(\"{prop.DynamoKey}\", {nestedSourcePrefix} is null ? new AttributeValue {{ NULL = true }} : {nestedCode})" - ); + string nestedCode; + if (prop.NestedMapping is MapperBasedNesting mapperBased) + { + nestedCode = + $"new AttributeValue {{ M = {mapperBased.Mapper.MapperFullyQualifiedName}.ToItem({nestedSourcePrefix}) }}"; } - else if (prop.Strategy is not null && prop.HasGetter) + else if (prop.NestedMapping is InlineNesting inline) { - // Scalar property - only render if it has a getter - var setMethod = $"Set{prop.Strategy.TypeName}"; - var genericArg = prop.Strategy.GenericArgument; - var propValue = $"{sourcePrefix}.{prop.PropertyName}"; - - // Build type-specific args - var typeArgs = - prop.Strategy.ToTypeSpecificArgs.Length > 0 - ? ", " + string.Join(", ", prop.Strategy.ToTypeSpecificArgs) - : ""; - - // Omit flags - use mapper defaults - var omitEmpty = - context.MapperOptions.OmitEmptyStrings.ToString().ToLowerInvariant(); - var omitNull = context.MapperOptions.OmitNullStrings.ToString().ToLowerInvariant(); - - sb.Append( - $".{setMethod}{genericArg}(\"{prop.DynamoKey}\", {propValue}{typeArgs}, {omitEmpty}, {omitNull})" - ); + var helperMethodName = + helperRegistry.GetOrRegisterToItemHelper( + inline.Info.ModelFullyQualifiedType, + inline.Info + ); + nestedCode = + $"new AttributeValue {{ M = {helperMethodName}({nestedSourcePrefix}) }}"; + } + else + { + throw new InvalidOperationException("Unknown nested mapping type"); } + + return + $".Set(\"{prop.DynamoKey}\", {nestedSourcePrefix} is null ? new AttributeValue {{ NULL = true }} : {nestedCode})"; } - return sb.ToString(); + if (prop.Strategy is not null && prop.HasGetter) + { + var setMethod = $"Set{prop.Strategy.TypeName}"; + var genericArg = prop.Strategy.GenericArgument; + var propValue = $"{sourcePrefix}.{prop.PropertyName}"; + + var typeArgs = + prop.Strategy.ToTypeSpecificArgs.Length > 0 + ? ", " + string.Join(", ", prop.Strategy.ToTypeSpecificArgs) + : ""; + + var omitEmpty = context.MapperOptions.OmitEmptyStrings.ToString().ToLowerInvariant(); + var omitNull = context.MapperOptions.OmitNullStrings.ToString().ToLowerInvariant(); + + return + $".{setMethod}{genericArg}(\"{prop.DynamoKey}\", {propValue}{typeArgs}, {omitEmpty}, {omitNull})"; + } + + return null; } /// @@ -618,8 +628,11 @@ private static string RenderSimpleNestedFromItem( return sb.ToString(); } - /// Renders a single property initialization assignment for object initializer. - private static void RenderPropertyInitAssignment( + /// + /// Renders a single property initialization assignment for object initializer. Made internal + /// to allow reuse by HelperMethodEmitter for formatted multi-line rendering. + /// + internal static void RenderPropertyInitAssignment( NestedPropertySpec prop, string mapVarName, StringBuilder sb, GeneratorContext context, HelperMethodRegistry helperRegistry ) From e48f732e9bfb5ea4ccb064dd0842551d030fbd02 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 17 Feb 2026 16:56:57 -0500 Subject: [PATCH 15/17] chore(deps): update Verify.XunitV3 to version 31.13.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 03cd980..24b20e5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 1f8c74770c0b6fbeb10e5875f20c83dbe0962c39 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Tue, 17 Feb 2026 20:13:13 -0500 Subject: [PATCH 16/17] refactor(models): make `MapperInfo` sealed and adjust equality implementation - Changed `MapperInfo` to a sealed record to prevent inheritance. - Removed `virtual` keyword from the `Equals` method for consistency. - Updated equality logic to streamline comparison operations. --- LayeredCraft.DynamoMapper.sln.DotSettings | 1 + .../Models/MapperInfo.cs | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/LayeredCraft.DynamoMapper.sln.DotSettings b/LayeredCraft.DynamoMapper.sln.DotSettings index 2c26edd..6f48e3e 100644 --- a/LayeredCraft.DynamoMapper.sln.DotSettings +++ b/LayeredCraft.DynamoMapper.sln.DotSettings @@ -92,6 +92,7 @@ ExpressionBody True False + False False False False diff --git a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs index c80c213..7e95b50 100644 --- a/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs +++ b/src/LayeredCraft.DynamoMapper.Generators/Models/MapperInfo.cs @@ -5,7 +5,7 @@ namespace DynamoMapper.Generator.Models; -internal record MapperInfo( +internal sealed record MapperInfo( MapperClassInfo? MapperClass, ModelClassInfo? ModelClass, EquatableArray Diagnostics, @@ -13,10 +13,9 @@ internal record MapperInfo( HelperMethodRegistry? HelperRegistry ) { - public virtual bool Equals(MapperInfo? other) => other is not null && - MapperClass == other.MapperClass && - ModelClass == other.ModelClass && - Diagnostics == other.Diagnostics; + public bool Equals(MapperInfo? other) => other is not null && + MapperClass == other.MapperClass && ModelClass == other.ModelClass && + Diagnostics == other.Diagnostics; public override int GetHashCode() => HashCode.Combine(MapperClass, ModelClass, Diagnostics); } From af6fb94884c0c5c90a81fe3420ff1b2627e7bdbf Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Wed, 18 Feb 2026 07:54:56 -0500 Subject: [PATCH 17/17] chore: update version prefix to 1.0.4 and SDK version to 10.0.103 --- Directory.Build.props | 2 +- LayeredCraft.DynamoMapper.slnx | 1 + global.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index bf658c6..927879a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.0.3 + 1.0.4 MIT diff --git a/LayeredCraft.DynamoMapper.slnx b/LayeredCraft.DynamoMapper.slnx index 623c4a8..7fecd2d 100644 --- a/LayeredCraft.DynamoMapper.slnx +++ b/LayeredCraft.DynamoMapper.slnx @@ -71,6 +71,7 @@ + diff --git a/global.json b/global.json index 4ea6570..248c7c5 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestMinor", - "version": "10.0.101" + "version": "10.0.103" }, "test": { "runner": "Microsoft.Testing.Platform"