Skip to content

Instantly share code, notes, and snippets.

@krzysztofzablocki
Created May 26, 2021 09:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krzysztofzablocki/35da123a95032c625a31eb7f95fb7575 to your computer and use it in GitHub Desktop.
Save krzysztofzablocki/35da123a95032c625a31eb7f95fb7575 to your computer and use it in GitHub Desktop.
Extension of http://merowing.info/2020/06/adding-support-for-versioning-and-migration-to-your-codable-models./ that supports a situation when you shipped app without versioning and want to add it now
import Foundation
public protocol VersionType: CaseIterable, Codable, Comparable, RawRepresentable {}
public extension VersionType where RawValue: Comparable {
static func < (a: Self, b: Self) -> Bool {
return a.rawValue < b.rawValue
}
}
public protocol Versionable: Codable {
associatedtype Version: VersionType
typealias MigrationClosure = (inout [String: Any]) -> Void
static func migrate(to: Version) -> Migration
static var version: Version { get }
static var fallbackDecoding: ((Decoder) throws -> Self)? { get }
/// Persisted Version of this type
var version: Version { get }
}
public extension Versionable {
static var version: Version {
let allCases = Version.allCases
return allCases[allCases.index(allCases.endIndex, offsetBy: -1)]
}
}
public enum Migration {
case none
case migrate(Versionable.MigrationClosure)
func callAsFunction(_ payload: inout [String: Any]) {
switch self {
case .none:
return
case let .migrate(closure):
closure(&payload)
}
}
}
struct VersionContainer<Version: VersionType>: Codable {
var version: Version
}
import Foundation
struct TryingToDecodeNewerVersionThanTheAppSupports: Error {
}
public struct VersionableContainer<Type: Versionable>: Codable {
public let instance: Type
enum CodingKeys: CodingKey {
case version
case instance
}
public init(instance: Type) {
self.instance = instance
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(instance.version, forKey: .version)
let data = try JSONEncoder().encode(instance)
try container.encode(data, forKey: .instance)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let freeDecoder = JSONDecoder()
var encodedVersion: Type.Version
var data: Data
do {
encodedVersion = try container.decode(Type.Version.self, forKey: .version)
data = try container.decode(Data.self, forKey: .instance)
if encodedVersion == Type.version {
instance = try freeDecoder.decode(Type.self, from: data)
return
}
} catch {
// if this is not valid versionable format it means that we had version we couldn't decode
// that should only happen if it was newer than current app version
guard !container.contains(.version) || !container.contains(.instance) else {
throw TryingToDecodeNewerVersionThanTheAppSupports()
}
guard let fallbackFactory = Type.fallbackDecoding, Type.Version.allCases.count > 0 else {
throw error
}
// fallback to instance and assume it's first version that existed
encodedVersion = Type.Version.allCases.first!
let type = try fallbackFactory(decoder)
// if only one version exist no migration is needed
if Type.Version.allCases.count == 1 {
instance = type
return
}
data = try JSONEncoder().encode(type)
}
var payload = try (try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]).unwrap(message: "Unable to deserialize json")
#if DEBUG
let originalList = Type.Version.allCases
let sorted = originalList.sorted(by: { $0 < $1 })
assert(originalList.map { $0 } == sorted.map { $0 }, "\(Type.self) Versions should be sorted by their comparable order")
#endif
Type
.Version
.allCases
.filter { encodedVersion < $0 }
.forEach {
Type.migrate(to: $0)(&payload)
payload["version"] = $0.rawValue
}
instance = try freeDecoder.decode(Type.self, from: try JSONSerialization.data(withJSONObject: payload as Any, options: []))
}
}
extension VersionableContainer: Equatable where Type: Equatable {
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment