Skip to content

Commit

Permalink
Merge pull request #5 from mentalflux/on-change-attachments
Browse files Browse the repository at this point in the history
Adds support for changing the attachment location of a @ComposeBodyOnChange function to the BindingReducer or a child Scope Reducer.
  • Loading branch information
scogeo authored Feb 25, 2024
2 parents a9d0ea4 + a465ea8 commit b732659
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 26 deletions.
19 changes: 18 additions & 1 deletion Sources/TCAComposer/Macros/ComposeMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,25 @@ public macro ComposeBodyActionAlertCase(_ name: String = "") =
module: "TCAComposerMacros", type: "ComposeDirectiveMacro"
)

/// Specified the location in the `body` to attach the `.onChange()` modifier.
public enum ComposeBodyOnChangeAttachment {
// Attaches the `.onChange()` modifier to the `BindingReducer`
case binding

// Attaches the `.onChange()` modifier to the reducer core.
case core

// Attaches the `.onChange()` modifier to the `Scope` reducer for the specified child.
case scope(String)
}

/// Adds an `onChange(of: ...)` modifier to the `body` of the Reducer.
/// - Parameters:
/// - of: A `KeyPath` of `State` to use in calling the `.onChange()` modifier
/// - attachment: Specified which Reducer in the `body` to attach the `.onChange()` to. By default it will be attached to the core.
///
@attached(peer)
public macro ComposeBodyOnChange<State, Value>(of keyPath: KeyPath<State, Value>) =
public macro ComposeBodyOnChange<State, Value>(of keyPath: KeyPath<State, Value>, attachment: ComposeBodyOnChangeAttachment = .core) =
#externalMacro(
module: "TCAComposerMacros", type: "ComposeDirectiveMacro"
)
Expand Down
20 changes: 13 additions & 7 deletions Sources/TCAComposerMacros/Composition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SwiftSyntaxBuilder
import SwiftSyntaxMacroExpansion
import SwiftSyntaxMacros
import XCTestDynamicOverlay
import OrderedCollections

class Composition {
var options = Set<Option>()
Expand All @@ -28,10 +29,11 @@ class Composition {
var bodyCoreMembers = [BodyMember]()
var bodyCoreModifiers = [BodyMember]()
var bodyAfterCoreMembers = [BodyMember]()
var bindingReducer: BodyMember?

// Reducers that need to be be converted to BodyMembers
// after @ComposeBodyReducerChild macros have been processed
var childReducers = [ScopedChildReducer]()
var childReducers = OrderedDictionary<String, ScopedChildReducer>()
var childBodyReducers = [ScopedChildReducer]()

// Preserve a reference to the source of the child delcartaion for diagnostics.
Expand Down Expand Up @@ -276,6 +278,10 @@ class Composition {
}
}

if isBindable {
bindingReducer = .init(name: Identifiers.BindingReducer)
}

if let initialStateCaseExpr {
let caseName = initialStateCaseExpr.segments.trimmedDescription
guard let stateMember = stateMembers.first(where: { $0.name == caseName }) else {
Expand Down Expand Up @@ -320,15 +326,15 @@ class Composition {

// Add scopes if not enum
if !isStateEnum || bodyCoreMembers.isEmpty {
bodyBeforeCoreMembers.insert(contentsOf: childReducers.map { $0.reducerBuilderMember }, at: 0)
bodyBeforeCoreMembers.insert(contentsOf: childReducers.values.map { $0.reducerBuilderMember }, at: 0)
}
else {
bodyCoreModifiers.insert(contentsOf: childReducers.map { $0.coreBodyModifier }, at: 0)
bodyCoreModifiers.insert(contentsOf: childReducers.values.map { $0.coreBodyModifier }, at: 0)
}

if isBindable {
if let bindingReducer {
actionMembers.append(.init(name: "binding", type: "BindingAction<State>"))
bodyBeforeCoreMembers.insert(.init(name: Identifiers.BindingReducer), at: 0)
bodyBeforeCoreMembers.insert(bindingReducer, at: 0)
// TODO: handle conformance and attributes for Action here
}

Expand Down Expand Up @@ -702,10 +708,10 @@ class Composition {
self?.reducer(for: name)
})
} else {
childReducers.append(
childReducers[name] =
ScopedChildReducer(name: name) { [weak self] in
self?.reducer(for: name)
})
}
}

case "state":
Expand Down
64 changes: 61 additions & 3 deletions Sources/TCAComposerMacros/ReducerAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ final class ReducerAnalyzer: SyntaxVisitor {
}

var stateKeyPath = ""

var attachment: ComposeBodyOnChangeAttachment?

for argument in argumentList {
switch argument.label?.text {
case "of":
Expand All @@ -419,9 +420,46 @@ final class ReducerAnalyzer: SyntaxVisitor {
)
)
)
continue
return
}
stateKeyPath = "\\\(keyPath.dropFirst(6))"
case "attachment":
attachment = .init(argument.expression.trimmedDescription)
switch attachment {
case .binding:
guard composition.bindingReducer != nil else {
composition.context.diagnose(
Diagnostic(
node: argument.expression,
message: MacroExpansionErrorMessage(
"""
`.binding` attachment requires the Reducer have the `.bindable` option specified.
"""
)
)
)
return
}

case let .scope(name):
guard composition.childReducers[name] != nil else {
composition.context.diagnose(
Diagnostic(
node: argument.expression,
message: MacroExpansionErrorMessage(
"""
'\(name)' is not a valid scoped child reducer name.
"""
)
)
)
return
}

default:
break
}

default:
XCTFail(
"""
Expand Down Expand Up @@ -458,7 +496,27 @@ final class ReducerAnalyzer: SyntaxVisitor {
closure: closure
)

composition.bodyCoreModifiers.append(onChange)
switch attachment {
case .binding:
guard var bindingReducer = composition.bindingReducer else {
XCTFail("Binding reducer unexpectedly not found")
return
}
bindingReducer.modifiers.append(onChange)
composition.bindingReducer = bindingReducer

case let .scope(childName):
guard var childReducer = composition.childReducers[childName] else {
XCTFail("Child reducer unexpectedly not found")
return
}
childReducer.modifiers.append(onChange)
composition.childReducers[childName] = childReducer

case nil,
.core:
composition.bodyCoreModifiers.append(onChange)
}
}

func processActionCase(_ node: EnumDeclSyntax, attribute: AttributeSyntax) {
Expand Down
43 changes: 38 additions & 5 deletions Sources/TCAComposerMacros/SharedTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,22 @@ struct BodyMember {
let name: TokenSyntax
@LabeledExprListBuilder let argumentList: () -> LabeledExprListSyntax
let closure: ClosureExprSyntax?

var modifiers: [BodyMember]

var reducerBuilder: FunctionCallExprSyntax {
FunctionCallExprSyntax(
var builder = FunctionCallExprSyntax(
callee: DeclReferenceExprSyntax(
baseName: name
),
trailingClosure: closure,
argumentList: argumentList
)

for modifier in modifiers {
builder = modifier.modify(wrap: builder)
}

return builder
}

enum Position: String {
Expand All @@ -62,11 +69,13 @@ struct BodyMember {
init(
name: TokenSyntax,
@LabeledExprListBuilder argumentList: @escaping () -> LabeledExprListSyntax = { [] },
closure: ClosureExprSyntax? = nil
closure: ClosureExprSyntax? = nil,
modifiers: [BodyMember] = []
) {
self.name = name
self.argumentList = argumentList
self.closure = closure
self.modifiers = modifiers
}

func modify(wrap: FunctionCallExprSyntax) -> FunctionCallExprSyntax {
Expand All @@ -86,12 +95,35 @@ struct BodyMember {
}
}

enum ComposeBodyOnChangeAttachment {
case binding
case core
case scope(String)

init?(_ value: String) {
switch value {
case ".binding":
self = .binding

case ".core":
self = .core

case let scope where scope.hasPrefix(".scope(\""):
self = .scope(String(scope.dropFirst(8).dropLast(2)))

default:
return nil
}
}
}

struct ScopedChildReducer {
let name: String
let keyPaths: ScopeKeyPaths
let reducer: (() -> FunctionCallExprSyntax?)?
let functionName: TokenSyntax

var modifiers = [BodyMember]()

init(
name: String,
functionName: TokenSyntax = Identifiers.Scope,
Expand Down Expand Up @@ -139,7 +171,8 @@ struct ScopedChildReducer {
}
LabeledExprSyntax(label: "action", expression: keyPaths.actionSyntax)
},
closure: closure)
closure: closure,
modifiers: modifiers)
}
}

Expand Down
20 changes: 10 additions & 10 deletions TCAComposer.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "e072139e13f2f3e582251b49835abcf3421ac69a",
"version" : "1.2.3"
"revision" : "551150d5e60e3be78972607d89cd69069cca3e7c",
"version" : "1.2.4"
}
},
{
Expand All @@ -32,17 +32,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections",
"state" : {
"revision" : "d029d9d39c87bed85b1c50adee7c41795261a192",
"version" : "1.0.6"
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
"version" : "1.1.0"
}
},
{
"identity" : "swift-composable-architecture",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-composable-architecture",
"state" : {
"revision" : "96ce68db884033c4b8ae251cdf11bb47a3e5250f",
"version" : "1.7.2"
"revision" : "856f9b8d82f6851b7f61ec4c5ce9e4c18ebbdb45",
"version" : "1.8.2"
}
},
{
Expand All @@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump",
"state" : {
"revision" : "aedcf6f4cd486ccef5b312ccac85d4b3f6e58605",
"version" : "1.1.2"
"revision" : "6ea3b1b6a4957806d72030a32360d4bcb155a0d2",
"version" : "1.2.0"
}
},
{
Expand Down Expand Up @@ -104,8 +104,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "8e68404f641300bfd0e37d478683bb275926760c",
"version" : "1.15.2"
"revision" : "e7b77228b34057041374ebef00c0fd7739d71a2b",
"version" : "1.15.3"
}
},
{
Expand Down
Loading

0 comments on commit b732659

Please sign in to comment.