Skip to content

Instantly share code, notes, and snippets.

@manmal
Last active June 10, 2020 21:42
Show Gist options
  • Save manmal/725eb2c731181ea3cd2e67b023dc7e97 to your computer and use it in GitHub Desktop.
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 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