From 4c5342203573b60ee76d9c003e25d03001a91fe7 Mon Sep 17 00:00:00 2001 From: Bradley David Bergeron Date: Wed, 8 Nov 2023 19:20:53 -0500 Subject: [PATCH 1/3] Swift concurrency improvements --- Package@swift-5.7.swift | 31 +++++++++++++++++++ .../ResilientDecoding/ErrorReporting.swift | 4 +-- Sources/ResilientDecoding/Resilient.swift | 4 +-- .../ResilientArray+DecodingOutcome.swift | 8 ++--- .../ResilientDecoding/ResilientArray.swift | 12 +++---- .../ResilientDictionary+DecodingOutcome.swift | 8 ++--- .../ResilientDictionary.swift | 12 +++---- .../ResilientRawRepresentable.swift | 2 +- 8 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 Package@swift-5.7.swift diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift new file mode 100644 index 0000000..b5b2eea --- /dev/null +++ b/Package@swift-5.7.swift @@ -0,0 +1,31 @@ +// swift-tools-version:5.8 +import PackageDescription + +let package = Package( + name: "ResilientDecoding", + platforms: [ + .iOS(.v12), + .tvOS(.v12), + .watchOS(.v5), + .macOS(.v10_14), + ], + products: [ + .library( + name: "ResilientDecoding", + targets: ["ResilientDecoding"]), + ], + targets: [ + .target( + name: "ResilientDecoding", + dependencies: []), + .testTarget( + name: "ResilientDecodingTests", + dependencies: ["ResilientDecoding"]), + ] +) + +for target in package.targets { + var settings = target.swiftSettings ?? [] + settings.append(.enableExperimentalFeature("StrictConcurrency")) + target.swiftSettings = settings +} diff --git a/Sources/ResilientDecoding/ErrorReporting.swift b/Sources/ResilientDecoding/ErrorReporting.swift index e436829..dc868c1 100644 --- a/Sources/ResilientDecoding/ErrorReporting.swift +++ b/Sources/ResilientDecoding/ErrorReporting.swift @@ -234,12 +234,12 @@ public struct UnknownNovelValueError: Error { /** The raw value for which `init(rawValue:)` returned `nil`. */ - public let novelValue: Any + public let novelValue: Sendable /** - parameter novelValue: A value which is believed to be valid but the code does not know how to handle. */ - public init(novelValue: T) { + public init(novelValue: T) { self.novelValue = novelValue } diff --git a/Sources/ResilientDecoding/Resilient.swift b/Sources/ResilientDecoding/Resilient.swift index 71bfbd1..0b63e2f 100644 --- a/Sources/ResilientDecoding/Resilient.swift +++ b/Sources/ResilientDecoding/Resilient.swift @@ -6,7 +6,7 @@ import Foundation // MARK: - Resilient @propertyWrapper -public struct Resilient: Decodable { +public struct Resilient: Decodable, Sendable { /** If this initializer is called it is likely because a property was marked as `Resilient` despite the underlying type not supporting resilient decoding. For instance, a developer may write `@Resilient var numberOfThings: Int`, but since `Int` doesn't provide a mechanism for recovering from a decoding failure (like `Array`s and `Optional`s do) wrapping the property in `Resilient` does nothing. @@ -77,7 +77,7 @@ public struct Resilient: Decodable { /** The outcome of decoding a `Resilient` type */ -public enum ResilientDecodingOutcome { +public enum ResilientDecodingOutcome: Sendable { /** A value was decoded successfully */ diff --git a/Sources/ResilientDecoding/ResilientArray+DecodingOutcome.swift b/Sources/ResilientDecoding/ResilientArray+DecodingOutcome.swift index 460dbd9..77e3000 100644 --- a/Sources/ResilientDecoding/ResilientArray+DecodingOutcome.swift +++ b/Sources/ResilientDecoding/ResilientArray+DecodingOutcome.swift @@ -5,18 +5,18 @@ import Foundation extension Resilient { - init(_ results: [Result]) where Value == [T] { + init(_ results: [Result]) where Value == [T] { self.init(results, transform: { $0 }) } - init(_ results: [Result]) where Value == [T]? { + init(_ results: [Result]) where Value == [T]? { self.init(results, transform: { $0 }) } /** - parameter transform: While the two lines above both say `{ $0 }` they are actually different because the first one is of type `([T]) -> [T]` and the second is of type `([T]) -> [T]?`. */ - private init(_ results: [Result], transform: ([T]) -> Value) { + private init(_ results: [Result], transform: ([T]) -> Value) { let elements = results.compactMap { try? $0.get() } let value = transform(elements) if elements.count == results.count { @@ -41,7 +41,7 @@ extension ResilientDecodingOutcome { /** A type representing some number of errors encountered while decoding an array */ - public struct ArrayDecodingError: Error { + public struct ArrayDecodingError: Error { public let results: [Result] public var errors: [Error] { results.compactMap { result in diff --git a/Sources/ResilientDecoding/ResilientArray.swift b/Sources/ResilientDecoding/ResilientArray.swift index a72507e..9ba7e49 100644 --- a/Sources/ResilientDecoding/ResilientArray.swift +++ b/Sources/ResilientDecoding/ResilientArray.swift @@ -22,7 +22,7 @@ extension KeyedDecodingContainer { /** Decodes a `Resilient` array, omitting elements as errors are encountered. */ - public func decode(_ type: Resilient<[Element]>.Type, forKey key: Key) throws -> Resilient<[Element]> + public func decode(_ type: Resilient<[Element]>.Type, forKey key: Key) throws -> Resilient<[Element]> where Element: Decodable { @@ -32,7 +32,7 @@ extension KeyedDecodingContainer { /** Decodes an optional `Resilient` array. A missing key or `nil` value will silently set the property to `nil`. */ - public func decode(_ type: Resilient<[Element]?>.Type, forKey key: Key) throws -> Resilient<[Element]?> { + public func decode(_ type: Resilient<[Element]?>.Type, forKey key: Key) throws -> Resilient<[Element]?> { resilientlyDecode(valueForKey: key, fallback: nil) { $0.resilientlyDecodeArray().map { $0 } } } @@ -40,7 +40,7 @@ extension KeyedDecodingContainer { extension Decoder { - func resilientlyDecodeArray() -> Resilient<[Element]> + func resilientlyDecodeArray() -> Resilient<[Element]> { resilientlyDecodeArray(of: Element.self, transform: { $0 }) } @@ -48,7 +48,7 @@ extension Decoder { /** We can't just use `map` because the transform needs to happen _before_ we wrap the value in `Resilient` so that that the element type of `ArrayDecodingError` is correct. */ - func resilientlyDecodeArray( + func resilientlyDecodeArray( of intermediateElementType: IntermediateElement.Type, transform: (IntermediateElement) -> Element) -> Resilient<[Element]> { @@ -83,11 +83,11 @@ extension Decoder { For the following cases, the user probably meant to use `[T]` as the property type. */ extension KeyedDecodingContainer { - public func decode(_ type: Resilient<[T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> { + public func decode(_ type: Resilient<[T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> { assertionFailure() return try decode(Resilient<[T]>.self, forKey: key).map { $0 } } - public func decode(_ type: Resilient<[T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> { + public func decode(_ type: Resilient<[T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> { assertionFailure() return try decode(Resilient<[T]>.self, forKey: key).map { $0 } } diff --git a/Sources/ResilientDecoding/ResilientDictionary+DecodingOutcome.swift b/Sources/ResilientDecoding/ResilientDictionary+DecodingOutcome.swift index de63011..495950e 100644 --- a/Sources/ResilientDecoding/ResilientDictionary+DecodingOutcome.swift +++ b/Sources/ResilientDecoding/ResilientDictionary+DecodingOutcome.swift @@ -5,18 +5,18 @@ import Foundation extension Resilient { - init(_ results: [String: Result]) where Value == [String: T] { + init(_ results: [String: Result]) where Value == [String: T] { self.init(results, transform: { $0 }) } - init(_ results: [String: Result]) where Value == [String: T]? { + init(_ results: [String: Result]) where Value == [String: T]? { self.init(results, transform: { $0 }) } /** - parameter transform: While the two lines above both say `{ $0 }` they are actually different because the first one is of type `([String: T]) -> [String: T]` and the second is of type `([String: T]) -> [String: T]?`. */ - private init(_ results: [String: Result], transform: ([String: T]) -> Value) { + private init(_ results: [String: Result], transform: ([String: T]) -> Value) { let dictionary = results.compactMapValues { try? $0.get() } let value = transform(dictionary) if dictionary.count == results.count { @@ -41,7 +41,7 @@ extension ResilientDecodingOutcome { /** A type representing some number of errors encountered while decoding a dictionary */ - public struct DictionaryDecodingError: Error { + public struct DictionaryDecodingError: Error { public let results: [String: Result] public var errors: [Error] { /// It is currently impossible to have both a `topLevelError` and `results` at the same time, but this code is simpler than having an `enum` nested in this type. diff --git a/Sources/ResilientDecoding/ResilientDictionary.swift b/Sources/ResilientDecoding/ResilientDictionary.swift index 28b91ea..bd700e3 100644 --- a/Sources/ResilientDecoding/ResilientDictionary.swift +++ b/Sources/ResilientDecoding/ResilientDictionary.swift @@ -20,7 +20,7 @@ extension KeyedDecodingContainer { /** Decodes a `Resilient` dictionary, omitting values as errors are encountered. */ - public func decode(_ type: Resilient<[String: Value]>.Type, forKey key: Key) throws -> Resilient<[String: Value]> + public func decode(_ type: Resilient<[String: Value]>.Type, forKey key: Key) throws -> Resilient<[String: Value]> { resilientlyDecode(valueForKey: key, fallback: [:]) { $0.resilientlyDecodeDictionary() } } @@ -28,7 +28,7 @@ extension KeyedDecodingContainer { /** Decodes an optional `Resilient` dictionary. If the field is missing or the value is `nil` the decoded property will also be `nil`. */ - public func decode(_ type: Resilient<[String: Value]?>.Type, forKey key: Key) throws -> Resilient<[String: Value]?> { + public func decode(_ type: Resilient<[String: Value]?>.Type, forKey key: Key) throws -> Resilient<[String: Value]?> { resilientlyDecode(valueForKey: key, fallback: nil) { $0.resilientlyDecodeDictionary().map { $0 } } } @@ -36,7 +36,7 @@ extension KeyedDecodingContainer { extension Decoder { - func resilientlyDecodeDictionary() -> Resilient<[String: Value]> + func resilientlyDecodeDictionary() -> Resilient<[String: Value]> { resilientlyDecodeDictionary(of: Value.self, transform: { $0 }) } @@ -44,7 +44,7 @@ extension Decoder { /** We can't just use `map` because the transform needs to happen _before_ we wrap the value in `Resilient` so that that the value type of `DictionaryDecodingError` is correct. */ - func resilientlyDecodeDictionary( + func resilientlyDecodeDictionary( of intermediateValueType: IntermediateValue.Type, transform: (IntermediateValue) -> Value) -> Resilient<[String: Value]> { @@ -86,11 +86,11 @@ private struct DecodingResultContainer: Decodable { For the following cases, the user probably meant to use `[String: T]` as the property type. */ extension KeyedDecodingContainer { - public func decode(_ type: Resilient<[String: T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> { + public func decode(_ type: Resilient<[String: T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> { assertionFailure() return try decode(Resilient<[T]>.self, forKey: key).map { $0 } } - public func decode(_ type: Resilient<[String: T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> { + public func decode(_ type: Resilient<[String: T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> { assertionFailure() return try decode(Resilient<[T]>.self, forKey: key).map { $0 } } diff --git a/Sources/ResilientDecoding/ResilientRawRepresentable.swift b/Sources/ResilientDecoding/ResilientRawRepresentable.swift index dfc62a9..4f2b398 100644 --- a/Sources/ResilientDecoding/ResilientRawRepresentable.swift +++ b/Sources/ResilientDecoding/ResilientRawRepresentable.swift @@ -16,7 +16,7 @@ import Foundation ``` then any struct with a `Resilient` property with that type (for instance `@Resilient var myEnum: MyEnum`) will be set to `.unknown` in the event of a decoding failure. */ -public protocol ResilientRawRepresentable: Decodable, RawRepresentable where RawValue: Decodable { +public protocol ResilientRawRepresentable: Decodable, Sendable, RawRepresentable where RawValue: Decodable & Sendable { associatedtype DecodingFallback From 71be09b7e56b318512371ad17fe301f85c776f92 Mon Sep 17 00:00:00 2001 From: Bradley David Bergeron Date: Thu, 9 Nov 2023 10:08:20 -0500 Subject: [PATCH 2/3] Update tests to verify Sendable --- ...ge@swift-5.7.swift => Package@swift-5.8.swift | 16 ++++++++-------- Tests/ResilientDecodingTests/BugTests.swift | 2 +- .../ResilientArrayTests.swift | 2 +- .../ResilientDecodingErrorReporterTests.swift | 2 +- .../ResilientDictionaryTests.swift | 2 +- .../ResilientOptionalTests.swift | 2 +- .../ResilientRawRepresentableArrayTests.swift | 2 +- ...esilientRawRepresentableDictionaryTests.swift | 2 +- .../ResilientRawRepresentableTests.swift | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) rename Package@swift-5.7.swift => Package@swift-5.8.swift (63%) diff --git a/Package@swift-5.7.swift b/Package@swift-5.8.swift similarity index 63% rename from Package@swift-5.7.swift rename to Package@swift-5.8.swift index b5b2eea..f38742e 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.8.swift @@ -17,15 +17,15 @@ let package = Package( targets: [ .target( name: "ResilientDecoding", - dependencies: []), + dependencies: [], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ]), .testTarget( name: "ResilientDecodingTests", - dependencies: ["ResilientDecoding"]), + dependencies: ["ResilientDecoding"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ]), ] ) - -for target in package.targets { - var settings = target.swiftSettings ?? [] - settings.append(.enableExperimentalFeature("StrictConcurrency")) - target.swiftSettings = settings -} diff --git a/Tests/ResilientDecodingTests/BugTests.swift b/Tests/ResilientDecodingTests/BugTests.swift index 74493f8..281742d 100644 --- a/Tests/ResilientDecodingTests/BugTests.swift +++ b/Tests/ResilientDecodingTests/BugTests.swift @@ -20,7 +20,7 @@ final class BugTests: XCTestCase { let rawValue: URL static var isFrozen: Bool { true } } - struct Mock: Decodable { + struct Mock: Decodable, Sendable { @Resilient var optional: URL? @Resilient var array: [URL] @Resilient var dictionary: [String: URL] diff --git a/Tests/ResilientDecodingTests/ResilientArrayTests.swift b/Tests/ResilientDecodingTests/ResilientArrayTests.swift index 4847146..f176bec 100644 --- a/Tests/ResilientDecodingTests/ResilientArrayTests.swift +++ b/Tests/ResilientDecodingTests/ResilientArrayTests.swift @@ -4,7 +4,7 @@ import ResilientDecoding import XCTest -private struct ResilientArrayWrapper: Decodable { +private struct ResilientArrayWrapper: Decodable, Sendable { @Resilient var resilientArray: [Int] @Resilient var optionalResilientArray: [Int]? } diff --git a/Tests/ResilientDecodingTests/ResilientDecodingErrorReporterTests.swift b/Tests/ResilientDecodingTests/ResilientDecodingErrorReporterTests.swift index 2153916..8751e64 100644 --- a/Tests/ResilientDecodingTests/ResilientDecodingErrorReporterTests.swift +++ b/Tests/ResilientDecodingTests/ResilientDecodingErrorReporterTests.swift @@ -4,7 +4,7 @@ import ResilientDecoding import XCTest -private struct ResilientArrayWrapper: Decodable { +private struct ResilientArrayWrapper: Decodable, Sendable { @Resilient var resilientArray: [Int] @Resilient var resilientEnum: ResilientEnum? } diff --git a/Tests/ResilientDecodingTests/ResilientDictionaryTests.swift b/Tests/ResilientDecodingTests/ResilientDictionaryTests.swift index 876d656..65a7f57 100644 --- a/Tests/ResilientDecodingTests/ResilientDictionaryTests.swift +++ b/Tests/ResilientDecodingTests/ResilientDictionaryTests.swift @@ -10,7 +10,7 @@ import ResilientDecoding #endif import XCTest -private struct ResilientDictionaryWrapper: Decodable { +private struct ResilientDictionaryWrapper: Decodable, Sendable { @Resilient var resilientDictionary: [String: Int] @Resilient var optionalResilientDictionary: [String: Int]? } diff --git a/Tests/ResilientDecodingTests/ResilientOptionalTests.swift b/Tests/ResilientDecodingTests/ResilientOptionalTests.swift index 121a69a..e1be44d 100644 --- a/Tests/ResilientDecodingTests/ResilientOptionalTests.swift +++ b/Tests/ResilientDecodingTests/ResilientOptionalTests.swift @@ -4,7 +4,7 @@ import ResilientDecoding import XCTest -private struct ResilientOptionalWrapper: Decodable { +private struct ResilientOptionalWrapper: Decodable, Sendable { @Resilient var resilientOptional: Int? } diff --git a/Tests/ResilientDecodingTests/ResilientRawRepresentableArrayTests.swift b/Tests/ResilientDecodingTests/ResilientRawRepresentableArrayTests.swift index a867817..70d7541 100644 --- a/Tests/ResilientDecodingTests/ResilientRawRepresentableArrayTests.swift +++ b/Tests/ResilientDecodingTests/ResilientRawRepresentableArrayTests.swift @@ -4,7 +4,7 @@ import ResilientDecoding import XCTest -private struct ResilientRawRepresentableArrayWrapper: Decodable { +private struct ResilientRawRepresentableArrayWrapper: Decodable, Sendable { @Resilient var resilientArray: [ResilientEnum] @Resilient var optionalResilientArray: [ResilientEnum]? @Resilient var resilientArrayOfFrozenType: [ResilientFrozenEnum] diff --git a/Tests/ResilientDecodingTests/ResilientRawRepresentableDictionaryTests.swift b/Tests/ResilientDecodingTests/ResilientRawRepresentableDictionaryTests.swift index e28f500..a94c3c6 100644 --- a/Tests/ResilientDecodingTests/ResilientRawRepresentableDictionaryTests.swift +++ b/Tests/ResilientDecodingTests/ResilientRawRepresentableDictionaryTests.swift @@ -4,7 +4,7 @@ import ResilientDecoding import XCTest -private struct ResilientRawRepresentableDictionaryWrapper: Decodable { +private struct ResilientRawRepresentableDictionaryWrapper: Decodable, Sendable { @Resilient var resilientDictionary: [String: ResilientEnum] @Resilient var optionalResilientDictionary: [String: ResilientEnum]? @Resilient var resilientDictionaryOfFrozenType: [String: ResilientFrozenEnum] diff --git a/Tests/ResilientDecodingTests/ResilientRawRepresentableTests.swift b/Tests/ResilientDecodingTests/ResilientRawRepresentableTests.swift index 0665f2f..8f12030 100644 --- a/Tests/ResilientDecodingTests/ResilientRawRepresentableTests.swift +++ b/Tests/ResilientDecodingTests/ResilientRawRepresentableTests.swift @@ -4,7 +4,7 @@ import XCTest import ResilientDecoding -private struct ResilientRawRepresentableEnumWrapper: Decodable { +private struct ResilientRawRepresentableEnumWrapper: Decodable, Sendable { @Resilient var resilientEnumWithFallback: ResilientEnumWithFallback @Resilient var resilientFrozenEnumWithFallback: ResilientFrozenEnumWithFallback @Resilient var optionalResilientEnum: ResilientEnum? From d4e5e864ac6ebf0eaff53e9e37176eed65ce892b Mon Sep 17 00:00:00 2001 From: Bradley David Bergeron Date: Thu, 9 Nov 2023 10:09:55 -0500 Subject: [PATCH 3/3] Run tests in parallel, exclude tests from coverage --- .../xcschemes/ResilientDecoding.xcscheme | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ResilientDecoding.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ResilientDecoding.xcscheme index b4c5b51..33eb1be 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/ResilientDecoding.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ResilientDecoding.xcscheme @@ -41,10 +41,21 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" - codeCoverageEnabled = "YES"> + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES"> + + + + + skipped = "NO" + parallelizable = "YES">