From e5aba039b0086522f6fe4d139f693cd9efd94329 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 6 Aug 2024 21:46:54 -0700 Subject: [PATCH 1/7] Optional Paths --- Package@swift-6.0.swift | 4 +- Sources/CasePaths/CasePathIterable.swift | 2 +- Sources/CasePaths/CasePathReflectable.swift | 2 +- Sources/CasePaths/CasePathable.swift | 137 +++++++++++------- Sources/CasePaths/Never+CasePathable.swift | 18 ++- Sources/CasePaths/Optional+CasePathable.swift | 21 +-- Sources/CasePaths/Result+CasePathable.swift | 4 +- Tests/CasePathsTests/CasePathsTests.swift | 4 +- Tests/CasePathsTests/DeprecatedTests.swift | 3 - Tests/CasePathsTests/OptionalPathsTests.swift | 43 ++++++ 10 files changed, 153 insertions(+), 85 deletions(-) create mode 100644 Tests/CasePathsTests/OptionalPathsTests.swift diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 94cae578..2de0add7 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -19,8 +19,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), + .package(url: "https://github.com/pointfreeco/swift-macro-testing", branch: "fix-test-trait"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"), ], targets: [ .target( @@ -50,7 +50,7 @@ let package = Package( ] ), ], - swiftLanguageVersions: [.v6] + swiftLanguageModes: [.v6] ) #if !os(Windows) diff --git a/Sources/CasePaths/CasePathIterable.swift b/Sources/CasePaths/CasePathIterable.swift index c49ed45e..cf4ae731 100644 --- a/Sources/CasePaths/CasePathIterable.swift +++ b/Sources/CasePaths/CasePathIterable.swift @@ -14,4 +14,4 @@ /// Array(Field.allCasePaths) // [\.title, \.body, \.isLive] /// ``` public protocol CasePathIterable: CasePathable -where AllCasePaths: Sequence, AllCasePaths.Element == PartialCaseKeyPath {} +where AllCasePaths: Sequence, AllCasePaths.Element == PartialOptionalKeyPath {} diff --git a/Sources/CasePaths/CasePathReflectable.swift b/Sources/CasePaths/CasePathReflectable.swift index 2e7cbb21..db37c026 100644 --- a/Sources/CasePaths/CasePathReflectable.swift +++ b/Sources/CasePaths/CasePathReflectable.swift @@ -23,5 +23,5 @@ public protocol CasePathReflectable { /// /// - Parameter root: An root value. /// - Returns: A case path to the root value. - subscript(root: Root) -> PartialCaseKeyPath { get } + subscript(root: Root) -> PartialOptionalKeyPath { get } } diff --git a/Sources/CasePaths/CasePathable.swift b/Sources/CasePaths/CasePathable.swift index 9fc77b15..e5c58adc 100644 --- a/Sources/CasePaths/CasePathable.swift +++ b/Sources/CasePaths/CasePathable.swift @@ -49,60 +49,99 @@ public protocol CasePathable { @dynamicMemberLookup public struct Case: Sendable { fileprivate let _embed: @Sendable (Value) -> Any - fileprivate let _extract: @Sendable (Any) -> Value? + fileprivate let _get: @Sendable (Any) -> Value? + fileprivate let _set: @Sendable (inout Any, Value) -> Void } extension Case { + private struct Unembeddable {} + public init( embed: @escaping @Sendable (Value) -> Root, extract: @escaping @Sendable (Root) -> Value? ) { self._embed = embed - self._extract = { @Sendable in ($0 as? Root).flatMap(extract) } + self._get = { @Sendable in ($0 as? Root).flatMap(extract) } + self._set = { @Sendable in $0 = embed($1) } + } + + public init( + get: @escaping @Sendable (Root) -> Value?, + set: @escaping @Sendable (inout Root, Value) -> Void + ) { + self.init(embed: nil, get: get, set: set) + } + + fileprivate init( + embed: (@Sendable (Value) -> Root)?, + get: @escaping @Sendable (Root) -> Value?, + set: @escaping @Sendable (inout Root, Value) -> Void + ) { + self._embed = embed ?? { _ in Unembeddable() } + self._get = { @Sendable in ($0 as? Root).flatMap(get) } + self._set = { @Sendable in + var root = $0 as! Root + set(&root, $1) + $0 = root + } } public init() { self.init(embed: { $0 }, extract: { $0 }) } - public init(_ keyPath: CaseKeyPath) { + public init(_ keyPath: OptionalKeyPath) { self = Case()[keyPath: keyPath] } - // #if swift(>=6) - // public subscript( - // dynamicMember keyPath: KeyPath> - // & Sendable - // ) -> Case - // where Value: CasePathable { - // Case( - // embed: { embed(Value.allCasePaths[keyPath: keyPath].embed($0)) }, - // extract: { extract(from: $0).flatMap(Value.allCasePaths[keyPath: keyPath].extract) } - // ) - // } - // #else public subscript( dynamicMember keyPath: KeyPath> ) -> Case where Value: CasePathable { + get { + @UncheckedSendable var keyPath = keyPath + return Case( + embed: { [$keyPath] in + embed(Value.allCasePaths[keyPath: $keyPath.wrappedValue].embed($0)) + }, + get: { [$keyPath] in + extract(from: $0).flatMap(Value.allCasePaths[keyPath: $keyPath.wrappedValue].extract) + }, + set: { [$keyPath] in + set(into: &$0, Value.allCasePaths[keyPath: $keyPath.wrappedValue].embed($1)) + } + ) + } + set {} + } + + @_disfavoredOverload + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> Case { @UncheckedSendable var keyPath = keyPath return Case( - embed: { [$keyPath] in - embed(Value.allCasePaths[keyPath: $keyPath.wrappedValue].embed($0)) + get: { [$keyPath] in + extract(from: $0)?[keyPath: $keyPath.wrappedValue] }, - extract: { [$keyPath] in - extract(from: $0).flatMap(Value.allCasePaths[keyPath: $keyPath.wrappedValue].extract) + set: { [$keyPath] in + guard var value = extract(from: $0) else { return } + value[keyPath: $keyPath.wrappedValue] = $1 + set(into: &$0, value) } ) } - // #endif public func embed(_ value: Value) -> Any { self._embed(value) } public func extract(from root: Any) -> Value? { - self._extract(root) + self._get(root) + } + + public func set(into root: inout Any, _ value: Value) { + self._set(&root, value) } } @@ -190,7 +229,9 @@ extension Case: _AnyCase { /// identity case key path `\SomeEnum.Cases.self`. It refers to the whole enum and can be passed to /// a function that takes case key paths when you want to extract, change, or replace all of the /// data stored in an enum in a single step. -public typealias CaseKeyPath = KeyPath, Case> +public typealias CaseKeyPath = WritableKeyPath, Case> + +public typealias OptionalKeyPath = KeyPath, Case> extension CaseKeyPath { /// Embeds a value in an enum at this case key path's case. @@ -243,7 +284,9 @@ extension CaseKeyPath { where Root == Case, Value == Case { Case(self).embed(()) as! Enum } +} +extension OptionalKeyPath { /// Whether an argument matches the case key path's case. /// /// ```swift @@ -277,11 +320,17 @@ extension CaseKeyPath { where Root == Case, Value == Case { rhs[case: lhs] != nil } + + public func extract(from root: R) -> V? where Root == Case, Value == Case { + Case(self).extract(from: root) + } } /// A partially type-erased key path, from a concrete root enum to any resulting value type. public typealias PartialCaseKeyPath = PartialKeyPath> +public typealias PartialOptionalKeyPath = PartialKeyPath> + extension _AppendKeyPath { /// Attempts to embeds any value in an enum at this case key path's case. /// @@ -289,16 +338,20 @@ extension _AppendKeyPath { /// type, the operation will fail. /// - Returns: An enum for the case of this key path that holds the given value, or `nil`. @_disfavoredOverload - public func callAsFunction( + public func callAsFunction( _ value: Any ) -> Enum? - where Self == PartialCaseKeyPath { + where Self == PartialOptionalKeyPath { func open(_ value: AnyAssociatedValue) -> Enum? { (Case()[keyPath: self] as? Case)?.embed(value) as? Enum ?? (Case()[keyPath: self] as? Case)?.embed(value) as? Enum } return _openExistential(value, do: open) } + + public func extract(from root: R) -> Any? where Self == PartialOptionalKeyPath { + (Case()[keyPath: self] as? any _AnyCase)?._extract(from: root) + } } extension CasePathable { @@ -337,13 +390,13 @@ extension CasePathable { /// See ``CasePathable/subscript(case:)-8yr2s`` for replacing an associated value in a root /// enum, and see ``Swift/KeyPath/callAsFunction(_:)`` for embedding an associated value in a /// brand new root enum. - public subscript(case keyPath: CaseKeyPath) -> Value? { - Case(keyPath).extract(from: self) + public subscript(case keyPath: OptionalKeyPath) -> Value? { + keyPath.extract(from: self) } /// Attempts to extract the associated value from a root enum using a partial case key path. @_disfavoredOverload - public subscript(case keyPath: PartialCaseKeyPath) -> Any? { + public subscript(case keyPath: PartialOptionalKeyPath) -> Any? { (Case()[keyPath: keyPath] as? any _AnyCase)?._extract(from: self) } @@ -371,7 +424,7 @@ extension CasePathable { /// enum, and see ``Swift/KeyPath/callAsFunction(_:)`` for embedding an associated value in a /// brand new root enum. @_disfavoredOverload - public subscript(case keyPath: CaseKeyPath) -> Value { + public subscript(case keyPath: OptionalKeyPath) -> Value { @available(*, unavailable) get { fatalError() } set { @@ -450,7 +503,7 @@ extension CasePathable { /// userActions.filter { $0.is(\.home) } // [UserAction.home(.onAppear)] /// userActions.filter { $0.is(\.settings) } // [UserAction.settings(.subscribeButtonTapped)] /// ``` - public func `is`(_ keyPath: PartialCaseKeyPath) -> Bool { + public func `is`(_ keyPath: PartialOptionalKeyPath) -> Bool { self[case: keyPath] != nil } @@ -481,7 +534,7 @@ extension CasePathable { /// - line: The line where the modify occurs. /// - column: The column where the modify occurs. public mutating func modify( - _ keyPath: CaseKeyPath, + _ keyPath: OptionalKeyPath, yield: (inout Value) -> Void, fileID: StaticString = #fileID, filePath: StaticString = #filePath, @@ -503,7 +556,9 @@ extension CasePathable { return } yield(&value) - self = `case`.embed(value) as! Self + var anySelf = self as Any + `case`.set(into: &anySelf, value) + self = anySelf as! Self } } @@ -521,25 +576,6 @@ extension AnyCasePath { } extension AnyCasePath where Value: CasePathable { - // #if swift(>=6) - // /// Returns a new case path created by appending the case path at the given key path to this one. - // /// - // /// This subscript is automatically invoked by case key path expressions via dynamic member - // /// lookup, and should not be invoked directly. - // /// - // /// - Parameter keyPath: A key path to a case-pathable case path. - // public subscript( - // dynamicMember keyPath: KeyPath> - // & Sendable - // ) -> AnyCasePath { - // AnyCasePath( - // embed: { self.embed(Value.allCasePaths[keyPath: keyPath].embed($0)) }, - // extract: { - // self.extract(from: $0).flatMap(Value.allCasePaths[keyPath: keyPath].extract(from:)) - // } - // ) - // } - // #else /// Returns a new case path created by appending the case path at the given key path to this one. /// /// This subscript is automatically invoked by case key path expressions via dynamic member @@ -561,5 +597,4 @@ extension AnyCasePath where Value: CasePathable { } ) } - // #endif } diff --git a/Sources/CasePaths/Never+CasePathable.swift b/Sources/CasePaths/Never+CasePathable.swift index 0bf6b090..809f2f24 100644 --- a/Sources/CasePaths/Never+CasePathable.swift +++ b/Sources/CasePaths/Never+CasePathable.swift @@ -1,6 +1,6 @@ extension Never: CasePathable, CasePathIterable { public struct AllCasePaths: CasePathReflectable, Sendable { - public subscript(root: Never) -> PartialCaseKeyPath { + public subscript(root: Never) -> PartialOptionalKeyPath { \.never } } @@ -16,21 +16,27 @@ extension Case where Value: CasePathable { /// This property can chain any case path into a `Never` value, which, as an uninhabited type, /// cannot be embedded nor extracted from an enum. public var never: Case { - @Sendable func absurd(_: Never) -> T {} - return Case(embed: absurd, extract: { (_: Value) in nil }) + get { + @Sendable func absurd(_: Never) -> T {} + return Case(embed: absurd, extract: { (_: Value) in nil }) + } + set {} } } extension Case { @available(*, deprecated, message: "This enum must be '@CasePathable' to enable key path syntax") public var never: Case { - @Sendable func absurd(_: Never) -> T {} - return Case(embed: absurd, extract: { (_: Value) in nil }) + get { + @Sendable func absurd(_: Never) -> T {} + return Case(embed: absurd, extract: { (_: Value) in nil }) + } + set {} } } extension Never.AllCasePaths: Sequence { - public func makeIterator() -> some IteratorProtocol> { + public func makeIterator() -> some IteratorProtocol> { [].makeIterator() } } diff --git a/Sources/CasePaths/Optional+CasePathable.swift b/Sources/CasePaths/Optional+CasePathable.swift index bc7deaaa..16bcfc74 100644 --- a/Sources/CasePaths/Optional+CasePathable.swift +++ b/Sources/CasePaths/Optional+CasePathable.swift @@ -1,7 +1,7 @@ extension Optional: CasePathable, CasePathIterable { @dynamicMemberLookup public struct AllCasePaths: CasePathReflectable, Sendable { - public subscript(root: Optional) -> PartialCaseKeyPath { + public subscript(root: Optional) -> PartialOptionalKeyPath { switch root { case .none: return \.none case .some: return \.some @@ -54,19 +54,6 @@ extension Optional: CasePathable, CasePathIterable { } extension Case { - // #if swift(>=6) - // /// A case path to the presence of a nested value. - // /// - // /// This subscript can chain into an optional's wrapped value without explicitly specifying each - // /// `some` component. - // @_disfavoredOverload - // public subscript( - // dynamicMember keyPath: KeyPath> & Sendable - // ) -> Case - // where Value: CasePathable { - // self[dynamicMember: keyPath].some - // } - // #else /// A case path to the presence of a nested value. /// /// This subscript can chain into an optional's wrapped value without explicitly specifying each @@ -76,13 +63,13 @@ extension Case { dynamicMember keyPath: KeyPath> ) -> Case where Value: CasePathable { - self[dynamicMember: keyPath].some + get { self[dynamicMember: keyPath].some } + set {} } - // #endif } extension Optional.AllCasePaths: Sequence { - public func makeIterator() -> some IteratorProtocol> { + public func makeIterator() -> some IteratorProtocol> { [\.none, \.some].makeIterator() } } diff --git a/Sources/CasePaths/Result+CasePathable.swift b/Sources/CasePaths/Result+CasePathable.swift index 86af9356..efb4061c 100644 --- a/Sources/CasePaths/Result+CasePathable.swift +++ b/Sources/CasePaths/Result+CasePathable.swift @@ -1,6 +1,6 @@ extension Result: CasePathable, CasePathIterable { public struct AllCasePaths: CasePathReflectable, Sendable { - public subscript(root: Result) -> PartialCaseKeyPath { + public subscript(root: Result) -> PartialOptionalKeyPath { switch root { case .success: return \.success case .failure: return \.failure @@ -36,7 +36,7 @@ extension Result: CasePathable, CasePathIterable { } extension Result.AllCasePaths: Sequence { - public func makeIterator() -> some IteratorProtocol> { + public func makeIterator() -> some IteratorProtocol> { [\.success, \.failure].makeIterator() } } diff --git a/Tests/CasePathsTests/CasePathsTests.swift b/Tests/CasePathsTests/CasePathsTests.swift index 03574781..d122f70a 100644 --- a/Tests/CasePathsTests/CasePathsTests.swift +++ b/Tests/CasePathsTests/CasePathsTests.swift @@ -128,8 +128,8 @@ final class CasePathsTests: XCTestCase { #endif func testAppend() { - let fooToBar = \Foo.Cases.bar - let barToInt = \Bar.Cases.int + let fooToBar: WritableKeyPath = \Foo.Cases.bar + let barToInt: WritableKeyPath = \Bar.Cases.int let fooToInt = fooToBar.appending(path: barToInt) XCTAssertEqual(Foo.bar(.int(42))[case: fooToInt], 42) diff --git a/Tests/CasePathsTests/DeprecatedTests.swift b/Tests/CasePathsTests/DeprecatedTests.swift index 8ffd25a9..ad1d4ea8 100644 --- a/Tests/CasePathsTests/DeprecatedTests.swift +++ b/Tests/CasePathsTests/DeprecatedTests.swift @@ -602,9 +602,6 @@ final class DeprecatedTests: XCTestCase { let actual = path.extract(from: root) XCTAssertEqual(actual, "deadbeef") } - #if swift(>=6) - XCTExpectFailure() - #endif XCTAssertEqual(AnyCasePath(Authentication.authenticated).extract(from: root), "deadbeef") } diff --git a/Tests/CasePathsTests/OptionalPathsTests.swift b/Tests/CasePathsTests/OptionalPathsTests.swift new file mode 100644 index 00000000..108de4c1 --- /dev/null +++ b/Tests/CasePathsTests/OptionalPathsTests.swift @@ -0,0 +1,43 @@ +import CasePaths +import XCTest + +final class OptionalPathsTests: XCTestCase { + func testBasics() { + let path: OptionalKeyPath = \.b.c.d.some.count + + var a = A(b: .c(C(d: D(count: 42)))) + XCTAssertEqual(path.extract(from: a), 42) + + a.b.modify(\.c.d.some.count) { $0 += 1 } + XCTAssertEqual(path.extract(from: a), 43) + + a.b.modify(\.c.d) { $0 = nil } + XCTAssertNil(path.extract(from: a)) + + let partialPath: PartialKeyPath = path + + a = A(b: .c(C(d: D(count: 42)))) + XCTAssertEqual(partialPath.extract(from: a) as? Int, 42) + + a.b.modify(\.c.d) { $0 = nil } + XCTAssertNil(partialPath.extract(from: a)) + + XCTAssertNil(partialPath(123)) + + XCTAssertNil(partialPath("123")) + } + + struct A { + var b: B + } + @CasePathable enum B { + case c(C) + case z + } + struct C { + var d: D? + } + struct D { + var count = 0 + } +} From e9df0d1860f82ec7d8533ee6fba429a4e471d5f5 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 6 Aug 2024 23:13:29 -0700 Subject: [PATCH 2/7] wip --- Sources/CasePaths/CasePathable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CasePaths/CasePathable.swift b/Sources/CasePaths/CasePathable.swift index e5c58adc..20e925d4 100644 --- a/Sources/CasePaths/CasePathable.swift +++ b/Sources/CasePaths/CasePathable.swift @@ -397,7 +397,7 @@ extension CasePathable { /// Attempts to extract the associated value from a root enum using a partial case key path. @_disfavoredOverload public subscript(case keyPath: PartialOptionalKeyPath) -> Any? { - (Case()[keyPath: keyPath] as? any _AnyCase)?._extract(from: self) + keyPath.extract(from: self) } /// Replaces the associated value of a root enum at a case key path when the case matches. From 03dd57412c8b2b08ea8451246a36d9ce63b32138 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 7 Aug 2024 14:30:06 -0700 Subject: [PATCH 3/7] Optional Paths --- Package.swift | 2 +- Package@swift-6.0.swift | 2 +- Sources/CasePaths/AnyOptionalPath.swift | 67 +++++++++++++++++++++++++ Sources/CasePaths/CasePathable.swift | 25 ++++++++- 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 Sources/CasePaths/AnyOptionalPath.swift diff --git a/Package.swift b/Package.swift index c9cd7396..11ab4d45 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0"), + .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2"), ], targets: [ .target( diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 2de0add7..7b05cb09 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -19,7 +19,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), - .package(url: "https://github.com/pointfreeco/swift-macro-testing", branch: "fix-test-trait"), + .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), ], targets: [ diff --git a/Sources/CasePaths/AnyOptionalPath.swift b/Sources/CasePaths/AnyOptionalPath.swift new file mode 100644 index 00000000..59df25f0 --- /dev/null +++ b/Sources/CasePaths/AnyOptionalPath.swift @@ -0,0 +1,67 @@ +import Foundation + +/// A type-erased optional path that supports extracting an optional value from a root, and +/// non-optionally updating a value when present. +/// +/// This type defines key path-like semantics for optional-chaining. +public struct AnyOptionalPath: Sendable { + private let _get: @Sendable (Root) -> Value? + private let _set: @Sendable (inout Root, Value) -> Void + + /// Creates a type-erased optional path from a pair of functions. + /// + /// - Parameters: + /// - get: A function that can optionally fail in extracting a value from a root. + /// - set: A function that always succeeds in updating a value in a root when present. + public init( + get: @escaping @Sendable (Root) -> Value?, + set: @escaping @Sendable (inout Root, Value) -> Void + ) { + self._get = get + self._set = set + } + + /// Creates a type-erased optional path from a type-erased case path. + /// + /// - Parameters: + /// - get: A function that can optionally fail in extracting a value from a root. + /// - set: A function that always succeeds in updating a value in a root when present. + public init(_ casePath: AnyCasePath) { + self.init(get: casePath.extract) { $0 = casePath.embed($1) } + } + + /// Attempts to extract a value from a root. + /// + /// - Parameter root: A root to extract from. + /// - Returns: A value if it can be extracted from the given root, otherwise `nil`. + public func extract(from root: Root) -> Value? { + self._get(root) + } + + /// Returns a root by embedding a value. + /// + /// - Parameters: + /// - root: A root to modify. + /// - value: A value to update in the root when an existing value is present. + public func set(into root: inout Root, _ value: Value) { + self._set(&root, value) + } +} + +extension AnyOptionalPath where Root == Value { + /// The identity optional path. + /// + /// An optional path that: + /// + /// * Given a value to extract, returns the given value. + /// * Given a value to update, replaces the given value. + public init() where Root == Value { + self.init(get: { $0 }, set: { $0 = $1 }) + } +} + +extension AnyOptionalPath: CustomDebugStringConvertible { + public var debugDescription: String { + "AnyOptionalPath<\(typeName(Root.self)), \(typeName(Value.self))>" + } +} diff --git a/Sources/CasePaths/CasePathable.swift b/Sources/CasePaths/CasePathable.swift index 20e925d4..fa317a20 100644 --- a/Sources/CasePaths/CasePathable.swift +++ b/Sources/CasePaths/CasePathable.swift @@ -324,6 +324,12 @@ extension OptionalKeyPath { public func extract(from root: R) -> V? where Root == Case, Value == Case { Case(self).extract(from: root) } + + public func set(into root: inout R, _ value: V) where Root == Case, Value == Case { + var anyRoot = root as Any + Case(self).set(into: &anyRoot, value) + root = anyRoot as! R + } } /// A partially type-erased key path, from a concrete root enum to any resulting value type. @@ -563,7 +569,7 @@ extension CasePathable { } extension AnyCasePath { - /// Creates a type-erased case path for given case key path. + /// Creates a type-erased case path for a given case key path. /// /// - Parameter keyPath: A case key path. public init(_ keyPath: CaseKeyPath) { @@ -575,6 +581,23 @@ extension AnyCasePath { } } +extension AnyOptionalPath { + /// Creates a type-erased optional path for a given optional key path. + /// + /// - Parameter keyPath: An optional key path. + public init(_ keyPath: OptionalKeyPath) { + let `case` = Case(keyPath) + self.init( + get: { `case`.extract(from: $0) }, + set: { + var any = $0 as Any + `case`.set(into: &any, $1) + $0 = any as! Root + } + ) + } +} + extension AnyCasePath where Value: CasePathable { /// Returns a new case path created by appending the case path at the given key path to this one. /// From fee5904db58ab04ebf8f5f06bdb9ba1962803183 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Wed, 7 Aug 2024 14:50:59 -0700 Subject: [PATCH 4/7] wip --- Sources/CasePaths/CasePathable.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CasePaths/CasePathable.swift b/Sources/CasePaths/CasePathable.swift index fa317a20..363bd535 100644 --- a/Sources/CasePaths/CasePathable.swift +++ b/Sources/CasePaths/CasePathable.swift @@ -590,9 +590,9 @@ extension AnyOptionalPath { self.init( get: { `case`.extract(from: $0) }, set: { - var any = $0 as Any - `case`.set(into: &any, $1) - $0 = any as! Root + var anyRoot = $0 as Any + `case`.set(into: &anyRoot, $1) + $0 = anyRoot as! Root } ) } From e047b57ca7b13d459e53e81049563beadcab197a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 23 Dec 2024 13:35:53 -0800 Subject: [PATCH 5/7] Update Package.swift --- Package.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index b3ebea40..b95c107a 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,6 @@ let package = Package( dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2"), ], targets: [ .target( @@ -55,7 +54,7 @@ let package = Package( if ProcessInfo.processInfo.environment["OMIT_MACRO_TESTS"] == nil { package.dependencies.append( - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0") + .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2"), ) package.targets.append( .testTarget( From c207bc82286d9460589aa4e7c75fb065913f226f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 23 Dec 2024 13:36:20 -0800 Subject: [PATCH 6/7] Update Package@swift-6.0.swift --- Package@swift-6.0.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index abe3eb0c..e6b09a0e 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -20,7 +20,6 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"601.0.0-prerelease"), - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), ], targets: [ @@ -56,7 +55,7 @@ let package = Package( if ProcessInfo.processInfo.environment["OMIT_MACRO_TESTS"] == nil { package.dependencies.append( - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.0") + .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2") ) package.targets.append( .testTarget( From 358716ecce6291225a54358fbc4ab18a34c5dfa9 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 23 Dec 2024 13:36:34 -0800 Subject: [PATCH 7/7] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b95c107a..d3ce2459 100644 --- a/Package.swift +++ b/Package.swift @@ -54,7 +54,7 @@ let package = Package( if ProcessInfo.processInfo.environment["OMIT_MACRO_TESTS"] == nil { package.dependencies.append( - .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2"), + .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.5.2") ) package.targets.append( .testTarget(