Skip to content

Commit be26057

Browse files
authored
feat: support GUID collections and set-like CLR collections (#99)
1 parent 6206e5e commit be26057

21 files changed

Lines changed: 1006 additions & 237 deletions

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ trim_trailing_whitespace = true
1616
indent_size = 4
1717
max_line_length = 120
1818

19-
[*HostFactoryResolver.cs]
19+
[{*HostFactoryResolver.cs,AnalyzerReleases.Shipped.md,AnalyzerReleases.Unshipped.md}]
2020
ij_formatter_enabled = false
2121
resharper_disable_formatter = true

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<VersionPrefix>1.2.0</VersionPrefix>
3+
<VersionPrefix>1.3.0</VersionPrefix>
44
<!-- SPDX license identifier for MIT -->
55
<PackageLicenseExpression>MIT</PackageLicenseExpression>
66
<!-- Other useful metadata -->

docs/core-concepts/how-it-works.md

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,20 @@ static partial void AfterToItem(Product source, Dictionary<string, AttributeValu
248248

249249
DynamoMapper natively supports:
250250

251-
| .NET Type | DynamoDB Type | Notes |
252-
|------------------------------------------|---------------|--------------------------------------------------|
253-
| `string` | S (String) | |
254-
| `int`, `long`, `decimal`, `double` | N (Number) | Culture-invariant |
255-
| `bool` | BOOL | |
256-
| `Guid` | S | ToString/Parse |
257-
| `DateTime`, `DateTimeOffset`, `TimeSpan` | S | ISO-8601 / constant format |
258-
| `enum` | S | String name |
259-
| Nullable variants | S/N/BOOL | Null checks generated |
260-
| Collections | L/M/SS/NS/BS | Lists, maps, and sets of supported element types |
251+
| .NET Type | DynamoDB Type | Notes |
252+
|------------------------------------------|---------------|--------------------------------------|
253+
| `string` | S (String) | |
254+
| `int`, `long`, `decimal`, `double` | N (Number) | Culture-invariant |
255+
| `bool` | BOOL | |
256+
| `Guid` | S | ToString/Parse |
257+
| `DateTime`, `DateTimeOffset`, `TimeSpan` | S | ISO-8601 / constant format |
258+
| `enum` | S | String name |
259+
| Nullable variants | S/N/BOOL | Null checks generated |
260+
| Collections | L/M/SS/NS/BS | Lists, maps, and set-like CLR shapes |
261+
262+
Set-like CLR collections use DynamoDB native set types (`SS`, `NS`, `BS`) when the element type
263+
supports them. Other supported set element types, such as `Guid`, `DateTimeOffset`, `TimeSpan`,
264+
and enums, are stored as `L` and materialized back into the declared CLR set shape during reads.
261265

262266
### Custom Types
263267

skills/dynamo-mapper/references/type-matrix.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Supported root collection shapes:
1818
Important note:
1919

2020
- `byte[]` is supported as a byte collection, not as a special binary scalar type
21+
- set-like CLR shapes preserve their declared shape on round-trip, but DynamoDB storage depends on
22+
the element type
2123

2224
## Supported collection elements
2325

@@ -34,6 +36,10 @@ Set families:
3436
- numeric -> `NS`
3537
- `byte[]` -> `BS`
3638

39+
When a set-like CLR collection uses another supported element type such as `Guid`,
40+
`DateTimeOffset`, `TimeSpan`, or enums, DynamoMapper stores it as `L` and materializes it back
41+
into the declared CLR set shape on read.
42+
3743
## Nested shapes
3844

3945
Supported:
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
11
; Shipped analyzer releases
22
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
33

4+
## Release 1.2.0
5+
6+
### New Rules
7+
8+
Rule ID | Category | Severity | Notes
9+
--------|----------|----------|------
10+
DM0001 | DynamoMapper.Usage | Error |
11+
DM0003 | DynamoMapper.Usage | Error |
12+
DM0004 | DynamoMapper.Usage | Error |
13+
DM0005 | DynamoMapper.Usage | Error |
14+
DM0006 | DynamoMapper.Usage | Error |
15+
DM0007 | DynamoMapper.Usage | Error |
16+
DM0008 | DynamoMapper.Usage | Error |
17+
DM0009 | DynamoMapper.Usage | Error |
18+
DM0101 | DynamoMapper.Usage | Error |
19+
DM0102 | DynamoMapper.Usage | Error |
20+
DM0103 | DynamoMapper.Usage | Error |
Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
; Unshipped analyzer release
22
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
33

4-
### New Rules
4+
### Changed Rules
55

6-
Rule ID | Category | Severity | Notes
7-
---------|--------------------|----------|-----------------------
8-
DM0001 | DynamoMapper.Usage | Error | DiagnosticDescriptors
9-
DM0003 | DynamoMapper.Usage | Error | DiagnosticDescriptors
10-
DM0004 | DynamoMapper.Usage | Error | DiagnosticDescriptors
11-
DM0005 | DynamoMapper.Usage | Error | DiagnosticDescriptors
12-
DM0006 | DynamoMapper.Usage | Error | DiagnosticDescriptors
13-
DM0007 | DynamoMapper.Usage | Error | DiagnosticDescriptors
14-
DM0008 | DynamoMapper.Usage | Error | DiagnosticDescriptors
15-
DM0009 | DynamoMapper.Usage | Error | DiagnosticDescriptors
16-
DM0101 | DynamoMapper.Usage | Error | DiagnosticDescriptors
17-
DM0102 | DynamoMapper.Usage | Error | DiagnosticDescriptors
18-
DM0103 | DynamoMapper.Usage | Error | DiagnosticDescriptors
6+
Rule ID | New Category | New Severity | Old Category | Old Severity | Notes
7+
--------|--------------|--------------|--------------|--------------|------
8+
DM0001 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
9+
DM0003 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
10+
DM0004 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
11+
DM0005 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
12+
DM0006 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
13+
DM0007 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
14+
DM0008 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
15+
DM0009 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
16+
DM0101 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
17+
DM0102 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed
18+
DM0103 | LayeredCraft.DynamoMapper.Usage | Error | DynamoMapper.Usage | Error | Category changed

src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/CollectionTypeAnalyzer.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal readonly record struct ElementTypeValidationResult(
4040
ElementType: elementType,
4141
TargetKind: DynamoKind.L,
4242
KeyType: null,
43-
IsArray: true
43+
CollectionReadMaterialization.Array
4444
);
4545
}
4646

@@ -66,8 +66,7 @@ internal readonly record struct ElementTypeValidationResult(
6666
Category: CollectionCategory.Map,
6767
ElementType: valueType,
6868
TargetKind: DynamoKind.M,
69-
KeyType: keyType,
70-
IsArray: false
69+
keyType
7170
);
7271
}
7372
}
@@ -92,9 +91,17 @@ internal readonly record struct ElementTypeValidationResult(
9291
ElementType: elementType,
9392
TargetKind: setKind.Value,
9493
KeyType: null,
95-
IsArray: false
94+
CollectionReadMaterialization.HashSet
9695
);
9796
}
97+
98+
return new CollectionInfo(
99+
CollectionCategory.List,
100+
elementType,
101+
DynamoKind.L,
102+
null,
103+
CollectionReadMaterialization.HashSet
104+
);
98105
}
99106
}
100107

@@ -120,8 +127,7 @@ internal readonly record struct ElementTypeValidationResult(
120127
Category: CollectionCategory.List,
121128
ElementType: elementType,
122129
TargetKind: DynamoKind.L,
123-
KeyType: null,
124-
IsArray: false
130+
null
125131
);
126132
}
127133
}

src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/Models/CollectionInfo.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ namespace LayeredCraft.DynamoMapper.Generator.PropertyMapping.Models;
1616
/// <param name="KeyType">
1717
/// For map types only, the key type (must be string).
1818
/// </param>
19-
/// <param name="IsArray">
20-
/// True if the original property type is an array (T[]), false otherwise.
19+
/// <param name="ReadMaterialization">
20+
/// Describes any additional CLR-shape materialization required after deserializing the
21+
/// DynamoDB representation.
2122
/// </param>
2223
/// <param name="ElementNestedMapping">
2324
/// For collections of nested objects, contains the nested mapping info for the element type.
@@ -28,10 +29,21 @@ internal sealed record CollectionInfo(
2829
ITypeSymbol ElementType,
2930
DynamoKind TargetKind,
3031
ITypeSymbol? KeyType = null,
31-
bool IsArray = false,
32+
CollectionReadMaterialization ReadMaterialization = CollectionReadMaterialization.None,
3233
NestedMappingInfo? ElementNestedMapping = null
3334
);
3435

36+
/// <summary>
37+
/// Describes how a deserialized collection result should be materialized back into the
38+
/// declared CLR shape.
39+
/// </summary>
40+
internal enum CollectionReadMaterialization
41+
{
42+
None,
43+
Array,
44+
HashSet,
45+
}
46+
3547
/// <summary>
3648
/// Categorizes collection types by their DynamoDB mapping behavior.
3749
/// </summary>

src/LayeredCraft.DynamoMapper.Generators/PropertyMapping/NestedObjectTypeAnalyzer.cs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,13 @@ private static IPropertySymbol[] GetMappableProperties(
304304
}
305305

306306
// Create collection strategy (simplified for nested objects)
307-
var collectionStrategy = CreateCollectionStrategy(collectionInfo, propertyType);
307+
var collectionStrategy =
308+
CreateCollectionStrategy(
309+
collectionInfo,
310+
propertyType,
311+
fieldOptions,
312+
nestedContext.Context
313+
);
308314
propertySpecs.Add(
309315
new NestedPropertySpec(
310316
property.Name,
@@ -532,7 +538,8 @@ GeneratorContext context
532538
/// Creates a collection type mapping strategy.
533539
/// </summary>
534540
private static TypeMappingStrategy CreateCollectionStrategy(
535-
CollectionInfo collectionInfo, ITypeSymbol originalType
541+
CollectionInfo collectionInfo, ITypeSymbol originalType, DynamoFieldOptions? fieldOptions,
542+
GeneratorContext context
536543
)
537544
{
538545
var isNullable = originalType.NullableAnnotation == NullableAnnotation.Annotated;
@@ -560,16 +567,61 @@ private static TypeMappingStrategy CreateCollectionStrategy(
560567
),
561568
};
562569

570+
var (fromArgs, toArgs) =
571+
GetCollectionElementTypeSpecificArgs(collectionInfo.ElementType, fieldOptions, context);
572+
563573
return new TypeMappingStrategy(
564574
typeName,
565575
genericArg,
566576
nullableModifier,
567-
[],
568-
[],
577+
fromArgs,
578+
toArgs,
569579
KindOverride: collectionInfo.TargetKind
570580
);
571581
}
572582

583+
private static (string[] FromArgs, string[] ToArgs) GetCollectionElementTypeSpecificArgs(
584+
ITypeSymbol elementType, DynamoFieldOptions? fieldOptions, GeneratorContext context
585+
)
586+
{
587+
var underlyingType = UnwrapNullable(elementType);
588+
589+
return underlyingType switch
590+
{
591+
{ SpecialType: SpecialType.System_DateTime } => CreateCollectionFormatArgs(
592+
fieldOptions?.Format ?? context.MapperOptions.DateTimeFormat
593+
),
594+
INamedTypeSymbol t when context.WellKnownTypes.IsType(
595+
t,
596+
WellKnownTypeData.WellKnownType.System_DateTimeOffset
597+
) => CreateCollectionFormatArgs(
598+
fieldOptions?.Format ?? context.MapperOptions.DateTimeFormat
599+
),
600+
INamedTypeSymbol t when context.WellKnownTypes.IsType(
601+
t,
602+
WellKnownTypeData.WellKnownType.System_Guid
603+
) => CreateCollectionFormatArgs(
604+
fieldOptions?.Format ?? context.MapperOptions.GuidFormat
605+
),
606+
INamedTypeSymbol t when context.WellKnownTypes.IsType(
607+
t,
608+
WellKnownTypeData.WellKnownType.System_TimeSpan
609+
) => CreateCollectionFormatArgs(
610+
fieldOptions?.Format ?? context.MapperOptions.TimeSpanFormat
611+
),
612+
INamedTypeSymbol { TypeKind: TypeKind.Enum } => CreateCollectionFormatArgs(
613+
fieldOptions?.Format ?? context.MapperOptions.EnumFormat
614+
),
615+
_ => ([], []),
616+
};
617+
}
618+
619+
private static (string[] FromArgs, string[] ToArgs) CreateCollectionFormatArgs(string format)
620+
{
621+
var arg = $"\"{format}\"";
622+
return ([arg], [arg]);
623+
}
624+
573625
/// <summary>
574626
/// Unwraps Nullable{T} to get the underlying type.
575627
/// </summary>

0 commit comments

Comments
 (0)