Skip to content

Commit

Permalink
Add the @PolymorphicTransactionConstraintEntry macro to simplify crea…
Browse files Browse the repository at this point in the history
…ting enumerations conforming to the PolymorphicTransactionConstraintEntry protocol. (#40)
  • Loading branch information
tachyonics authored Sep 15, 2024
1 parent de7ebcd commit c5b1b60
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 170 deletions.
21 changes: 7 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ Or alternatively executed within a DynamoDB transaction-
try await table.transactWrite(entryList)
```

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

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

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

```swift
typealias TestTypeAStandardTransactionConstraintEntry = StandardTransactionConstraintEntry<TestTypeA>
typealias TestTypeBStandardTransactionConstraintEntry = StandardTransactionConstraintEntry<TestTypeB>

enum TestPolymorphicTransactionConstraintEntry: PolymorphicTransactionConstraintEntry {
@PolymorphicTransactionConstraintEntry
enum TestPolymorphicTransactionConstraintEntry: Sendable {
case testTypeA(TestTypeAStandardTransactionConstraintEntry)
case testTypeB(TestTypeBStandardTransactionConstraintEntry)

func handle<Context: PolymorphicWriteEntryContext>(context: Context) throws -> Context.WriteTransactionConstraintType {
switch self {
case .testTypeA(let writeEntry):
return try context.transform(writeEntry)
case .testTypeB(let writeEntry):
return try context.transform(writeEntry)
}
}
}

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

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

```swift
@PolymorphicWriteEntry(passCompositePrimaryKey: false)
Expand Down
6 changes: 6 additions & 0 deletions Sources/DynamoDBTables/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ public macro PolymorphicWriteEntry(passCompositePrimaryKey: Bool = true) =
#externalMacro(
module: "DynamoDBTablesMacros",
type: "PolymorphicWriteEntryMacro")

@attached(extension, conformances: PolymorphicTransactionConstraintEntry, names: named(handle(context:)), named(compositePrimaryKey))
public macro PolymorphicTransactionConstraintEntry(passCompositePrimaryKey: Bool = true) =
#externalMacro(
module: "DynamoDBTablesMacros",
type: "PolymorphicTransactionConstraintEntryMacro")
193 changes: 193 additions & 0 deletions Sources/DynamoDBTablesMacros/BaseEntryMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the DynamoDBTables open source project
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of DynamoDBTables authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

//
// BaseEntryMacro.swift
// DynamoDBTablesMacros
//

import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros

protocol MacroAttributes {
static var macroName: String { get }

static var protocolName: String { get }

static var transformType: String { get }

static var contextType: String { get }
}

enum BaseEntryDiagnostic<Attributes: MacroAttributes>: String, DiagnosticMessage {
case notAttachedToEnumDeclaration
case enumMustHaveSendableConformance
case enumMustNotHaveZeroCases
case enumCasesMustHaveASingleParameter

var diagnosticID: MessageID {
MessageID(domain: "\(Attributes.macroName)Macro", id: rawValue)
}

var severity: DiagnosticSeverity { .error }

static var obj: String { "" }

var message: String {
switch self {
case .notAttachedToEnumDeclaration:
return "@\(Attributes.macroName) must be attached to an enum declaration."
case .enumMustHaveSendableConformance:
return "@\(Attributes.macroName) decorated enum must conform to Sendable."
case .enumMustNotHaveZeroCases:
return "@\(Attributes.macroName) decorated enum must be have at least a singe case."
case .enumCasesMustHaveASingleParameter:
return "@\(Attributes.macroName) decorated enum can only have case entries with a single parameter."
}
}
}

enum BaseEntryMacro<Attributes: MacroAttributes>: ExtensionMacro {
private static func getCases(caseMembers: [EnumCaseDeclSyntax], context: some MacroExpansionContext, passCompositePrimaryKey: Bool)
-> (hasDiagnostics: Bool, handleCases: SwitchCaseListSyntax, compositePrimaryKeyCases: SwitchCaseListSyntax)
{
var handleCases: SwitchCaseListSyntax = []
var compositePrimaryKeyCases: SwitchCaseListSyntax = []
var hasDiagnostics = false
for caseMember in caseMembers {
for element in caseMember.elements {
// ensure that the enum case only has one parameter
guard let parameterClause = element.parameterClause, parameterClause.parameters.count == 1 else {
context.diagnose(.init(node: element, message: BaseEntryDiagnostic<Attributes>.enumCasesMustHaveASingleParameter))
hasDiagnostics = true
// do nothing for this case
continue
}

// TODO: when made possible by the language, check that the type of the parameter conforms to `WriteEntry` or `TransactionConstraintEntry`
// https://github.com/swift-server-community/dynamo-db-tables/issues/38

let handleCaseSyntax = SwitchCaseListSyntax.Element(
"""
case let .\(element.name)(writeEntry):
return try context.transform(writeEntry)
""")

handleCases.append(handleCaseSyntax)

if passCompositePrimaryKey {
let compositePrimaryKeyCaseSyntax = SwitchCaseListSyntax.Element(
"""
case let .\(element.name)(writeEntry):
return writeEntry.compositePrimaryKey
""")

compositePrimaryKeyCases.append(compositePrimaryKeyCaseSyntax)
}
}
}

return (hasDiagnostics, handleCases, compositePrimaryKeyCases)
}

static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax]
{
let passCompositePrimaryKey: Bool
if let arguments = node.arguments, case let .argumentList(argumentList) = arguments, let firstArgument = argumentList.first, argumentList.count == 1,
firstArgument.label?.text == "passCompositePrimaryKey", let expression = firstArgument.expression.as(BooleanLiteralExprSyntax.self),
case let .keyword(keyword) = expression.literal.tokenKind, keyword == SwiftSyntax.Keyword.false
{
passCompositePrimaryKey = false
} else {
passCompositePrimaryKey = true
}

// make sure this is attached to an enum
guard let enumDeclaration = declaration as? EnumDeclSyntax else {
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.notAttachedToEnumDeclaration))

return []
}

let requiresProtocolConformance = protocols.reduce(false) { partialResult, protocolSyntax in
if let identifierTypeSyntax = protocolSyntax.as(IdentifierTypeSyntax.self), identifierTypeSyntax.name.text == Attributes.protocolName {
return true
}

return partialResult
}

// make sure the type is conforming to Sendable
let hasSendableConformance = declaration.inheritanceClause?.inheritedTypes.reduce(false) { partialResult, inheritedType in
if let identifierTypeSyntax = inheritedType.type.as(IdentifierTypeSyntax.self), identifierTypeSyntax.name.text == "Sendable" {
return true
}

return partialResult
} ?? false

guard hasSendableConformance else {
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.enumMustHaveSendableConformance))

return []
}

let memberBlock = enumDeclaration.memberBlock.members

let caseMembers: [EnumCaseDeclSyntax] = memberBlock.compactMap { member in
if let caseMember = member.decl.as(EnumCaseDeclSyntax.self) {
return caseMember
}

return nil
}

// make sure this is attached to an enum
guard !caseMembers.isEmpty else {
context.diagnose(.init(node: declaration, message: BaseEntryDiagnostic<Attributes>.enumMustNotHaveZeroCases))

return []
}

let (hasDiagnostics, handleCases, compositePrimaryKeyCases) = self.getCases(caseMembers: caseMembers, context: context,
passCompositePrimaryKey: passCompositePrimaryKey)

if hasDiagnostics {
return []
}

let type = TypeSyntax(extendedGraphemeClusterLiteral: requiresProtocolConformance ? "\(type.trimmed): \(Attributes.protocolName) "
: "\(type.trimmed) ")
let extensionDecl = try ExtensionDeclSyntax(
extendedType: type,
memberBlockBuilder: {
try FunctionDeclSyntax(
"func handle<Context: \(raw: Attributes.contextType)>(context: Context) throws -> Context.\(raw: Attributes.transformType)")
{
SwitchExprSyntax(subject: ExprSyntax(stringLiteral: "self"), cases: handleCases)
}

if passCompositePrimaryKey {
try VariableDeclSyntax("var compositePrimaryKey: StandardCompositePrimaryKey?") {
SwitchExprSyntax(subject: ExprSyntax(stringLiteral: "self"), cases: compositePrimaryKeyCases)
}
}
})

return [extensionDecl]
}
}
1 change: 1 addition & 0 deletions Sources/DynamoDBTablesMacros/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
struct DynamoDBTablesMacrosCompilerPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
PolymorphicWriteEntryMacro.self,
PolymorphicTransactionConstraintEntryMacro.self,
]
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the DynamoDBTables open source project
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of DynamoDBTables authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

//
// PolymorphicTransactionConstraintEntryMacro.swift
// DynamoDBTablesMacros
//

import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacros

struct PolymorphicTransactionConstraintEntryMacroAttributes: MacroAttributes {
static var macroName: String = "PolymorphicTransactionConstraintEntry"

static var protocolName: String = "PolymorphicTransactionConstraintEntry"

static var transformType: String = "WriteTransactionConstraintType"

static var contextType: String = "PolymorphicWriteEntryContext"
}

public enum PolymorphicTransactionConstraintEntryMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax]
{
try BaseEntryMacro<PolymorphicTransactionConstraintEntryMacroAttributes>.expansion(of: node,
attachedTo: declaration,
providingExtensionsOf: type,
conformingTo: protocols,
in: context)
}
}
Loading

0 comments on commit c5b1b60

Please sign in to comment.