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

Attempt to make ResilientDecoding work with Swift Concurrency #53

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// swift-tools-version:5.8
Copy link
Collaborator

@dfed dfed Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this swift tools version does not align with the name of this package file.

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
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're intending this file to be swift tools version 5.8, I'd recommend writing this as:

Suggested change
.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
}
.target(
name: "ResilientDecoding",
dependencies: [],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency")
]),
.testTarget(
name: "ResilientDecodingTests",
dependencies: ["ResilientDecoding"],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency")
])
]
)

Note that this is an upcoming rather than experimental feature. In tools version 5.7, you indeed do need to call this experimental IIRC.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With 5.8, this is still considered an experimental feature, so we need to keep it as . enableExperimentalFeature. I will inline it to the targets, however, as you suggest.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welp I may have some code in my own projects to update then

4 changes: 2 additions & 2 deletions Sources/ResilientDecoding/ErrorReporting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

@dfed dfed Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting that this is a breaking change, and we should update the sample dependency management code in the README to match. Podspec needs bumping too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure, I see this being a minor version bump at a minimum.


/**
- parameter novelValue: A value which is believed to be valid but the code does not know how to handle.
*/
public init<T>(novelValue: T) {
public init<T: Sendable>(novelValue: T) {
self.novelValue = novelValue
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/ResilientDecoding/Resilient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
// MARK: - Resilient

@propertyWrapper
public struct Resilient<Value: Decodable>: Decodable {
public struct Resilient<Value: Decodable & Sendable>: 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.
Expand Down Expand Up @@ -77,7 +77,7 @@ public struct Resilient<Value: Decodable>: Decodable {
/**
The outcome of decoding a `Resilient` type
*/
public enum ResilientDecodingOutcome {
public enum ResilientDecodingOutcome: Sendable {
/**
A value was decoded successfully
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import Foundation

extension Resilient {

init<T>(_ results: [Result<T, Error>]) where Value == [T] {
init<T: Sendable>(_ results: [Result<T, Error>]) where Value == [T] {
self.init(results, transform: { $0 })
}

init<T>(_ results: [Result<T, Error>]) where Value == [T]? {
init<T: Sendable>(_ results: [Result<T, Error>]) 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<T>(_ results: [Result<T, Error>], transform: ([T]) -> Value) {
private init<T: Sendable>(_ results: [Result<T, Error>], transform: ([T]) -> Value) {
let elements = results.compactMap { try? $0.get() }
let value = transform(elements)
if elements.count == results.count {
Expand All @@ -41,7 +41,7 @@ extension ResilientDecodingOutcome {
/**
A type representing some number of errors encountered while decoding an array
*/
public struct ArrayDecodingError<Element>: Error {
public struct ArrayDecodingError<Element: Sendable>: Error {
public let results: [Result<Element, Error>]
public var errors: [Error] {
results.compactMap { result in
Expand Down
12 changes: 6 additions & 6 deletions Sources/ResilientDecoding/ResilientArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extension KeyedDecodingContainer {
/**
Decodes a `Resilient` array, omitting elements as errors are encountered.
*/
public func decode<Element>(_ type: Resilient<[Element]>.Type, forKey key: Key) throws -> Resilient<[Element]>
public func decode<Element: Sendable>(_ type: Resilient<[Element]>.Type, forKey key: Key) throws -> Resilient<[Element]>
where
Element: Decodable
{
Expand All @@ -32,23 +32,23 @@ extension KeyedDecodingContainer {
/**
Decodes an optional `Resilient` array. A missing key or `nil` value will silently set the property to `nil`.
*/
public func decode<Element: Decodable>(_ type: Resilient<[Element]?>.Type, forKey key: Key) throws -> Resilient<[Element]?> {
public func decode<Element: Decodable & Sendable>(_ type: Resilient<[Element]?>.Type, forKey key: Key) throws -> Resilient<[Element]?> {
resilientlyDecode(valueForKey: key, fallback: nil) { $0.resilientlyDecodeArray().map { $0 } }
}

}

extension Decoder {

func resilientlyDecodeArray<Element: Decodable>() -> Resilient<[Element]>
func resilientlyDecodeArray<Element: Decodable & Sendable>() -> Resilient<[Element]>
{
resilientlyDecodeArray(of: Element.self, transform: { $0 })
}

/**
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<IntermediateElement: Decodable, Element>(
func resilientlyDecodeArray<IntermediateElement: Decodable, Element: Sendable>(
of intermediateElementType: IntermediateElement.Type,
transform: (IntermediateElement) -> Element) -> Resilient<[Element]>
{
Expand Down Expand Up @@ -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<T: Decodable>(_ type: Resilient<[T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> {
public func decode<T: Decodable & Sendable>(_ type: Resilient<[T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> {
assertionFailure()
return try decode(Resilient<[T]>.self, forKey: key).map { $0 }
}
public func decode<T: Decodable>(_ type: Resilient<[T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> {
public func decode<T: Decodable & Sendable>(_ type: Resilient<[T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> {
assertionFailure()
return try decode(Resilient<[T]>.self, forKey: key).map { $0 }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import Foundation

extension Resilient {

init<T>(_ results: [String: Result<T, Error>]) where Value == [String: T] {
init<T: Sendable>(_ results: [String: Result<T, Error>]) where Value == [String: T] {
self.init(results, transform: { $0 })
}

init<T>(_ results: [String: Result<T, Error>]) where Value == [String: T]? {
init<T: Sendable>(_ results: [String: Result<T, Error>]) 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<T>(_ results: [String: Result<T, Error>], transform: ([String: T]) -> Value) {
private init<T: Sendable>(_ results: [String: Result<T, Error>], transform: ([String: T]) -> Value) {
let dictionary = results.compactMapValues { try? $0.get() }
let value = transform(dictionary)
if dictionary.count == results.count {
Expand All @@ -41,7 +41,7 @@ extension ResilientDecodingOutcome {
/**
A type representing some number of errors encountered while decoding a dictionary
*/
public struct DictionaryDecodingError<Value>: Error {
public struct DictionaryDecodingError<Value: Sendable>: Error {
public let results: [String: Result<Value, Error>]
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.
Expand Down
12 changes: 6 additions & 6 deletions Sources/ResilientDecoding/ResilientDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,31 @@ extension KeyedDecodingContainer {
/**
Decodes a `Resilient` dictionary, omitting values as errors are encountered.
*/
public func decode<Value>(_ type: Resilient<[String: Value]>.Type, forKey key: Key) throws -> Resilient<[String: Value]>
public func decode<Value: Sendable>(_ type: Resilient<[String: Value]>.Type, forKey key: Key) throws -> Resilient<[String: Value]>
{
resilientlyDecode(valueForKey: key, fallback: [:]) { $0.resilientlyDecodeDictionary() }
}

/**
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<Value: Decodable>(_ type: Resilient<[String: Value]?>.Type, forKey key: Key) throws -> Resilient<[String: Value]?> {
public func decode<Value: Decodable & Sendable>(_ type: Resilient<[String: Value]?>.Type, forKey key: Key) throws -> Resilient<[String: Value]?> {
resilientlyDecode(valueForKey: key, fallback: nil) { $0.resilientlyDecodeDictionary().map { $0 } }
}

}

extension Decoder {

func resilientlyDecodeDictionary<Value: Decodable>() -> Resilient<[String: Value]>
func resilientlyDecodeDictionary<Value: Decodable & Sendable>() -> Resilient<[String: Value]>
{
resilientlyDecodeDictionary(of: Value.self, transform: { $0 })
}

/**
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<IntermediateValue: Decodable, Value>(
func resilientlyDecodeDictionary<IntermediateValue: Decodable, Value: Sendable>(
of intermediateValueType: IntermediateValue.Type,
transform: (IntermediateValue) -> Value) -> Resilient<[String: Value]>
{
Expand Down Expand Up @@ -86,11 +86,11 @@ private struct DecodingResultContainer<Success: Decodable>: Decodable {
For the following cases, the user probably meant to use `[String: T]` as the property type.
*/
extension KeyedDecodingContainer {
public func decode<T: Decodable>(_ type: Resilient<[String: T?]>.Type, forKey key: Key) throws -> Resilient<[T?]> {
public func decode<T: Decodable & Sendable>(_ 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<T: Decodable>(_ type: Resilient<[String: T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> {
public func decode<T: Decodable & Sendable>(_ type: Resilient<[String: T?]?>.Type, forKey key: Key) throws -> Resilient<[T?]?> {
assertionFailure()
return try decode(Resilient<[T]>.self, forKey: key).map { $0 }
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ResilientDecoding/ResilientRawRepresentable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down