Skip to content

Commit 34e9d4e

Browse files
authored
Add macro for PolymorphicOperationReturnType. (#62)
* Add macro for PolymorphicOperationReturnType. * Update documentation. * Re-add missing code.
1 parent f944654 commit 34e9d4e

9 files changed

+192
-70
lines changed

Diff for: README.md

+7-17
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,11 @@ guard let retrievedDatabaseItem2 = batch[key2] else {
208208

209209
In addition to the `query` operation, there is a more complex API that allows retrieval of multiple rows that have different types.
210210

211+
This API is most conveniently used in conjunction with an enum annotated with the `@PolymorphicOperationReturnType` macro-
212+
211213
```swift
212-
enum TestPolymorphicOperationReturnType: PolymorphicOperationReturnType {
213-
typealias AttributesType = StandardPrimaryKeyAttributes
214-
215-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<StandardPrimaryKeyAttributes, Self>)] = [
216-
(TypeA.self, .init( {.typeA($0)} )),
217-
(TypeB.self, .init( {.typeB($0)} )),
218-
]
219-
214+
@PolymorphicOperationReturnType
215+
enum TestPolymorphicOperationReturnType {
220216
case typeA(StandardTypedDatabaseItem<TypeA>)
221217
case typeB(StandardTypedDatabaseItem<TypeB>)
222218
}
@@ -286,17 +282,11 @@ for databaseItem in queryItems {
286282
}
287283
```
288284

289-
and similarly for polymorphic queries-
285+
and similarly for polymorphic queries, most conveniently used in conjunction with an enum annotated with the `@PolymorphicOperationReturnType` macro-
290286

291287
```swift
292-
enum TestPolymorphicOperationReturnType: PolymorphicOperationReturnType {
293-
typealias AttributesType = GSI1PrimaryKeyAttributes
294-
295-
static var types: [(Codable.Type, PolymorphicOperationReturnOption<GSI1PrimaryKeyAttributes, Self>)] = [
296-
(TypeA.self, .init( {.typeA($0)} )),
297-
(TypeB.self, .init( {.typeB($0)} )),
298-
]
299-
288+
@PolymorphicOperationReturnType
289+
enum TestPolymorphicOperationReturnType {
300290
case typeA(StandardTypedDatabaseItem<TypeA>)
301291
case typeB(StandardTypedDatabaseItem<TypeB>)
302292
}

Diff for: Sources/DynamoDBTables/Macros.swift

+6
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,9 @@ public macro PolymorphicTransactionConstraintEntry() =
2525
#externalMacro(
2626
module: "DynamoDBTablesMacros",
2727
type: "PolymorphicTransactionConstraintEntryMacro")
28+
29+
@attached(extension, conformances: PolymorphicOperationReturnType, names: named(AttributesType), named(TimeToLiveAttributesType), named(types))
30+
public macro PolymorphicOperationReturnType(databaseItemType: String = "StandardTypedDatabaseItem") =
31+
#externalMacro(
32+
module: "DynamoDBTablesMacros",
33+
type: "PolymorphicOperationReturnTypeMacro")

Diff for: Sources/DynamoDBTablesMacros/BaseEntryMacro.swift

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ import SwiftDiagnostics
1818
import SwiftSyntax
1919
import SwiftSyntaxMacros
2020

21-
protocol MacroAttributes {
21+
protocol CoreMacroAttributes {
2222
static var macroName: String { get }
2323

2424
static var protocolName: String { get }
25+
}
2526

27+
protocol MacroAttributes: CoreMacroAttributes {
2628
static var transformType: String { get }
2729

2830
static var contextType: String { get }
2931
}
3032

31-
enum BaseEntryDiagnostic<Attributes: MacroAttributes>: String, DiagnosticMessage {
33+
enum BaseEntryDiagnostic<Attributes: CoreMacroAttributes>: String, DiagnosticMessage {
3234
case notAttachedToEnumDeclaration
3335
case enumMustNotHaveZeroCases
3436
case enumCasesMustHaveASingleParameter

Diff for: Sources/DynamoDBTablesMacros/Plugin.swift

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
let providingMacros: [Macro.Type] = [
2424
PolymorphicWriteEntryMacro.self,
2525
PolymorphicTransactionConstraintEntryMacro.self,
26+
PolymorphicOperationReturnTypeMacro.self,
2627
]
2728
}
2829
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
// PolymorphicOperationReturnTypeMacro.swift
14+
// DynamoDBTablesMacros
15+
//
16+
17+
import SwiftDiagnostics
18+
import SwiftSyntax
19+
import SwiftSyntaxMacros
20+
21+
private struct Attributes: CoreMacroAttributes {
22+
static let macroName: String = "PolymorphicOperationReturnType"
23+
24+
static let protocolName: String = "PolymorphicOperationReturnType"
25+
}
26+
27+
private struct BasicDiagnosticMessage: DiagnosticMessage {
28+
let message: String
29+
let diagnosticID: MessageID
30+
let severity: SwiftDiagnostics.DiagnosticSeverity = .error
31+
32+
init(message: String, rawValue: String) {
33+
self.message = message
34+
self.diagnosticID = MessageID(domain: "PolymorphicOperationReturnTypeMacro", id: rawValue)
35+
}
36+
}
37+
38+
public enum PolymorphicOperationReturnTypeMacro: ExtensionMacro {
39+
public static func expansion(
40+
of node: AttributeSyntax,
41+
attachedTo declaration: some DeclGroupSyntax,
42+
providingExtensionsOf type: some TypeSyntaxProtocol,
43+
conformingTo protocols: [TypeSyntax],
44+
in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax]
45+
{
46+
// make sure this is attached to an enum
47+
guard let enumDeclaration = declaration as? EnumDeclSyntax else {
48+
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.notAttachedToEnumDeclaration))
49+
50+
return []
51+
}
52+
53+
let databaseItemType: String
54+
let standardDatabaseType = "StandardTypedDatabaseItem"
55+
if let arguments = node.arguments, case let .argumentList(argumentList) = arguments, let firstArgument = argumentList.first, argumentList.count == 1,
56+
firstArgument.label?.text == "databaseItemType", let expression = firstArgument.expression.as(StringLiteralExprSyntax.self)
57+
{
58+
databaseItemType = expression.representedLiteralValue ?? standardDatabaseType
59+
} else {
60+
databaseItemType = standardDatabaseType
61+
}
62+
63+
let requiresProtocolConformance = protocols.reduce(false) { partialResult, protocolSyntax in
64+
if let identifierTypeSyntax = protocolSyntax.as(IdentifierTypeSyntax.self), identifierTypeSyntax.name.text == Attributes.protocolName {
65+
return true
66+
}
67+
68+
return partialResult
69+
}
70+
71+
let memberBlock = enumDeclaration.memberBlock.members
72+
73+
let caseMembers: [EnumCaseDeclSyntax] = memberBlock.compactMap { member in
74+
if let caseMember = member.decl.as(EnumCaseDeclSyntax.self) {
75+
return caseMember
76+
}
77+
78+
return nil
79+
}
80+
81+
// make sure this is attached to an enum
82+
guard !caseMembers.isEmpty else {
83+
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.enumMustNotHaveZeroCases))
84+
85+
return []
86+
}
87+
88+
let (hasDiagnostics, handleCases) = self.getCases(caseMembers: caseMembers, context: context, databaseItemType: databaseItemType)
89+
90+
if hasDiagnostics {
91+
return []
92+
}
93+
94+
let type = TypeSyntax(extendedGraphemeClusterLiteral: requiresProtocolConformance ? "\(type.trimmed): \(Attributes.protocolName) "
95+
: "\(type.trimmed) ")
96+
let extensionDecl = try ExtensionDeclSyntax(
97+
extendedType: type,
98+
memberBlockBuilder: {
99+
try TypeAliasDeclSyntax("typealias AttributesType = StandardPrimaryKeyAttributes")
100+
try TypeAliasDeclSyntax("typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes")
101+
102+
let casesArray = ArrayExprSyntax(
103+
leftSquare: .leftSquareToken(),
104+
elements: handleCases,
105+
rightSquare: .rightSquareToken())
106+
107+
try VariableDeclSyntax(
108+
"""
109+
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] =
110+
\(casesArray)
111+
""")
112+
})
113+
114+
return [extensionDecl]
115+
}
116+
}
117+
118+
extension PolymorphicOperationReturnTypeMacro {
119+
private static func getCases(caseMembers: [EnumCaseDeclSyntax], context: some MacroExpansionContext, databaseItemType: String)
120+
-> (hasDiagnostics: Bool, handleCases: ArrayElementListSyntax)
121+
{
122+
var handleCases: ArrayElementListSyntax = []
123+
var hasDiagnostics = false
124+
for caseMember in caseMembers {
125+
for element in caseMember.elements {
126+
// ensure that the enum case only has one parameter
127+
guard let parameters = element.parameterClause?.parameters, let parameterType = parameters.first?.type.as(IdentifierTypeSyntax.self),
128+
parameters.count == 1
129+
else {
130+
context.diagnose(.init(node: element, message: BaseEntryDiagnostic<Attributes>.enumCasesMustHaveASingleParameter))
131+
hasDiagnostics = true
132+
// do nothing for this case
133+
continue
134+
}
135+
136+
guard parameterType.name.text == databaseItemType,
137+
let firstArgumentType = parameterType.genericArgumentClause?.arguments.first?.argument
138+
else {
139+
let message = "PolymorphicOperationReturnTypeMacro decorated enum cases parameter must be of \(databaseItemType) type."
140+
context.diagnose(.init(node: element, message: BasicDiagnosticMessage(message: message, rawValue: "enumCasesMustBeOfTheExpectedType")))
141+
hasDiagnostics = true
142+
// do nothing for this case
143+
continue
144+
}
145+
146+
let handleCaseSyntax = ArrayElementSyntax(
147+
expression: ExprSyntax("""
148+
(
149+
\(firstArgumentType).self, .init { .\(element.name)($0) }
150+
)
151+
"""),
152+
trailingComma: .commaToken())
153+
154+
handleCases.append(handleCaseSyntax)
155+
}
156+
}
157+
158+
return (hasDiagnostics, handleCases)
159+
}
160+
}

Diff for: Tests/DynamoDBTablesTests/InMemoryDynamoDBCompositePrimaryKeyTableTests.swift

+2-8
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,8 @@ import AWSDynamoDB
2828
@testable import DynamoDBTables
2929
import Testing
3030

31-
enum TestPolymorphicOperationReturnType: PolymorphicOperationReturnType {
32-
typealias AttributesType = StandardPrimaryKeyAttributes
33-
typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes
34-
35-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] = [
36-
(TestTypeA.self, .init { .testTypeA($0) }),
37-
]
38-
31+
@PolymorphicOperationReturnType
32+
enum TestPolymorphicOperationReturnType {
3933
case testTypeA(StandardTypedDatabaseItem<TestTypeA>)
4034
}
4135

Diff for: Tests/DynamoDBTablesTests/SimulateConcurrencyDynamoDBCompositePrimaryKeyTableTests.swift

+4-8
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,11 @@ import Testing
3030

3131
private typealias DatabaseRowType = StandardTypedDatabaseItem<TestTypeA>
3232

33-
enum ExpectedQueryableTypes: PolymorphicOperationReturnType {
34-
typealias AttributesType = StandardPrimaryKeyAttributes
35-
typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes
33+
typealias CustomTypedDatabaseItem = StandardTypedDatabaseItem
3634

37-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] = [
38-
(TestTypeA.self, .init { .testTypeA($0) }),
39-
]
40-
41-
case testTypeA(StandardTypedDatabaseItem<TestTypeA>)
35+
@PolymorphicOperationReturnType(databaseItemType: "CustomTypedDatabaseItem")
36+
enum ExpectedQueryableTypes {
37+
case testTypeA(CustomTypedDatabaseItem<TestTypeA>)
4238
}
4339

4440
struct SimulateConcurrencyDynamoDBCompositePrimaryKeyTableTests {

Diff for: Tests/DynamoDBTablesTests/SmokeDynamoDBTestInput.swift

+6-26
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,14 @@
2626
@testable import DynamoDBTables
2727
import Foundation
2828

29-
enum AllQueryableTypes: PolymorphicOperationReturnType {
30-
typealias AttributesType = StandardPrimaryKeyAttributes
31-
typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes
32-
33-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] = [
34-
(TypeA.self, .init { .typeA($0) }),
35-
(TypeB.self, .init { .typeB($0) }),
36-
]
37-
29+
@PolymorphicOperationReturnType
30+
enum AllQueryableTypes {
3831
case typeA(StandardTypedDatabaseItem<TypeA>)
3932
case typeB(StandardTypedDatabaseItem<TypeB>)
4033
}
4134

42-
enum SomeQueryableTypes: PolymorphicOperationReturnType {
43-
typealias AttributesType = StandardPrimaryKeyAttributes
44-
typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes
45-
46-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] = [
47-
(TypeA.self, .init { .typeA($0) }),
48-
]
49-
35+
@PolymorphicOperationReturnType
36+
enum SomeQueryableTypes {
5037
case typeA(StandardTypedDatabaseItem<TypeA>)
5138
}
5239

@@ -55,15 +42,8 @@ struct GSI1PKIndexIdentity: IndexIdentity {
5542
static let identity = "GSI1PK"
5643
}
5744

58-
enum AllQueryableTypesWithIndex: PolymorphicOperationReturnType {
59-
typealias AttributesType = StandardPrimaryKeyAttributes
60-
typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes
61-
62-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] = [
63-
(RowWithIndex<TypeA, GSI1PKIndexIdentity>.self, .init { .typeAWithIndex($0) }),
64-
(TypeB.self, .init { .typeB($0) }),
65-
]
66-
45+
@PolymorphicOperationReturnType
46+
enum AllQueryableTypesWithIndex {
6747
case typeAWithIndex(StandardTypedDatabaseItem<RowWithIndex<TypeA, GSI1PKIndexIdentity>>)
6848
case typeB(StandardTypedDatabaseItem<TestTypeB>)
6949
}

Diff for: Tests/DynamoDBTablesTests/TestConfiguration.swift

+2-9
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,8 @@ struct TestTypeC: Codable {
5353
}
5454
}
5555

56-
enum TestQueryableTypes: PolymorphicOperationReturnType {
57-
typealias AttributesType = StandardPrimaryKeyAttributes
58-
typealias TimeToLiveAttributesType = StandardTimeToLiveAttributes
59-
60-
static let types: [(Codable.Type, PolymorphicOperationReturnOption<AttributesType, Self, TimeToLiveAttributesType>)] = [
61-
(TestTypeA.self, .init { .testTypeA($0) }),
62-
(TestTypeB.self, .init { .testTypeB($0) }),
63-
]
64-
56+
@PolymorphicOperationReturnType
57+
enum TestQueryableTypes {
6558
case testTypeA(StandardTypedDatabaseItem<TestTypeA>)
6659
case testTypeB(StandardTypedDatabaseItem<TestTypeB>)
6760
}

0 commit comments

Comments
 (0)