From 0bfdbe400456643412fb5713e30e8ff80c7cdf24 Mon Sep 17 00:00:00 2001 From: Rob Amos Date: Thu, 12 Dec 2024 14:23:46 +1100 Subject: [PATCH 1/2] Repurposed out FlagVisitor.beginGroup into FlagVisitor.beginContainer. FlagVisitor.beginGroup is now called before descending into a group and provides the group's wigwag. --- Sources/Vexil/Visitor.swift | 80 ++- Sources/VexilMacros/FlagContainerMacro.swift | 4 +- Sources/VexilMacros/FlagGroupMacro.swift | 23 +- .../EquatableFlagContainerMacroTests.swift | 482 ++---------------- .../FlagContainerMacroTests.swift | 118 +---- Tests/VexilTests/VisitorTests.swift | 118 +++++ 6 files changed, 275 insertions(+), 550 deletions(-) create mode 100644 Tests/VexilTests/VisitorTests.swift diff --git a/Sources/Vexil/Visitor.swift b/Sources/Vexil/Visitor.swift index 3cf82856..6b7872d9 100644 --- a/Sources/Vexil/Visitor.swift +++ b/Sources/Vexil/Visitor.swift @@ -15,10 +15,76 @@ /// Visitor pattern. Conform your type to this protocol and pass /// it to ``FlagPole/walk(visitor:)`` or any container using /// ``FlagContainer/walk(visitor:)``. +/// +/// Walking always starts at a Container, and then walks the children of that container. +/// When one of the children is a group, a call to ``beginGroup(keyPath:wigwag:)`` is made before +/// descending into the group's container. That container will then call ``beginContainer(keyPath:container:)`` +/// itself. You can use this to differentiate between the operations you are looking for. +/// +/// # Example +/// +/// Given the following flag hierarchy: +/// +/// ```swift +/// @FlagContainer +/// struct TestFlags { +/// +/// @Flag(...) +/// var topLevelFlag: Bool +/// +/// @FlagGroup(...) +/// var subgroup: SubgroupFlags +/// +/// } +/// +/// @FlagContainer +/// struct SubgroupFlags { +/// +/// @FlagGroup(...) +/// var doubleSubgroup: DoubleSubgroupFlags +/// +/// } +/// +/// @FlagContainer +/// struct DoubleSubgroupFlags { +/// +/// @Flag(...) +/// var thirdLevelFlag: Bool +/// +/// } +/// ``` +/// +/// You should expect to see the following callbacks: +/// +/// ```swift +/// visitor.beginContainer("") // root +/// visitor.visitFlag("top-level-flag") +/// visitor.beginGroup("subgroup") +/// +/// visitor.beginContainer("subgroup") +/// visitor.beginGroup("subgroup.double-subgroup") +/// +/// visitor.beginContainer("subgroup.double-subgroup") +/// visitor.visitFlag("subgroup.double-subgroup.third-level-flag") +/// visitor.endContainer("subgroup.double-subgroup") +/// +/// visitor.endGroup("subgroup.double-subgroup") +/// visitor.endContainer("subgroup") +/// +/// visitor.endGroup("subgroup") +/// visitor.endContainer("") // root +/// ``` +/// public protocol FlagVisitor { - /// Called when beginning to visit a new ``FlagGroup`` - func beginGroup(keyPath: FlagKeyPath) + /// Called when beginning to walk within a ``FlagContainer`` + func beginContainer(keyPath: FlagKeyPath, containerType: Container.Type) + + /// Called when finished visiting a ``FlagContainer``. + func endContainer(keyPath: FlagKeyPath) + + /// Called when about to descend into a new ``FlagGroup`` + func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) where Container: FlagContainer /// Called when finished visiting a ``FlagGroup`` func endGroup(keyPath: FlagKeyPath) @@ -49,7 +115,15 @@ public protocol FlagVisitor { public extension FlagVisitor { - func beginGroup(keyPath: FlagKeyPath) { + func beginContainer(keyPath: FlagKeyPath, containerType: Container.Type) { + // Intentionally left blank + } + + func endContainer(keyPath: FlagKeyPath) { + // Intentionally left blank + } + + func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) where Container: FlagContainer { // Intentionally left blank } diff --git a/Sources/VexilMacros/FlagContainerMacro.swift b/Sources/VexilMacros/FlagContainerMacro.swift index a6053895..733623f7 100644 --- a/Sources/VexilMacros/FlagContainerMacro.swift +++ b/Sources/VexilMacros/FlagContainerMacro.swift @@ -92,7 +92,7 @@ extension FlagContainerMacro: ExtensionMacro { // Flag Hierarchy Walking try FunctionDeclSyntax("func walk(visitor: any FlagVisitor)") { - "visitor.beginGroup(keyPath: _flagKeyPath)" + "visitor.beginContainer(keyPath: _flagKeyPath, containerType: \(type).self)" for variable in declaration.memberBlock.variables { if let flag = variable.asFlag(in: context) { flag.makeVisitExpression() @@ -100,7 +100,7 @@ extension FlagContainerMacro: ExtensionMacro { group.makeVisitExpression() } } - "visitor.endGroup(keyPath: _flagKeyPath)" + "visitor.endContainer(keyPath: _flagKeyPath)" } .with(\.modifiers, declaration.modifiers.scopeSyntax) diff --git a/Sources/VexilMacros/FlagGroupMacro.swift b/Sources/VexilMacros/FlagGroupMacro.swift index 617e9f6e..47341aaf 100644 --- a/Sources/VexilMacros/FlagGroupMacro.swift +++ b/Sources/VexilMacros/FlagGroupMacro.swift @@ -69,10 +69,25 @@ public struct FlagGroupMacro { """ } - func makeVisitExpression() -> CodeBlockItemSyntax { - """ - \(raw: propertyName).walk(visitor: visitor) - """ + func makeVisitExpression() -> CodeBlockItemListSyntax { + .init { + """ + visitor.beginGroup( + keyPath: \(key), + wigwag: { + FlagGroupWigwag<\(type)>( + keyPath: \(key), + name: \(name ?? "nil"), + description: \(description ?? "nil"), + displayOption: \(displayOption ?? ".navigation"), + lookup: _flagLookup + ) + } + ) + """ + "\(raw: propertyName).walk(visitor: visitor)" + "visitor.endGroup(keyPath: \(key))" + } } } diff --git a/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift b/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift index 003bea88..1bba4beb 100644 --- a/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift +++ b/Tests/VexilMacroTests/EquatableFlagContainerMacroTests.swift @@ -20,10 +20,6 @@ import XCTest final class EquatableFlagContainerMacroTests: XCTestCase { -#if compiler(>=6) - - // MARK: - Swift 6 - func testDoesntGenerateWhenEmpty() throws { assertMacroExpansion( """ @@ -47,8 +43,8 @@ final class EquatableFlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [:] @@ -102,7 +98,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("some-flag")), value: { [self] in @@ -113,7 +109,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $someFlag } ) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [ @@ -177,7 +173,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { public func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("some-flag")), value: { [self] in @@ -188,7 +184,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $someFlag } ) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.endContainer(keyPath: _flagKeyPath) } public var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [ @@ -251,7 +247,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("some-flag")), value: { [self] in @@ -262,7 +258,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $someFlag } ) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [ @@ -319,7 +315,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("first")), value: { [self] in @@ -330,7 +326,20 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $first } ) + visitor.beginGroup( + keyPath: _flagKeyPath.append(.automatic("flag-group")), + wigwag: { + FlagGroupWigwag( + keyPath: _flagKeyPath.append(.automatic("flag-group")), + name: nil, + description: "Test Group", + displayOption: .navigation, + lookup: _flagLookup + ) + } + ) flagGroup.walk(visitor: visitor) + visitor.endGroup(keyPath: _flagKeyPath.append(.automatic("flag-group"))) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("second")), value: { [self] in @@ -341,7 +350,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $second } ) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [ @@ -400,7 +409,7 @@ final class EquatableFlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { public func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("first")), value: { [self] in @@ -411,433 +420,20 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $first } ) - flagGroup.walk(visitor: visitor) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("second")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("second"))) - }, - defaultValue: false, - wigwag: { [self] in - $second - } - ) - visitor.endGroup(keyPath: _flagKeyPath) - } - public var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [ - \\TestFlags.first: _flagKeyPath.append(.automatic("first")), - \\TestFlags.second: _flagKeyPath.append(.automatic("second")), - ] - } - } - - extension TestFlags: Equatable { - public static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { - lhs.first == rhs.first && - lhs.flagGroup == rhs.flagGroup && - lhs.second == rhs.second - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - ] - ) - } - -#else - - // MARK: - Swift 5.10 - - func testDoesntGenerateWhenEmpty() throws { - assertMacroExpansion( - """ - @FlagContainer - struct TestFlags { - } - """, - expandedSource: """ - - struct TestFlags { - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.endGroup(keyPath: _flagKeyPath) - } - var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [:] - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - ] - ) - } - - func testExpandsInternal() throws { - assertMacroExpansion( - """ - @FlagContainer - struct TestFlags { - @Flag(default: false, description: "Some Flag") - var someFlag: Bool - } - """, - expandedSource: """ - - struct TestFlags { - var someFlag: Bool { - get { - _flagLookup.value(for: _flagKeyPath.append(.automatic("some-flag"))) ?? false - } - } - - var $someFlag: FlagWigwag { - FlagWigwag( - keyPath: _flagKeyPath.append(.automatic("some-flag")), - name: nil, - defaultValue: false, - description: "Some Flag", - displayOption: .default, - lookup: _flagLookup - ) - } - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("some-flag")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("some-flag"))) - }, - defaultValue: false, - wigwag: { [self] in - $someFlag - } - ) - visitor.endGroup(keyPath: _flagKeyPath) - } - var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [ - \\TestFlags.someFlag: _flagKeyPath.append(.automatic("some-flag")), - ] - } - } - - extension TestFlags: Equatable { - static func == (lhs: TestFlags, rhs: TestFlags) -> Bool { - lhs.someFlag == rhs.someFlag - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - "Flag": FlagMacro.self, - ] - ) - } - - func testExpandsPublic() throws { - assertMacroExpansion( - """ - @FlagContainer - public struct TestFlags { - @Flag(default: false, description: "Some Flag") - var someFlag: Bool - } - """, - expandedSource: - """ - - public struct TestFlags { - var someFlag: Bool { - get { - _flagLookup.value(for: _flagKeyPath.append(.automatic("some-flag"))) ?? false - } - } - - var $someFlag: FlagWigwag { - FlagWigwag( - keyPath: _flagKeyPath.append(.automatic("some-flag")), - name: nil, - defaultValue: false, - description: "Some Flag", - displayOption: .default, - lookup: _flagLookup - ) - } - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - public init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - public func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("some-flag")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("some-flag"))) - }, - defaultValue: false, - wigwag: { [self] in - $someFlag - } - ) - visitor.endGroup(keyPath: _flagKeyPath) - } - public var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [ - \\TestFlags.someFlag: _flagKeyPath.append(.automatic("some-flag")), - ] - } - } - - extension TestFlags: Equatable { - public static func == (lhs: TestFlags, rhs: TestFlags) -> Bool { - lhs.someFlag == rhs.someFlag - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - "Flag": FlagMacro.self, - ] - ) - } - - func testExpandsButAlreadyConforming() throws { - assertMacroExpansion( - """ - @FlagContainer - struct TestFlags: FlagContainer { - @Flag(default: false, description: "Some Flag") - var someFlag: Bool - } - """, - expandedSource: """ - - struct TestFlags: FlagContainer { - var someFlag: Bool { - get { - _flagLookup.value(for: _flagKeyPath.append(.automatic("some-flag"))) ?? false - } - } - - var $someFlag: FlagWigwag { - FlagWigwag( - keyPath: _flagKeyPath.append(.automatic("some-flag")), - name: nil, - defaultValue: false, - description: "Some Flag", - displayOption: .default, - lookup: _flagLookup - ) - } - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("some-flag")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("some-flag"))) - }, - defaultValue: false, - wigwag: { [self] in - $someFlag - } - ) - visitor.endGroup(keyPath: _flagKeyPath) - } - var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [ - \\TestFlags.someFlag: _flagKeyPath.append(.automatic("some-flag")), - ] - } - } - - extension TestFlags: Equatable { - static func == (lhs: TestFlags, rhs: TestFlags) -> Bool { - lhs.someFlag == rhs.someFlag - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - "Flag": FlagMacro.self, - ] - ) - } - - func testExpandsVisitorAndEquatableImplementation() throws { - assertMacroExpansion( - """ - @FlagContainer - struct TestFlags { - @Flag(default: false, description: "Flag 1") - var first: Bool - @FlagGroup(description: "Test Group") - var flagGroup: GroupOfFlags - @Flag(default: false, description: "Flag 2") - var second: Bool - } - """, - expandedSource: """ - - struct TestFlags { - @Flag(default: false, description: "Flag 1") - var first: Bool - @FlagGroup(description: "Test Group") - var flagGroup: GroupOfFlags - @Flag(default: false, description: "Flag 2") - var second: Bool - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("first")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("first"))) - }, - defaultValue: false, - wigwag: { [self] in - $first - } - ) - flagGroup.walk(visitor: visitor) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("second")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("second"))) - }, - defaultValue: false, - wigwag: { [self] in - $second - } - ) - visitor.endGroup(keyPath: _flagKeyPath) - } - var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [ - \\TestFlags.first: _flagKeyPath.append(.automatic("first")), - \\TestFlags.second: _flagKeyPath.append(.automatic("second")), - ] - } - } - - extension TestFlags: Equatable { - static func == (lhs: TestFlags, rhs: TestFlags) -> Bool { - lhs.first == rhs.first && - lhs.flagGroup == rhs.flagGroup && - lhs.second == rhs.second - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - ] - ) - } - - func testExpandsVisitorAndEquatablePublicImplementation() throws { - assertMacroExpansion( - """ - @FlagContainer - public struct TestFlags { - @Flag(default: false, description: "Flag 1") - public var first: Bool - @FlagGroup(description: "Test Group") - public var flagGroup: GroupOfFlags - @Flag(default: false, description: "Flag 2") - public var second: Bool - } - """, - expandedSource: """ - - public struct TestFlags { - @Flag(default: false, description: "Flag 1") - public var first: Bool - @FlagGroup(description: "Test Group") - public var flagGroup: GroupOfFlags - @Flag(default: false, description: "Flag 2") - public var second: Bool - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - public init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - public func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("first")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("first"))) - }, - defaultValue: false, - wigwag: { [self] in - $first + visitor.beginGroup( + keyPath: _flagKeyPath.append(.automatic("flag-group")), + wigwag: { + FlagGroupWigwag( + keyPath: _flagKeyPath.append(.automatic("flag-group")), + name: nil, + description: "Test Group", + displayOption: .navigation, + lookup: _flagLookup + ) } ) flagGroup.walk(visitor: visitor) + visitor.endGroup(keyPath: _flagKeyPath.append(.automatic("flag-group"))) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("second")), value: { [self] in @@ -848,18 +444,18 @@ final class EquatableFlagContainerMacroTests: XCTestCase { $second } ) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.endContainer(keyPath: _flagKeyPath) } public var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [ \\TestFlags.first: _flagKeyPath.append(.automatic("first")), \\TestFlags.second: _flagKeyPath.append(.automatic("second")), - ] + ] } } extension TestFlags: Equatable { - public static func == (lhs: TestFlags, rhs: TestFlags) -> Bool { + public static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { lhs.first == rhs.first && lhs.flagGroup == rhs.flagGroup && lhs.second == rhs.second @@ -872,8 +468,6 @@ final class EquatableFlagContainerMacroTests: XCTestCase { ) } -#endif // swift version check - } #endif // canImport(VexilMacros) diff --git a/Tests/VexilMacroTests/FlagContainerMacroTests.swift b/Tests/VexilMacroTests/FlagContainerMacroTests.swift index 9b10f354..c0accb8f 100644 --- a/Tests/VexilMacroTests/FlagContainerMacroTests.swift +++ b/Tests/VexilMacroTests/FlagContainerMacroTests.swift @@ -43,8 +43,8 @@ final class FlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [:] @@ -80,8 +80,8 @@ final class FlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { public func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) + visitor.endContainer(keyPath: _flagKeyPath) } public var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [:] @@ -117,8 +117,8 @@ final class FlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [:] @@ -131,8 +131,6 @@ final class FlagContainerMacroTests: XCTestCase { ) } -#if compiler(>=6) - // MARK: - Swift 6 specific tests func testExpandsVisitorImplementation() throws { @@ -170,7 +168,7 @@ final class FlagContainerMacroTests: XCTestCase { extension TestFlags: FlagContainer { func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) + visitor.beginContainer(keyPath: _flagKeyPath, containerType: TestFlags.self) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("first")), value: { [self] in @@ -181,92 +179,20 @@ final class FlagContainerMacroTests: XCTestCase { $first } ) - flagGroup.walk(visitor: visitor) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("second")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("second"))) - }, - defaultValue: false, - wigwag: { [self] in - $second - } - ) - visitor.endGroup(keyPath: _flagKeyPath) - } - var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { - [ - \\TestFlags.first: _flagKeyPath.append(.automatic("first")), - \\TestFlags.second: _flagKeyPath.append(.automatic("second")), - ] - } - } - - extension TestFlags: Equatable { - static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { - lhs.first == rhs.first && - lhs.flagGroup == rhs.flagGroup && - lhs.second == rhs.second - } - } - """, - macros: [ - "FlagContainer": FlagContainerMacro.self, - ] - ) - } - -#else - - // MARK: - Swift 5.10 specific tests - - func testExpandsVisitorImplementation() throws { - assertMacroExpansion( - """ - @FlagContainer - struct TestFlags { - @Flag(default: false, description: "Flag 1") - var first: Bool - @FlagGroup(description: "Test Group") - var flagGroup: GroupOfFlags - @Flag(default: false, description: "Flag 2") - var second: Bool - } - """, - expandedSource: """ - - struct TestFlags { - @Flag(default: false, description: "Flag 1") - var first: Bool - @FlagGroup(description: "Test Group") - var flagGroup: GroupOfFlags - @Flag(default: false, description: "Flag 2") - var second: Bool - - fileprivate let _flagKeyPath: FlagKeyPath - - fileprivate let _flagLookup: any FlagLookup - - init(_flagKeyPath: FlagKeyPath, _flagLookup: any FlagLookup) { - self._flagKeyPath = _flagKeyPath - self._flagLookup = _flagLookup - } - } - - extension TestFlags: FlagContainer { - func walk(visitor: any FlagVisitor) { - visitor.beginGroup(keyPath: _flagKeyPath) - visitor.visitFlag( - keyPath: _flagKeyPath.append(.automatic("first")), - value: { [self] in - _flagLookup.value(for: _flagKeyPath.append(.automatic("first"))) - }, - defaultValue: false, - wigwag: { [self] in - $first + visitor.beginGroup( + keyPath: _flagKeyPath.append(.automatic("flag-group")), + wigwag: { + FlagGroupWigwag( + keyPath: _flagKeyPath.append(.automatic("flag-group")), + name: nil, + description: "Test Group", + displayOption: .navigation, + lookup: _flagLookup + ) } ) flagGroup.walk(visitor: visitor) + visitor.endGroup(keyPath: _flagKeyPath.append(.automatic("flag-group"))) visitor.visitFlag( keyPath: _flagKeyPath.append(.automatic("second")), value: { [self] in @@ -277,18 +203,18 @@ final class FlagContainerMacroTests: XCTestCase { $second } ) - visitor.endGroup(keyPath: _flagKeyPath) + visitor.endContainer(keyPath: _flagKeyPath) } var _allFlagKeyPaths: [PartialKeyPath: FlagKeyPath] { [ \\TestFlags.first: _flagKeyPath.append(.automatic("first")), \\TestFlags.second: _flagKeyPath.append(.automatic("second")), - ] + ] } } extension TestFlags: Equatable { - static func == (lhs: TestFlags, rhs: TestFlags) -> Bool { + static func ==(lhs: TestFlags, rhs: TestFlags) -> Bool { lhs.first == rhs.first && lhs.flagGroup == rhs.flagGroup && lhs.second == rhs.second @@ -301,8 +227,6 @@ final class FlagContainerMacroTests: XCTestCase { ) } -#endif // swift version check - } #endif // canImport(VexilMacros) diff --git a/Tests/VexilTests/VisitorTests.swift b/Tests/VexilTests/VisitorTests.swift new file mode 100644 index 00000000..a01901de --- /dev/null +++ b/Tests/VexilTests/VisitorTests.swift @@ -0,0 +1,118 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Vexil open source project +// +// Copyright (c) 2024 Unsigned Apps and the open source contributors. +// Licensed under the MIT license +// +// See LICENSE for license information +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +import Testing +import Vexil + +@Suite("Visitors") +struct VisitorTests { + + @Test("Visits every expected element") + func visitsEveryElement() { + let pole = FlagPole(hoist: TestFlags.self, configuration: .init(codingPathStrategy: .kebabcase, prefix: nil, separator: "."), sources: []) + let visitor = Visitor() + pole.walk(visitor: visitor) + + #expect( + visitor.events == [ + .beginContainer(""), // root + .visitFlag("top-level-flag"), + .visitFlag("second-test-flag"), + .beginGroup("subgroup"), + + .beginContainer("subgroup"), + .visitFlag("subgroup.second-level-flag"), + .beginGroup("subgroup.double-subgroup"), + + .beginContainer("subgroup.double-subgroup"), + .visitFlag("subgroup.double-subgroup.third-level-flag"), + .endContainer("subgroup.double-subgroup"), + + .endGroup("subgroup.double-subgroup"), + .endContainer("subgroup"), + + .endGroup("subgroup"), + .endContainer(""), // root + ] + ) + } +} + +// MARK: - Fixtures + +@FlagContainer +private struct TestFlags { + + @Flag(default: false, description: "Top level test flag") + var topLevelFlag: Bool + + @Flag(default: false, description: "Second test flag") + var secondTestFlag: Bool + + @FlagGroup(description: "Subgroup of test flags") + var subgroup: SubgroupFlags + +} + +@FlagContainer +private struct SubgroupFlags { + + @Flag(default: false, description: "Second level test flag") + var secondLevelFlag: Bool + + @FlagGroup(description: "Another level of test flags") + var doubleSubgroup: DoubleSubgroupFlags + +} + +@FlagContainer +private struct DoubleSubgroupFlags { + + @Flag(default: false, description: "Third level test flag") + var thirdLevelFlag: Bool + +} + +private final class Visitor: FlagVisitor { + + enum Event: Equatable { + case beginContainer(String) + case endContainer(String) + case beginGroup(String) + case endGroup(String) + case visitFlag(String) + } + + var events: [Event] = [] + + func beginContainer(keyPath: FlagKeyPath, containerType: Container.Type) { + events.append(.beginContainer(keyPath.key)) + } + + func endContainer(keyPath: FlagKeyPath) { + events.append(.endContainer(keyPath.key)) + } + + func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) where Container: FlagContainer { + events.append(.beginGroup(keyPath.key)) + } + + func endGroup(keyPath: FlagKeyPath) { + events.append(.endGroup(keyPath.key)) + } + + func visitFlag(keyPath: FlagKeyPath, value: () -> Value?, defaultValue: Value, wigwag: () -> FlagWigwag) where Value: FlagValue { + events.append(.visitFlag(keyPath.key)) + } + +} From 3037a040ea3f80b2a5975513727d2416f54276b4 Mon Sep 17 00:00:00 2001 From: Rob Amos Date: Thu, 12 Dec 2024 14:37:58 +1100 Subject: [PATCH 2/2] Formatting --- Sources/Vexil/Visitor.swift | 8 ++++---- Tests/VexilTests/VisitorTests.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Vexil/Visitor.swift b/Sources/Vexil/Visitor.swift index 6b7872d9..c55deb91 100644 --- a/Sources/Vexil/Visitor.swift +++ b/Sources/Vexil/Visitor.swift @@ -78,13 +78,13 @@ public protocol FlagVisitor { /// Called when beginning to walk within a ``FlagContainer`` - func beginContainer(keyPath: FlagKeyPath, containerType: Container.Type) + func beginContainer(keyPath: FlagKeyPath, containerType: Any.Type) /// Called when finished visiting a ``FlagContainer``. func endContainer(keyPath: FlagKeyPath) /// Called when about to descend into a new ``FlagGroup`` - func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) where Container: FlagContainer + func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) /// Called when finished visiting a ``FlagGroup`` func endGroup(keyPath: FlagKeyPath) @@ -115,7 +115,7 @@ public protocol FlagVisitor { public extension FlagVisitor { - func beginContainer(keyPath: FlagKeyPath, containerType: Container.Type) { + func beginContainer(keyPath: FlagKeyPath, containerType: Any.Type) { // Intentionally left blank } @@ -123,7 +123,7 @@ public extension FlagVisitor { // Intentionally left blank } - func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) where Container: FlagContainer { + func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) { // Intentionally left blank } diff --git a/Tests/VexilTests/VisitorTests.swift b/Tests/VexilTests/VisitorTests.swift index a01901de..a4859c99 100644 --- a/Tests/VexilTests/VisitorTests.swift +++ b/Tests/VexilTests/VisitorTests.swift @@ -95,7 +95,7 @@ private final class Visitor: FlagVisitor { var events: [Event] = [] - func beginContainer(keyPath: FlagKeyPath, containerType: Container.Type) { + func beginContainer(keyPath: FlagKeyPath, containerType: Any.Type) { events.append(.beginContainer(keyPath.key)) } @@ -103,7 +103,7 @@ private final class Visitor: FlagVisitor { events.append(.endContainer(keyPath.key)) } - func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) where Container: FlagContainer { + func beginGroup(keyPath: FlagKeyPath, wigwag: () -> FlagGroupWigwag) { events.append(.beginGroup(keyPath.key)) }