Created
May 26, 2021 09:16
-
-
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
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 | |
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 | |
} |
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 | |
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