Skip to content

Commit

Permalink
Merge pull request #1 from riiid/feature/TN-11858-reducerprotocol
Browse files Browse the repository at this point in the history
[TN-11858] Support ReducerProtocol
  • Loading branch information
korJAEYOUNGYUN authored Apr 26, 2023
2 parents 581ccef + be9a818 commit 14840cf
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Sources/TCADiagram/TCADiagram.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import TCADiagramLib
struct TCADiagram: ParsableCommand {
static var configuration: CommandConfiguration = .init(
commandName: "tca-diagram",
version: "0.2.0"
version: "0.3.0"
)

@Option(name: .shortAndLong, help: "Root directory of swift files")
Expand Down
84 changes: 82 additions & 2 deletions Sources/TCADiagramLib/Internal/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ extension SourceFileSyntax {
actions: inout Set<String>,
relations: inout [Relation]
) throws {

if let reducerProtocolParent = try predicateReducerProtocol(node) {
try travel(parent: reducerProtocolParent, node: node, actions: &actions, relations: &relations)
}

if let (node, parent, child) = try predicatePullbackCall(node) {
relations.append(
.init(
Expand All @@ -14,8 +19,6 @@ extension SourceFileSyntax {
optional: isOptionalPullback(node)
)
)
} else if false {
// TODO: predicateReducerProtocol
} else if let name = try predicateActionDecl(node) {
actions.insert(name)
} else {
Expand All @@ -28,9 +31,86 @@ extension SourceFileSyntax {
}
}
}

/// ReducerProtocol이 선언된 파일에서 child를 가져옵니다.
///
/// pullback과는 다르게 ReducerProtocol의 Scope나 ifLet에서는 부모피쳐 이름을 찾을수가 없습니다.
/// Reducer 선언부에서 찾은 부모 이름을 유지하면서 자식 피쳐들을 찾아나갑니다.
func travel(
parent: String,
node: Syntax,
actions: inout Set<String>,
relations: inout [Relation]
) throws {
if let (childs, isOptional) = try predicateChildReducerProtocol(node) {
childs.forEach { child in
relations.append(
.init(
parent: parent,
child: child.firstUppercased,
optional: isOptional
)
)
}
} else {
for child in node.children(viewMode: .all) {
try travel(
parent: parent,
node: child,
actions: &actions,
relations: &relations
)
}
}
}
}

extension SourceFileSyntax {

/// ReducerProtocol을 상속한 부분을 찾아 부모 피쳐 이름을 가져옵니다.
private func predicateReducerProtocol(_ node: Syntax) throws -> String? {
if
let node = StructDeclSyntax(node),
node.inheritanceClause?.tokens(viewMode: .fixedUp).contains(where: { $0.tokenKind == .identifier("ReducerProtocol") }) == true
{
return node.identifier.text
}
return nil
}

/// Scope 또는 ifLet 호출을 찾아 자식 피쳐 이름을 가져옵니다.
private func predicateChildReducerProtocol(_ node: Syntax) throws -> ([String], Bool)? {
if
let node = FunctionCallExprSyntax(node),
node.argumentList.contains(where: { syntax in syntax.label?.text == "action" })
{
if
node.tokens(viewMode: .fixedUp).contains(where: { $0.tokenKind == .identifier("Scope") }),
let child = node.trailingClosure?.statements.first?.description
.firstMatch(of: try Regex("\\s*(.+?)\\(\\)"))?[1]
.substring?
.description {
return ([child], false)
}

// ifLet은 method chaining으로 연달아서 붙어있기 때문에
// 매칭되는 모든 리듀서 이름들을 가져와 child 에 저장합니다.
if
node.tokens(viewMode: .fixedUp).contains(where: { $0.tokenKind == .identifier("ifLet") }) {
let childs = node.description
.matches(of: try Regex("ifLet.+{\\s+(.+?)\\(\\)"))
.compactMap {
$0[1].substring?.description
}
.filter {
$0 != "EmptyReducer"
}
return (childs, true)
}
}
return .none
}

/// pullback 함수 호출이 있는 부분을 찾아 부모, 자식 피쳐 이름을 가져옵니다.
///
/// 1. pullback 호출 부분을 찾습니다(코드 상으로는 마지막 컨디션입니다. 파라미터를 먼저 보는게 속도 측면에서 유리할 것 같아서).
Expand Down
22 changes: 22 additions & 0 deletions Tests/TCADiagramLibTests/DiagramTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,26 @@ final class DiagramTests: XCTestCase {
"""
XCTAssertEqual(result, expected)
}

func testReducerProtocolExample() throws {
let result = try Diagram.dump(reducerProtocolSampleSource)
let expected = """
```mermaid
%%{ init : { "theme" : "default", "flowchart" : { "curve" : "monotoneY" }}}%%
graph LR
SelfLessonDetail -- optional --> DoubleIfLetChild
SelfLessonDetail ---> DoubleScopeChild
SelfLessonDetail ---> Payment
SelfLessonDetail -- optional --> SantaWeb
SelfLessonDetail -- optional --> SelfLessonDetailFilter
DoubleIfLetChild(DoubleIfLetChild: 1)
DoubleScopeChild(DoubleScopeChild: 1)
Payment(Payment: 1)
SantaWeb(SantaWeb: 1)
SelfLessonDetailFilter(SelfLessonDetailFilter: 1)
```
"""
XCTAssertEqual(result, expected)
}
}
63 changes: 63 additions & 0 deletions Tests/TCADiagramLibTests/Resources/Sources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,66 @@ let sources: [String] = [
}
""",
]

let reducerProtocolSampleSource: [String] = [
"""
public struct SelfLessonDetail: ReducerProtocol {
@Dependency(\\.environmentSelfLessonDetail) private var environment
public init() {}
public var body: some ReducerProtocol<State, Action> {
BindingReducer()
Scope(state: \\State.payment, action: /Action.payment) {
Payment()
}
Scope(state: \\.subState, action: .self) {
Scope(
state: /State.SubState.promotionWeb,
action: /Action.promotionWeb
) {
DoubleScopeChild()
}
}
Reduce { state, action in
switch action {
case default:
return .none
}
}
.ifLet(\\.filter, action: /Action.filter) {
SelfLessonDetailFilter()
}
.ifLet(\\.selection, action: /Action.web) {
SantaWeb()
}
.ifLet(\\SelfLessonDetail.State.selection, action: /SelfLessonDetail.Action.webView) {
EmptyReducer()
.ifLet(\\Identified.value, action: .self) {
DoubleIfLetChild()
}
}
}
}
""",
"""
extension SelfLessonDetail {
public enum Action: Equatable {
}
}
extension Payment {
public enum Action: Equatable {
}
}
extension SantaWeb {
public enum Action: Equatable {
}
}
extension SelfLessonDetailFilter {
public enum Action: Equatable {
}
}
"""
]

0 comments on commit 14840cf

Please sign in to comment.