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

Support nested enum reducer (#2813) #2814

Merged
merged 8 commits into from Apr 29, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 14 additions & 4 deletions Sources/ComposableArchitectureMacros/ReducerMacro.swift
Expand Up @@ -468,7 +468,7 @@ private enum ReducerCase {
let parameter = parameterClause.parameters.first,
parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self)
{
let stateCase = attribute == .ephemeral ? element : element.suffixed("State")
let stateCase = attribute == .ephemeral ? element : element.suffixed("State").type
return "case \(stateCase.trimmedDescription)"
} else {
return "case \(element.trimmedDescription)"
Expand Down Expand Up @@ -496,7 +496,7 @@ private enum ReducerCase {
let parameter = parameterClause.parameters.first,
parameter.type.is(IdentifierTypeSyntax.self) || parameter.type.is(MemberTypeSyntax.self)
{
return "case \(element.suffixed("Action").trimmedDescription)"
return "case \(element.suffixed("Action").type.trimmedDescription)"
} else {
return nil
}
Expand Down Expand Up @@ -545,11 +545,12 @@ private enum ReducerCase {
{
let name = element.name.text
let type = parameter.type
let reducer = parameter.defaultValue?.value.trimmedDescription ?? "\(type.trimmed)()"
return """
ComposableArchitecture.Scope(\
state: \\Self.State.Cases.\(name), action: \\Self.Action.Cases.\(name)\
) {
\(type.trimmed)()
\(reducer)
}
"""
} else {
Expand Down Expand Up @@ -747,12 +748,21 @@ extension EnumCaseDeclSyntax {
}

extension EnumCaseElementSyntax {
fileprivate var type: Self {
var element = self
if var parameterClause = element.parameterClause {
parameterClause.parameters[parameterClause.parameters.startIndex].defaultValue = nil
element.parameterClause = parameterClause
}
return element
}

fileprivate func suffixed(_ suffix: TokenSyntax) -> Self {
var element = self
if var parameterClause = element.parameterClause,
let type = parameterClause.parameters.first?.type
{
let type = MemberTypeSyntax(baseType: type, name: suffix)
let type = MemberTypeSyntax(baseType: type.trimmed, name: suffix)
parameterClause.parameters[parameterClause.parameters.startIndex].type = TypeSyntax(type)
element.parameterClause = parameterClause
}
Expand Down
61 changes: 61 additions & 0 deletions Tests/ComposableArchitectureMacrosTests/ReducerMacroTests.swift
Expand Up @@ -340,6 +340,67 @@
"""#
}
}

func testEnum_DefaultInitializer() {
assertMacro {
"""
@Reducer
enum Destination {
case timeline(Timeline)
case meeting(Meeting = Meeting(context: .sheet))
}
"""
} expansion: {
#"""
enum Destination {
case timeline(Timeline)
case meeting(Meeting = Meeting(context: .sheet))

@CasePathable
@dynamicMemberLookup
@ObservableState
enum State: ComposableArchitecture.CaseReducerState {
typealias StateReducer = Destination
case timeline(Timeline.State)
case meeting(Meeting.State)
}

@CasePathable
enum Action {
case timeline(Timeline.Action)
case meeting(Meeting.Action)
}

@ComposableArchitecture.ReducerBuilder<Self.State, Self.Action>
static var body: ComposableArchitecture.ReducerBuilder<Self.State, Self.Action>._Sequence<ComposableArchitecture.Scope<Self.State, Self.Action, Timeline>, ComposableArchitecture.Scope<Self.State, Self.Action, Meeting>> {
ComposableArchitecture.Scope(state: \Self.State.Cases.timeline, action: \Self.Action.Cases.timeline) {
Timeline()
}
ComposableArchitecture.Scope(state: \Self.State.Cases.meeting, action: \Self.Action.Cases.meeting) {
Meeting(context: .sheet)
}
}

enum CaseScope {
case timeline(ComposableArchitecture.StoreOf<Timeline>)
case meeting(ComposableArchitecture.StoreOf<Meeting>)
}

static func scope(_ store: ComposableArchitecture.Store<Self.State, Self.Action>) -> CaseScope {
switch store.state {
case .timeline:
return .timeline(store.scope(state: \.timeline, action: \.timeline)!)
case .meeting:
return .meeting(store.scope(state: \.meeting, action: \.meeting)!)
}
}
}

extension Destination: ComposableArchitecture.CaseReducer, ComposableArchitecture.Reducer {
}
"""#
}
}

func testEnum_Empty() {
assertMacro {
Expand Down
11 changes: 11 additions & 0 deletions Tests/ComposableArchitectureTests/EnumReducerMacroTests.swift
Expand Up @@ -28,4 +28,15 @@
#endif
}
}

private enum TestEnumReducer_DefaultInitializer {
@Reducer
struct Feature {
let context: String
}
@Reducer
enum Destination1 {
case feature1(Feature = Feature(context: "context"))
}
}
#endif