Skip to content

Instantly share code, notes, and snippets.

@TheBasicMind
Last active February 12, 2019 06:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheBasicMind/5f44546e96a8a1e6fed25ff82459760a to your computer and use it in GitHub Desktop.
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)
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