Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for changing the attachment location of a @ComposeBodyOnChange function to the BindingReducer or a child Scope Reducer. #5

Merged
merged 1 commit into from
Feb 25, 2024
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
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