Skip to content

Commit c5b1b60

Browse files
authored
Add the @PolymorphicTransactionConstraintEntry macro to simplify creating enumerations conforming to the PolymorphicTransactionConstraintEntry protocol. (#40)
1 parent de7ebcd commit c5b1b60

File tree

7 files changed

+264
-170
lines changed

7 files changed

+264
-170
lines changed

README.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ Or alternatively executed within a DynamoDB transaction-
364364
try await table.transactWrite(entryList)
365365
```
366366

367-
and similarly for polymorphic queries by using the `@PolymorphicWriteEntry` macro-
367+
and similarly for polymorphic queries, most conveniently by using the `@PolymorphicWriteEntry` macro-
368368

369369
```swift
370370
typealias TestTypeBWriteEntry = StandardWriteEntry<TestTypeB>
@@ -400,24 +400,16 @@ let constraintList: [StandardTransactionConstraintEntry<TestTypeA>] = [
400400
try await table.transactWrite(entryList, constraints: constraintList)
401401
```
402402

403-
and similarly for polymorphic queries-
403+
and similarly for polymorphic queries, most conveniently by using the `@PolymorphicTransactionConstraintEntry` macro-
404404

405405
```swift
406406
typealias TestTypeAStandardTransactionConstraintEntry = StandardTransactionConstraintEntry<TestTypeA>
407407
typealias TestTypeBStandardTransactionConstraintEntry = StandardTransactionConstraintEntry<TestTypeB>
408408

409-
enum TestPolymorphicTransactionConstraintEntry: PolymorphicTransactionConstraintEntry {
409+
@PolymorphicTransactionConstraintEntry
410+
enum TestPolymorphicTransactionConstraintEntry: Sendable {
410411
case testTypeA(TestTypeAStandardTransactionConstraintEntry)
411412
case testTypeB(TestTypeBStandardTransactionConstraintEntry)
412-
413-
func handle<Context: PolymorphicWriteEntryContext>(context: Context) throws -> Context.WriteTransactionConstraintType {
414-
switch self {
415-
case .testTypeA(let writeEntry):
416-
return try context.transform(writeEntry)
417-
case .testTypeB(let writeEntry):
418-
return try context.transform(writeEntry)
419-
}
420-
}
421413
}
422414

423415
let constraintList: [TestPolymorphicTransactionConstraintEntry] = [
@@ -430,8 +422,9 @@ try await table.polymorphicTransactWrite(entryList, constraints: constraintList)
430422

431423
Both the `PolymorphicWriteEntry` and `PolymorphicTransactionConstraintEntry` conforming types can
432424
optionally provide a `compositePrimaryKey` property that will allow the API to return more information
433-
about failed transactions. This is enabled by default when using the `@PolymorphicWriteEntry` macro but
434-
can be disabled by setting the `passCompositePrimaryKey` argument.
425+
about failed transactions. This is enabled by default when using the `@PolymorphicWriteEntry` and
426+
`@PolymorphicTransactionConstraintEntry` macros but can be disabled by setting the
427+
`passCompositePrimaryKey` argument.
435428

436429
```swift
437430
@PolymorphicWriteEntry(passCompositePrimaryKey: false)

Sources/DynamoDBTables/Macros.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ public macro PolymorphicWriteEntry(passCompositePrimaryKey: Bool = true) =
1919
#externalMacro(
2020
module: "DynamoDBTablesMacros",
2121
type: "PolymorphicWriteEntryMacro")
22+
23+
@attached(extension, conformances: PolymorphicTransactionConstraintEntry, names: named(handle(context:)), named(compositePrimaryKey))
24+
public macro PolymorphicTransactionConstraintEntry(passCompositePrimaryKey: Bool = true) =
25+
#externalMacro(
26+
module: "DynamoDBTablesMacros",
27+
type: "PolymorphicTransactionConstraintEntryMacro")
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the DynamoDBTables open source project
4+
//
5+
// See LICENSE.txt for license information
6+
// See CONTRIBUTORS.txt for the list of DynamoDBTables authors
7+
//
8+
// SPDX-License-Identifier: Apache-2.0
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
//
13+
// BaseEntryMacro.swift
14+
// DynamoDBTablesMacros
15+
//
16+
17+
import SwiftDiagnostics
18+
import SwiftSyntax
19+
import SwiftSyntaxMacros
20+
21+
protocol MacroAttributes {
22+
static var macroName: String { get }
23+
24+
static var protocolName: String { get }
25+
26+
static var transformType: String { get }
27+
28+
static var contextType: String { get }
29+
}
30+
31+
enum BaseEntryDiagnostic<Attributes: MacroAttributes>: String, DiagnosticMessage {
32+
case notAttachedToEnumDeclaration
33+
case enumMustHaveSendableConformance
34+
case enumMustNotHaveZeroCases
35+
case enumCasesMustHaveASingleParameter
36+
37+
var diagnosticID: MessageID {
38+
MessageID(domain: "\(Attributes.macroName)Macro", id: rawValue)
39+
}
40+
41+
var severity: DiagnosticSeverity { .error }
42+
43+
static var obj: String { "" }
44+
45+
var message: String {
46+
switch self {
47+
case .notAttachedToEnumDeclaration:
48+
return "@\(Attributes.macroName) must be attached to an enum declaration."
49+
case .enumMustHaveSendableConformance:
50+
return "@\(Attributes.macroName) decorated enum must conform to Sendable."
51+
case .enumMustNotHaveZeroCases:
52+
return "@\(Attributes.macroName) decorated enum must be have at least a singe case."
53+
case .enumCasesMustHaveASingleParameter:
54+
return "@\(Attributes.macroName) decorated enum can only have case entries with a single parameter."
55+
}
56+
}
57+
}
58+
59+
enum BaseEntryMacro<Attributes: MacroAttributes>: ExtensionMacro {
60+
private static func getCases(caseMembers: [EnumCaseDeclSyntax], context: some MacroExpansionContext, passCompositePrimaryKey: Bool)
61+
-> (hasDiagnostics: Bool, handleCases: SwitchCaseListSyntax, compositePrimaryKeyCases: SwitchCaseListSyntax)
62+
{
63+
var handleCases: SwitchCaseListSyntax = []
64+
var compositePrimaryKeyCases: SwitchCaseListSyntax = []
65+
var hasDiagnostics = false
66+
for caseMember in caseMembers {
67+
for element in caseMember.elements {
68+
// ensure that the enum case only has one parameter
69+
guard let parameterClause = element.parameterClause, parameterClause.parameters.count == 1 else {
70+
context.diagnose(.init(node: element, message: BaseEntryDiagnostic<Attributes>.enumCasesMustHaveASingleParameter))
71+
hasDiagnostics = true
72+
// do nothing for this case
73+
continue
74+
}
75+
76+
// TODO: when made possible by the language, check that the type of the parameter conforms to `WriteEntry` or `TransactionConstraintEntry`
77+
// https://github.com/swift-server-community/dynamo-db-tables/issues/38
78+
79+
let handleCaseSyntax = SwitchCaseListSyntax.Element(
80+
"""
81+
case let .\(element.name)(writeEntry):
82+
return try context.transform(writeEntry)
83+
""")
84+
85+
handleCases.append(handleCaseSyntax)
86+
87+
if passCompositePrimaryKey {
88+
let compositePrimaryKeyCaseSyntax = SwitchCaseListSyntax.Element(
89+
"""
90+
case let .\(element.name)(writeEntry):
91+
return writeEntry.compositePrimaryKey
92+
""")
93+
94+
compositePrimaryKeyCases.append(compositePrimaryKeyCaseSyntax)
95+
}
96+
}
97+
}
98+
99+
return (hasDiagnostics, handleCases, compositePrimaryKeyCases)
100+
}
101+
102+
static func expansion(
103+
of node: AttributeSyntax,
104+
attachedTo declaration: some DeclGroupSyntax,
105+
providingExtensionsOf type: some TypeSyntaxProtocol,
106+
conformingTo protocols: [TypeSyntax],
107+
in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax]
108+
{
109+
let passCompositePrimaryKey: Bool
110+
if let arguments = node.arguments, case let .argumentList(argumentList) = arguments, let firstArgument = argumentList.first, argumentList.count == 1,
111+
firstArgument.label?.text == "passCompositePrimaryKey", let expression = firstArgument.expression.as(BooleanLiteralExprSyntax.self),
112+
case let .keyword(keyword) = expression.literal.tokenKind, keyword == SwiftSyntax.Keyword.false
113+
{
114+
passCompositePrimaryKey = false
115+
} else {
116+
passCompositePrimaryKey = true
117+
}
118+
119+
// make sure this is attached to an enum
120+
guard let enumDeclaration = declaration as? EnumDeclSyntax else {
121+
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.notAttachedToEnumDeclaration))
122+
123+
return []
124+
}
125+
126+
let requiresProtocolConformance = protocols.reduce(false) { partialResult, protocolSyntax in
127+
if let identifierTypeSyntax = protocolSyntax.as(IdentifierTypeSyntax.self), identifierTypeSyntax.name.text == Attributes.protocolName {
128+
return true
129+
}
130+
131+
return partialResult
132+
}
133+
134+
// make sure the type is conforming to Sendable
135+
let hasSendableConformance = declaration.inheritanceClause?.inheritedTypes.reduce(false) { partialResult, inheritedType in
136+
if let identifierTypeSyntax = inheritedType.type.as(IdentifierTypeSyntax.self), identifierTypeSyntax.name.text == "Sendable" {
137+
return true
138+
}
139+
140+
return partialResult
141+
} ?? false
142+
143+
guard hasSendableConformance else {
144+
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.enumMustHaveSendableConformance))
145+
146+
return []
147+
}
148+
149+
let memberBlock = enumDeclaration.memberBlock.members
150+
151+
let caseMembers: [EnumCaseDeclSyntax] = memberBlock.compactMap { member in
152+
if let caseMember = member.decl.as(EnumCaseDeclSyntax.self) {
153+
return caseMember
154+
}
155+
156+
return nil
157+
}
158+
159+
// make sure this is attached to an enum
160+
guard !caseMembers.isEmpty else {
161+
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.enumMustNotHaveZeroCases))
162+
163+
return []
164+
}
165+
166+
let (hasDiagnostics, handleCases, compositePrimaryKeyCases) = self.getCases(caseMembers: caseMembers, context: context,
167+
passCompositePrimaryKey: passCompositePrimaryKey)
168+
169+
if hasDiagnostics {
170+
return []
171+
}
172+
173+
let type = TypeSyntax(extendedGraphemeClusterLiteral: requiresProtocolConformance ? "\(type.trimmed): \(Attributes.protocolName) "
174+
: "\(type.trimmed) ")
175+
let extensionDecl = try ExtensionDeclSyntax(
176+
extendedType: type,
177+
memberBlockBuilder: {
178+
try FunctionDeclSyntax(
179+
"func handle<Context: \(raw: Attributes.contextType)>(context: Context) throws -> Context.\(raw: Attributes.transformType)")
180+
{
181+
SwitchExprSyntax(subject: ExprSyntax(stringLiteral: "self"), cases: handleCases)
182+
}
183+
184+
if passCompositePrimaryKey {
185+
try VariableDeclSyntax("var compositePrimaryKey: StandardCompositePrimaryKey?") {
186+
SwitchExprSyntax(subject: ExprSyntax(stringLiteral: "self"), cases: compositePrimaryKeyCases)
187+
}
188+
}
189+
})
190+
191+
return [extensionDecl]
192+
}
193+
}

Sources/DynamoDBTablesMacros/Plugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
struct DynamoDBTablesMacrosCompilerPlugin: CompilerPlugin {
2323
let providingMacros: [Macro.Type] = [
2424
PolymorphicWriteEntryMacro.self,
25+
PolymorphicTransactionConstraintEntryMacro.self,
2526
]
2627
}
2728
#endif
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the DynamoDBTables open source project
4+
//
5+
// See LICENSE.txt for license information
6+
// See CONTRIBUTORS.txt for the list of DynamoDBTables authors
7+
//
8+
// SPDX-License-Identifier: Apache-2.0
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
//
13+
// PolymorphicTransactionConstraintEntryMacro.swift
14+
// DynamoDBTablesMacros
15+
//
16+
17+
import SwiftDiagnostics
18+
import SwiftSyntax
19+
import SwiftSyntaxMacros
20+
21+
struct PolymorphicTransactionConstraintEntryMacroAttributes: MacroAttributes {
22+
static var macroName: String = "PolymorphicTransactionConstraintEntry"
23+
24+
static var protocolName: String = "PolymorphicTransactionConstraintEntry"
25+
26+
static var transformType: String = "WriteTransactionConstraintType"
27+
28+
static var contextType: String = "PolymorphicWriteEntryContext"
29+
}
30+
31+
public enum PolymorphicTransactionConstraintEntryMacro: ExtensionMacro {
32+
public static func expansion(
33+
of node: AttributeSyntax,
34+
attachedTo declaration: some DeclGroupSyntax,
35+
providingExtensionsOf type: some TypeSyntaxProtocol,
36+
conformingTo protocols: [TypeSyntax],
37+
in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax]
38+
{
39+
try BaseEntryMacro<PolymorphicTransactionConstraintEntryMacroAttributes>.expansion(of: node,
40+
attachedTo: declaration,
41+
providingExtensionsOf: type,
42+
conformingTo: protocols,
43+
in: context)
44+
}
45+
}

0 commit comments

Comments
 (0)