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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .claude/commands/pr.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Open PR for current branch
- $ARGUMENTS
- PR title must follow the format: `<type>(<scope>): <description>`
- If this PR is resolving an issue, mention the issue in the PR. If it closes an issue, use `closes #<issue number>` in the PR body.
- Use this template for the PR:

---
Expand All @@ -27,6 +28,8 @@

Closes #...

> Related issues or PRs. If this PR closes an issue, mention it here.

---

## πŸ’¬ Notes for Reviewers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ internal class MapperOptions
internal string TimeSpanFormat { get; set; } = "c";
internal Requiredness DefaultRequiredness { get; set; } = Requiredness.InferFromNullability;
internal string EnumFormat { get; set; } = "G";
internal string GuidFormat { get; set; } = "D";
internal bool OmitEmptyStrings { get; set; } = false;
internal bool OmitNullStrings { get; set; } = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,11 @@ GeneratorContext context
};

// Arguments 3+: Type-specific arguments (format strings)
// Format strings should use named parameters to avoid ambiguity
args.AddRange(
strategy.ToTypeSpecificArgs.Select(
(typeArg, index) =>
new ArgumentSpec(
// First arg that's a quoted string is typically a format parameter - use
// named parameter
index == 0
&& typeArg.StartsWith("\"")
? $"format: {typeArg}"
: typeArg,
ArgumentSource.TypeSpecific
)
)
strategy.ToTypeSpecificArgs.Select(typeArg => new ArgumentSpec(
typeArg,
ArgumentSource.TypeSpecific
))
);

// Omit flags: field override > global default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ when t.IsAssignableTo(WellKnownType.System_DateTimeOffset, context) =>
)
),
INamedTypeSymbol t when t.IsAssignableTo(WellKnownType.System_Guid, context) =>
CreateStrategy("Guid", analysis.Nullability),
$"\"{context.MapperOptions.GuidFormat}\"".Map(guidFmt =>
CreateStrategy("Guid", analysis.Nullability, fromArg: guidFmt, toArg: guidFmt)
),
INamedTypeSymbol t when t.IsAssignableTo(WellKnownType.System_TimeSpan, context) =>
$"\"{context.MapperOptions.TimeSpanFormat}\"".Map(timeFmt =>
CreateStrategy(
Expand Down Expand Up @@ -282,7 +284,7 @@ GeneratorContext context
};

// Build strategy - collections are nullable at collection level, not element level
var strategy = CreateStrategy(typeName, analysis.Nullability, genericArg: genericArg);
var strategy = CreateStrategy(typeName, analysis.Nullability, genericArg);

// Apply Kind override if present (or use inferred kind)
return strategy with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,103 +10,16 @@ public static class GuidAttributeValueExtensions
{
extension(Dictionary<string, AttributeValue> attributes)
{
/// <summary>Tries to get a <see cref="Guid" /> value from the attribute dictionary.</summary>
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="value">The <see cref="Guid" /> value when found.</param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
/// <see cref="Requiredness.InferFromNullability" />.
/// </param>
/// <param name="kind">The DynamoDB attribute kind to interpret as a string.</param>
/// <returns><c>true</c> when the key exists and the value is retrieved; otherwise <c>false</c>.</returns>
public bool TryGetGuid(
string key,
out Guid value,
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
)
{
value = Guid.Empty;
if (!attributes.TryGetValue(key, requiredness, out var attribute))
return false;

var stringValue = attribute!.GetString(kind);
value = stringValue.Length == 0 ? Guid.Empty : Guid.Parse(stringValue);
return true;
}

/// <summary>Gets a <see cref="Guid" /> value from the attribute dictionary.</summary>
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
/// <see cref="Requiredness.InferFromNullability" />.
/// </param>
/// <param name="kind">The DynamoDB attribute kind to interpret as a string.</param>
/// <returns>
/// The <see cref="Guid" /> value if the key exists and is valid; otherwise
/// <see cref="Guid.Empty" /> if the key is missing or the attribute has a DynamoDB NULL value.
/// </returns>
public Guid GetGuid(
string key,
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
) => attributes.TryGetGuid(key, out var value, requiredness, kind) ? value : Guid.Empty;

/// <summary>Tries to get a nullable <see cref="Guid" /> value from the attribute dictionary.</summary>
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="value">The nullable <see cref="Guid" /> value when found.</param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
/// <see cref="Requiredness.InferFromNullability" />.
/// </param>
/// <param name="kind">The DynamoDB attribute kind to interpret as a string.</param>
/// <returns><c>true</c> when the key exists and the value is retrieved; otherwise <c>false</c>.</returns>
public bool TryGetNullableGuid(
string key,
out Guid? value,
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
)
{
value = null;
if (!attributes.TryGetNullableValue(key, requiredness, out var attribute))
return false;

if (attribute.IsNull)
return true;

var stringValue = attribute!.GetNullableString(kind);
value = stringValue is null ? null : Guid.Parse(stringValue);
return true;
}

/// <summary>Gets a nullable <see cref="Guid" /> value from the attribute dictionary.</summary>
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
/// <see cref="Requiredness.InferFromNullability" />.
/// </param>
/// <param name="kind">The DynamoDB attribute kind to interpret as a string.</param>
/// <returns>
/// The <see cref="Guid" /> value if the key exists and is valid; otherwise <c>null</c> if the
/// key is missing or the attribute has a DynamoDB NULL value.
/// </returns>
public Guid? GetNullableGuid(
string key,
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
) => attributes.TryGetNullableGuid(key, out var value, requiredness, kind) ? value : null;

/// <summary>
/// Tries to get a <see cref="Guid" /> value from the attribute dictionary using an exact
/// format string.
/// </summary>
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="value">The <see cref="Guid" /> value when found.</param>
/// <param name="format">
/// The exact format string to use for parsing. Valid formats: "N" (32 digits),
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal).
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal). Default is "D".
/// </param>
/// <param name="value">The <see cref="Guid" /> value when found.</param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
/// <see cref="Requiredness.InferFromNullability" />.
Expand All @@ -115,8 +28,8 @@ public bool TryGetNullableGuid(
/// <returns><c>true</c> when the key exists and the value is retrieved; otherwise <c>false</c>.</returns>
public bool TryGetGuid(
string key,
string format,
out Guid value,
string format = "D",
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
)
Expand All @@ -137,7 +50,7 @@ public bool TryGetGuid(
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="format">
/// The exact format string to use for parsing. Valid formats: "N" (32 digits),
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal).
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal). Default is "D".
/// </param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
Expand All @@ -150,11 +63,11 @@ public bool TryGetGuid(
/// </returns>
public Guid GetGuid(
string key,
string format,
string format = "D",
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
) =>
attributes.TryGetGuid(key, format, out var value, requiredness, kind)
attributes.TryGetGuid(key, out var value, format, requiredness, kind)
? value
: Guid.Empty;

Expand All @@ -163,11 +76,11 @@ public Guid GetGuid(
/// exact format string.
/// </summary>
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="value">The nullable <see cref="Guid" /> value when found.</param>
/// <param name="format">
/// The exact format string to use for parsing. Valid formats: "N" (32 digits),
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal).
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal). Default is "D".
/// </param>
/// <param name="value">The nullable <see cref="Guid" /> value when found.</param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
/// <see cref="Requiredness.InferFromNullability" />.
Expand All @@ -176,8 +89,8 @@ public Guid GetGuid(
/// <returns><c>true</c> when the key exists and the value is retrieved; otherwise <c>false</c>.</returns>
public bool TryGetNullableGuid(
string key,
string format,
out Guid? value,
string format = "D",
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
)
Expand All @@ -201,7 +114,7 @@ public bool TryGetNullableGuid(
/// <param name="key">The attribute key to retrieve.</param>
/// <param name="format">
/// The exact format string to use for parsing. Valid formats: "N" (32 digits),
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal).
/// "D" (hyphens), "B" (braces), "P" (parentheses), "X" (hexadecimal). Default is "D".
/// </param>
/// <param name="requiredness">
/// Specifies whether the attribute is required. Default is
Expand All @@ -214,11 +127,11 @@ public bool TryGetNullableGuid(
/// </returns>
public Guid? GetNullableGuid(
string key,
string format,
string format = "D",
Requiredness requiredness = Requiredness.InferFromNullability,
DynamoKind kind = DynamoKind.S
) =>
attributes.TryGetNullableGuid(key, format, out var value, requiredness, kind)
attributes.TryGetNullableGuid(key, out var value, format, requiredness, kind)
? value
: null;

Expand Down
19 changes: 19 additions & 0 deletions src/LayeredCraft.DynamoMapper.Runtime/DynamoMapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,23 @@ public sealed class DynamoMapperAttribute : Attribute
/// </list>
/// </remarks>
public string EnumFormat { get; set; } = "G";

/// <summary>Gets or sets the default Guid format string.</summary>
/// <remarks>
/// <para>The default is "D" (standard format with hyphens): 00000000-0000-0000-0000-000000000000</para>
/// <para>Valid format strings:</para>
/// <list type="bullet">
/// <item>"N" - 32 digits: 00000000000000000000000000000000</item>
/// <item>"D" - 32 digits separated by hyphens: 00000000-0000-0000-0000-000000000000</item>
/// <item>
/// "B" - 32 digits separated by hyphens, enclosed in braces:
/// {00000000-0000-0000-0000-000000000000}
/// </item>
/// <item>
/// "P" - 32 digits separated by hyphens, enclosed in parentheses:
/// (00000000-0000-0000-0000-000000000000)
/// </item>
/// </list>
/// </remarks>
public string GuidFormat { get; set; } = "D";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”„ I don't like these being separate fields. I would rather have a single Format string field. Depending on what the type is, that would determine where/how the format is applied.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be applying these on the DynamoFieldAttribute.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the overall format for all items. I am adding per-field formats next.

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,26 @@ public static partial class ExampleEntityMapper
new Dictionary<string, AttributeValue>(22)
.SetBool("bool", source.Bool, false, true)
.SetBool("nullableBool", source.NullableBool, false, true)
.SetDateTime("dateTime", source.DateTime, format: "O", false, true)
.SetDateTime("nullableDateTime", source.NullableDateTime, format: "O", false, true)
.SetDateTimeOffset("dateTimeOffset", source.DateTimeOffset, format: "O", false, true)
.SetDateTimeOffset("nullableDateTimeOffset", source.NullableDateTimeOffset, format: "O", false, true)
.SetDateTime("dateTime", source.DateTime, "O", false, true)
.SetDateTime("nullableDateTime", source.NullableDateTime, "O", false, true)
.SetDateTimeOffset("dateTimeOffset", source.DateTimeOffset, "O", false, true)
.SetDateTimeOffset("nullableDateTimeOffset", source.NullableDateTimeOffset, "O", false, true)
.SetDecimal("decimal", source.Decimal, false, true)
.SetDecimal("nullableDecimal", source.NullableDecimal, false, true)
.SetDouble("double", source.Double, false, true)
.SetDouble("nullableDouble", source.NullableDouble, false, true)
.SetGuid("guid", source.Guid, false, true)
.SetGuid("nullableGuid", source.NullableGuid, false, true)
.SetGuid("guid", source.Guid, "D", false, true)
.SetGuid("nullableGuid", source.NullableGuid, "D", false, true)
.SetInt("int", source.Int, false, true)
.SetInt("nullableInt", source.NullableInt, false, true)
.SetLong("long", source.Long, false, true)
.SetLong("nullableLong", source.NullableLong, false, true)
.SetString("string", source.String, false, true)
.SetString("nullableString", source.NullableString, false, true)
.SetTimeSpan("timeSpan", source.TimeSpan, format: "c", false, true)
.SetTimeSpan("nullableTimeSpan", source.NullableTimeSpan, format: "c", false, true)
.SetEnum<global::MyNamespace.OrderStatus>("enum", source.Enum, format: "G", false, true)
.SetEnum<global::MyNamespace.OrderStatus>("nullableEnum", source.NullableEnum, format: "G", false, true);
.SetTimeSpan("timeSpan", source.TimeSpan, "c", false, true)
.SetTimeSpan("nullableTimeSpan", source.NullableTimeSpan, "c", false, true)
.SetEnum<global::MyNamespace.OrderStatus>("enum", source.Enum, "G", false, true)
.SetEnum<global::MyNamespace.OrderStatus>("nullableEnum", source.NullableEnum, "G", false, true);

[global::System.CodeDom.Compiler.GeneratedCode("DynamoMapper", "REPLACED")]
public static partial global::MyNamespace.ExampleEntity FromItem(global::System.Collections.Generic.Dictionary<string, global::Amazon.DynamoDBv2.Model.AttributeValue> x)
Expand All @@ -59,8 +59,8 @@ public static partial class ExampleEntityMapper
NullableDecimal = x.GetNullableDecimal("nullableDecimal", Requiredness.InferFromNullability),
Double = x.GetDouble("double", Requiredness.InferFromNullability),
NullableDouble = x.GetNullableDouble("nullableDouble", Requiredness.InferFromNullability),
Guid = x.GetGuid("guid", Requiredness.InferFromNullability),
NullableGuid = x.GetNullableGuid("nullableGuid", Requiredness.InferFromNullability),
Guid = x.GetGuid("guid", format: "D", Requiredness.InferFromNullability),
NullableGuid = x.GetNullableGuid("nullableGuid", format: "D", Requiredness.InferFromNullability),
Int = x.GetInt("int", Requiredness.InferFromNullability),
NullableInt = x.GetNullableInt("nullableInt", Requiredness.InferFromNullability),
Long = x.GetLong("long", Requiredness.InferFromNullability),
Expand Down
Loading
Loading