Last active
February 12, 2019 06:52
-
-
Save TheBasicMind/5f44546e96a8a1e6fed25ff82459760a to your computer and use it in GitHub Desktop.
Swift 4 network data wrapper for checking network (cloud) data conforms to an API version (uses Generics as appropriate)
This file contains 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 UIKit | |
/** | |
When encoding objects to JSON for use with | |
a cloud service such as iCloud, Swift's | |
built in coder will check if the key | |
paths of the object being decoded to, match | |
the keys paths of the object that was used | |
to encode in the first place. | |
container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey | |
However a matching set of key paths and types is | |
insufficient for ensuring the API of the decoded | |
object matches the API of the encoded object | |
(e.g. it may only be value constraints that | |
change). Additionally it is a good idea to | |
maintain API versions such that on decoding, it | |
is easy to know which version of the API a \ | |
previously coded object was encoded using. This | |
Gist provides a simple object wrapper for | |
maintaining API versions. Simply use | |
encodeApiVersion() and decodeApiVersion(:from:) | |
in place of encode() and decode(:from:) | |
*/ | |
enum APIError: Error { | |
case doesNotMatchVersion(currentVersion: Int, storedVersion: Int) | |
} | |
// Change this value when a new version | |
// of your encoding and decoding API is | |
// published. | |
let apiVersionNo = 1 | |
struct APIData<T: Codable>: Codable { | |
let apiVersion: Int | |
let apiData: T | |
enum CodingKeys: String, CodingKey { | |
case apiVersion = "apiVersion" | |
case apiData = "apiData" | |
} | |
init(versionNumber: Int, apiData: T) { | |
self.apiVersion = versionNumber | |
self.apiData = apiData | |
} | |
public init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let apiVersion = try container.decode(Int.self, forKey: .apiVersion) | |
if apiVersion != apiVersionNo { throw APIError.doesNotMatchVersion(currentVersion: apiVersionNo, storedVersion: apiVersion) } | |
let apiData = try container.decode(T.self, forKey: .apiData) | |
self.apiVersion = apiVersion | |
self.apiData = apiData | |
} | |
} | |
extension JSONEncoder { | |
open func encodeApiVersion<T>(_ value: T) throws -> Data where T : Codable { | |
let wrappedData = APIData(versionNumber: apiVersionNo, apiData: value) | |
let data = try self.encode(wrappedData) | |
return data | |
} | |
} | |
extension JSONDecoder { | |
open func decodeApiVersion<T>(_ type: T.Type, from data: Data) throws -> T where T : Codable { | |
let wrappedData: APIData = try decode(APIData<T>.self, from: data) | |
let data = wrappedData.apiData | |
return data | |
} | |
} | |
/** | |
Usage example | |
*/ | |
struct SomeStuff: Codable { | |
let aWord: String | |
init(_ word:String) { | |
aWord = word | |
} | |
} | |
let encoder = JSONEncoder() | |
let someStuff = SomeStuff("Hello") | |
let encodedWrappedData = try encoder.encodeApiVersion(someStuff) | |
let decoder = JSONDecoder() | |
var decodedData: SomeStuff | |
do { | |
decodedData = try decoder.decodeApiVersion(SomeStuff.self, from: encodedWrappedData) | |
let word = decodedData.aWord | |
print(word) | |
} catch APIError.doesNotMatchVersion(let currentVersion, let storedVersion) { | |
// Include code here for dealing with old versions | |
// Either decoding to an object meeting the old API | |
// and migrating / upgrading the data and / present- | |
// ing appropriate error messages, or whatever else. | |
print("version mismatch error currentVersion = \(currentVersion), storedVersion \(storedVersion)") | |
} catch { | |
// Deal with other possible decoding errors | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment