Last active
June 10, 2020 21:42
-
-
Save manmal/725eb2c731181ea3cd2e67b023dc7e97 to your computer and use it in GitHub Desktop.
Variation on @merowing_'s Versionable: https://github.com/krzysztofzablocki/Versionable - adds enumerated versions
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
// This is a variation on @merowing_'s Versionable: https://github.com/krzysztofzablocki/Versionable | |
// It's an attempt to add enumerated versions. Together with CaseIterable conformance, this makes | |
// it harder to "forget" to add migrations for version bumps. Not super happy with the namings, but it | |
// should be enough to get the idea. | |
// | |
// Modifications made: | |
// - New protocol `VersionType` | |
// - Versionable.Version is `VersionType` | |
// - Replaced Versionable.migrations with Versionable.migrate(to:) | |
// - decode() fetches migrations via Versionable.Version.allCases and Versionable.migrate(to:) | |
// - Removed Versionable.Migration | |
public protocol VersionType: CaseIterable, Codable, Comparable, RawRepresentable, CustomDebugStringConvertible {} | |
extension VersionType where RawValue == String { | |
var debugDescription: String { | |
rawValue | |
} | |
} | |
extension VersionType where RawValue == Int { | |
var debugDescription: String { | |
String(rawValue) | |
} | |
} | |
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 | |
/// Version of this type | |
static var version: Version { get } | |
static func migrate(to: Version) -> Migration | |
/// Persisted Version of this type | |
var version: Version { get } | |
} | |
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 | |
} | |
public final class VersionableDecoder { | |
public init() { | |
} | |
public func decode<T>(_ data: Data, type: T.Type) throws -> T where T: Versionable { | |
let decoder = JSONDecoder() | |
let serializedVersion = try decoder.decode(VersionContainer<T.Version>.self, from: data) | |
if serializedVersion.version == type.version { | |
return try decoder.decode(T.self, from: data) | |
} | |
var payload = try require(try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) | |
#if DEBUG | |
let originalList = type.migrations | |
let sorted = type.migrations.sorted(by: { $0.to < $1.to }) | |
assert(originalList.map { $0.to } == sorted.map { $0.to }, "\(type) migrations should be sorted by version") | |
#endif | |
type.Version | |
.allCases | |
.filter { serializedVersion.version < $0 } | |
.forEach { | |
type.migrate(to: $0)(&payload) | |
payload["version"] = $0.rawValue | |
} | |
let data = try JSONSerialization.data(withJSONObject: payload as Any, options: []) | |
return try decoder.decode(T.self, from: data) | |
} | |
} | |
internal enum RequireError: Error { | |
case isNil | |
} | |
/// Lifts optional or throws requested error if its nil | |
internal func require<T>(_ optional: T?, or error: Error = RequireError.isNil) throws -> T { | |
guard let value = optional else { | |
throw error | |
} | |
return value | |
} | |
private struct Object { | |
let text: String | |
let number: Int | |
let version: Version = Self.version | |
} | |
extension Object: Versionable { | |
enum _Version: Int, VersionType { | |
case v1 = 1 | |
case v2 = 2 | |
case v3 = 3 | |
} | |
typealias Version = _Version | |
static let version: _Version = .v3 | |
static func migrate(to: _Version) -> Migration { | |
switch to { | |
case .v1: | |
return .none | |
case .v2: | |
return .migrate { payload in | |
payload["text"] = "defaultText" | |
} | |
case .v3: | |
return .migrate { payload in | |
payload["number"] = (payload["text"] as? String) == "defaultText" ? 1 : 200 | |
} | |
} | |
} | |
} | |
let exampleJson = "{\"version\": 1}".data(using: .utf8)! | |
fileprivate let model = try! VersionableDecoder().decode(exampleJson, type: Object.self) | |
print(model) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment