Created
July 18, 2025 20:08
-
-
Save CBeloch/3344fe59aebdd823af5921ba4967f66b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// MARK: - LossySequence | |
/// LossySequence can be used as a replacement for a regular array, | |
/// where the given `Element` tends to fail decoding (e.g. because of an added enum type in the future etc). | |
/// | |
/// Failed decodings will be skipped from the returned sequence. Errors can be found in the `errors` property | |
/// | |
/// ```swift | |
/// struct NewsReponse { | |
/// @LossySequence let news: [News] | |
/// } | |
/// | |
/// // decode data | |
/// | |
/// response.news // only valid entities | |
/// response.$news.errors // contains all decoding errors | |
/// ``` | |
@propertyWrapper | |
public struct LossySequence<Element> { | |
// MARK: Properties | |
/// The wrapped value | |
public var wrappedValue: [Element] | |
/// List of errors | |
public var errors: [Swift.Error] = [] | |
/// Current index | |
private var currentIndex = 0 | |
/// projected value to make this type accessible by $ prefix | |
public var projectedValue: LossySequence<Element> { return self } | |
// MARK: Initializers | |
/// Initialize a new ``LossySequence`` | |
/// - Parameters: | |
/// - wrappedValue: The wrapped value | |
public init( | |
wrappedValue: [Element] | |
) { | |
self.wrappedValue = wrappedValue | |
} | |
} | |
// MARK: - Sequence | |
extension LossySequence: Sequence, IteratorProtocol { | |
public mutating func next() -> Element? { | |
// Safely grab the element at the current index | |
let model = self.wrappedValue.indices.contains(currentIndex) | |
? self.wrappedValue[currentIndex] | |
: nil | |
// advance current index | |
self.currentIndex += 1 | |
return model | |
} | |
} | |
// MARK: - Decodable | |
extension LossySequence: Decodable where Element: Decodable { | |
public init(from decoder: Decoder) throws { | |
var container = try decoder.unkeyedContainer() | |
// Ensure we have a count of contained elements in the container | |
guard let count = container.count else { | |
self.wrappedValue = .init() | |
return | |
} | |
var errors: [Swift.Error] = .init() | |
self.wrappedValue = try (0..<count).compactMap { _ in | |
do { | |
// Try to decode current index to Element | |
return try container.decode(Element.self) | |
} catch { | |
// Decoding failed, skip Element and advance | |
errors.append(error) | |
// Fallback Decodable to advance inside the container | |
_ = try container.decode(NothingCodable.self) | |
return nil | |
} | |
} | |
self.errors = errors | |
} | |
} | |
// MARK: - Encodable | |
extension LossySequence: Encodable where Element: Encodable { | |
public func encode(to encoder: Encoder) throws { | |
// Encode wrapped value | |
try self.wrappedValue.encode(to: encoder) | |
} | |
} | |
// MARK: - Equatable | |
extension LossySequence: Equatable where Element: Equatable { | |
public static func == ( | |
lhs: LossySequence<Element>, | |
rhs: LossySequence<Element> | |
) -> Bool { | |
lhs.wrappedValue == rhs.wrappedValue | |
} | |
} | |
// MARK: - Hashable | |
extension LossySequence: Hashable where Element: Hashable { | |
public func hash(into hasher: inout Hasher) { | |
self.wrappedValue.hash(into: &hasher) | |
} | |
} | |
// MARK: - Codable | |
/// An empty object that will always succeed to decode | |
private struct NothingCodable: Codable {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment