Created
October 4, 2018 13:12
-
-
Save guidomb/4f4a759c9810e5b999b579c28c84e15d to your computer and use it in GitHub Desktop.
A Firebase's Firestore document Swift decoder implementation
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
// AnyCodingKey.swift | |
import Foundation | |
struct AnyCodingKey: CodingKey, Equatable { | |
var stringValue: String | |
var intValue: Int? | |
init?(stringValue: String) { | |
self.stringValue = stringValue | |
self.intValue = nil | |
} | |
init?(intValue: Int) { | |
self.stringValue = "\(intValue)" | |
self.intValue = intValue | |
} | |
init<Key>(_ base: Key) where Key : CodingKey { | |
if let intValue = base.intValue { | |
self.init(intValue: intValue)! | |
} else { | |
self.init(stringValue: base.stringValue)! | |
} | |
} | |
} | |
extension AnyCodingKey: Hashable { | |
var hashValue: Int { | |
return self.intValue?.hashValue ?? self.stringValue.hashValue | |
} | |
} | |
// FirestoreDecoder.swift | |
import Foundation | |
final public class FirestoreDecoder { | |
public init() { } | |
public func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable { | |
let document = try JSONDecoder().decode(FirestoreDocument.self, from: data) | |
let decoder = _FirestoreDecoder(document: document) | |
return try T(from: decoder) | |
} | |
} | |
final class _FirestoreDecoder { | |
var codingPath: [CodingKey] = [] | |
var userInfo: [CodingUserInfoKey : Any] = [:] | |
var container: FirestoreDecodingContainer? | |
fileprivate let document: FirestoreDocument | |
init(document: FirestoreDocument) { | |
self.document = document | |
} | |
} | |
extension _FirestoreDecoder: Decoder { | |
fileprivate func assertCanCreateContainer() { | |
precondition(self.container == nil) | |
} | |
func container<Key>(keyedBy type: Key.Type) -> KeyedDecodingContainer<Key> where Key : CodingKey { | |
assertCanCreateContainer() | |
let container = KeyedContainer<Key>( | |
codingPath: self.codingPath, | |
userInfo: self.userInfo, | |
mapValue: .init(fields: document.fields) | |
) | |
self.container = container | |
return KeyedDecodingContainer(container) | |
} | |
func unkeyedContainer() -> UnkeyedDecodingContainer { | |
fatalError("Root container must be a keyed container.") | |
} | |
func singleValueContainer() -> SingleValueDecodingContainer { | |
fatalError("Root container must be a keyed container.") | |
} | |
} | |
protocol FirestoreDecodingContainer { | |
var value: FirestoreDocument.Value { get } | |
} | |
// KeyedDecodingContainer.swift | |
import Foundation | |
extension _FirestoreDecoder { | |
final class KeyedContainer<Key> where Key: CodingKey { | |
var codingPath: [CodingKey] | |
var userInfo: [CodingUserInfoKey: Any] | |
fileprivate let mapValue: FirestoreDocument.MapValue | |
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], mapValue: FirestoreDocument.MapValue) { | |
self.codingPath = codingPath | |
self.userInfo = userInfo | |
self.mapValue = mapValue | |
} | |
} | |
} | |
extension _FirestoreDecoder.KeyedContainer: KeyedDecodingContainerProtocol { | |
var allKeys: [Key] { | |
guard let fields = mapValue.fields else { | |
return [] | |
} | |
return fields.keys.compactMap { Key(stringValue: $0) } | |
} | |
func contains(_ key: Key) -> Bool { | |
return mapValue.fields?.keys.contains(key.stringValue) ?? false | |
} | |
func decodeNil(forKey key: Key) throws -> Bool { | |
if case .nullValue = try getValue(for: key) { | |
return true | |
} else { | |
return false | |
} | |
} | |
public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: String.Type, forKey key: Key) throws -> String { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Double.Type, forKey key: Key) throws -> Double { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Float.Type, forKey key: Key) throws -> Float { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { | |
let value = try getValue(for: key) | |
let container = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
value: value | |
) | |
return try container.decode(type) | |
} | |
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { | |
guard case .arrayValue(let nestedValue) = try getValue(for: key) else { | |
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "cannot decode nested container for key: \(key)") | |
} | |
return _FirestoreDecoder.UnkeyedContainer( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
arrayValue: nestedValue | |
) | |
} | |
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { | |
guard case .mapValue(let nestedValue) = try getValue(for: key) else { | |
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "cannot decode nested container for key: \(key)") | |
} | |
let container = _FirestoreDecoder.KeyedContainer<NestedKey>( | |
codingPath: nestedCodingPath(for: key), | |
userInfo: userInfo, | |
mapValue: nestedValue | |
) | |
return KeyedDecodingContainer(container) | |
} | |
func superDecoder() throws -> Decoder { | |
guard let fields = mapValue.fields else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot create super decoder with null fields") | |
throw DecodingError.dataCorrupted(context) | |
} | |
return _FirestoreDecoder(document: .init(fields: fields)) | |
} | |
func superDecoder(forKey key: Key) throws -> Decoder { | |
guard case .mapValue(let nestedValue) = try getValue(for: key), let fields = nestedValue.fields else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot create super decoder with non-map value for key \(key)") | |
throw DecodingError.dataCorrupted(context) | |
} | |
return _FirestoreDecoder(document: .init(fields: fields)) | |
} | |
} | |
extension _FirestoreDecoder.KeyedContainer: FirestoreDecodingContainer { | |
var value: FirestoreDocument.Value { | |
return .mapValue(mapValue) | |
} | |
} | |
fileprivate extension _FirestoreDecoder.KeyedContainer { | |
func getValue(for key: Key) throws -> FirestoreDocument.Value { | |
guard let fields = mapValue.fields, let value = fields[key.stringValue] else { | |
let context = DecodingError.Context(codingPath: self.codingPath, debugDescription: "key not found: \(key)") | |
throw DecodingError.keyNotFound(key, context) | |
} | |
return value | |
} | |
func nestedCodingPath(for key: Key) -> [CodingKey] { | |
return codingPath + [AnyCodingKey(key)] | |
} | |
} | |
// SingleValudeDecodingContainer.swift | |
import Foundation | |
extension _FirestoreDecoder { | |
final class SingleValueContainer { | |
var codingPath: [CodingKey] | |
var userInfo: [CodingUserInfoKey: Any] | |
let value: FirestoreDocument.Value | |
init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any], value: FirestoreDocument.Value) { | |
self.codingPath = codingPath | |
self.userInfo = userInfo | |
self.value = value | |
} | |
} | |
} | |
extension _FirestoreDecoder.SingleValueContainer: SingleValueDecodingContainer { | |
func decodeNil() -> Bool { | |
if case .nullValue = value { | |
return true | |
} else { | |
return false | |
} | |
} | |
func decode(_ type: Bool.Type) throws -> Bool { | |
if case .booleanValue = value { | |
return true | |
} else { | |
return false | |
} | |
} | |
func decode(_ type: String.Type) throws -> String { | |
guard case .stringValue(let stringValue) = value else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as String") | |
throw DecodingError.typeMismatch(String.self, context) | |
} | |
return stringValue | |
} | |
func decode(_ type: Double.Type) throws -> Double { | |
guard case .doubleValue(let doubleValue) = value else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Double") | |
throw DecodingError.typeMismatch(Double.self, context) | |
} | |
return doubleValue | |
} | |
func decode(_ type: Float.Type) throws -> Float { | |
guard case .doubleValue(let doubleValue) = value else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Float") | |
throw DecodingError.typeMismatch(Double.self, context) | |
} | |
return Float(doubleValue) | |
} | |
func decode(_ type: Int.Type) throws -> Int { | |
guard case .integerValue(let stringValue) = value, let integerValue = Int(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Int") | |
throw DecodingError.typeMismatch(Int.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: Int8.Type) throws -> Int8 { | |
guard case .integerValue(let stringValue) = value, let integerValue = Int8(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Int8") | |
throw DecodingError.typeMismatch(Int8.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: Int16.Type) throws -> Int16 { | |
guard case .integerValue(let stringValue) = value, let integerValue = Int16(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Int16") | |
throw DecodingError.typeMismatch(Int16.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: Int32.Type) throws -> Int32 { | |
guard case .integerValue(let stringValue) = value, let integerValue = Int32(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Int32") | |
throw DecodingError.typeMismatch(Int32.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: Int64.Type) throws -> Int64 { | |
guard case .integerValue(let stringValue) = value, let integerValue = Int64(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as Int64") | |
throw DecodingError.typeMismatch(Int64.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: UInt.Type) throws -> UInt { | |
guard case .integerValue(let stringValue) = value, let integerValue = UInt(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as UInt") | |
throw DecodingError.typeMismatch(UInt.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: UInt8.Type) throws -> UInt8 { | |
guard case .integerValue(let stringValue) = value, let integerValue = UInt8(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as UInt8") | |
throw DecodingError.typeMismatch(UInt8.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: UInt16.Type) throws -> UInt16 { | |
guard case .integerValue(let stringValue) = value, let integerValue = UInt16(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as UInt16") | |
throw DecodingError.typeMismatch(UInt16.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: UInt32.Type) throws -> UInt32 { | |
guard case .integerValue(let stringValue) = value, let integerValue = UInt32(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as UInt32") | |
throw DecodingError.typeMismatch(UInt32.self, context) | |
} | |
return integerValue | |
} | |
func decode(_ type: UInt64.Type) throws -> UInt64 { | |
guard case .integerValue(let stringValue) = value, let integerValue = UInt64(stringValue) else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as UInt64") | |
throw DecodingError.typeMismatch(UInt64.self, context) | |
} | |
return integerValue | |
} | |
func decode<T>(_ type: T.Type) throws -> T where T : Decodable { | |
if (type == Date.self || type == NSDate.self), | |
case .timestampValue(let timestamp) = value, | |
let date = FirestoreDocument.deserialize(date: timestamp) { | |
return date as! T | |
} else if (type == Data.self || type == NSData.self), | |
case .bytesValue(let base64String) = value, | |
let data = Data(base64Encoded: base64String) { | |
return data as! T | |
} else if case .mapValue(let mapValue) = value, let fields = mapValue.fields { | |
let document = FirestoreDocument(fields: fields) | |
let decoder = _FirestoreDecoder(document: document) | |
return try T(from: decoder) | |
} else { | |
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "value \(value) cannot be decoded as \(T.self)") | |
throw DecodingError.typeMismatch(T.self, context) | |
} | |
} | |
} | |
extension _FirestoreDecoder.SingleValueContainer: FirestoreDecodingContainer {} | |
// UnkeyedDecodingContainer.swift | |
import Foundation | |
extension _FirestoreDecoder { | |
final class UnkeyedContainer { | |
var codingPath: [CodingKey] | |
var userInfo: [CodingUserInfoKey: Any] | |
var currentIndex: Int = 0 | |
fileprivate let arrayValue: FirestoreDocument.ArrayValue | |
init( | |
codingPath: [CodingKey], | |
userInfo: [CodingUserInfoKey : Any], | |
arrayValue: FirestoreDocument.ArrayValue) { | |
self.codingPath = codingPath | |
self.userInfo = userInfo | |
self.arrayValue = arrayValue | |
} | |
} | |
} | |
extension _FirestoreDecoder.UnkeyedContainer: UnkeyedDecodingContainer { | |
var count: Int? { | |
return arrayValue.values?.count | |
} | |
var isAtEnd: Bool { | |
guard let count = arrayValue.values?.count else { | |
return true | |
} | |
return currentIndex < count | |
} | |
func decodeNil() throws -> Bool { | |
defer { | |
currentIndex += 1 | |
} | |
if case .nullValue = try currentValue() { | |
return true | |
} else { | |
return false | |
} | |
} | |
func decode<T>(_ type: T.Type) throws -> T where T : Decodable { | |
defer { | |
currentIndex += 1 | |
} | |
let value = try currentValue() | |
let decoder = _FirestoreDecoder.SingleValueContainer( | |
codingPath: nestedCodingPath, | |
userInfo: userInfo, | |
value: value | |
) | |
return try decoder.decode(type) | |
} | |
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { | |
defer { | |
currentIndex += 1 | |
} | |
guard case .arrayValue(let nestedValue) = try currentValue() else { | |
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Cannot create a UnkeyedDecodingContainer for a non-array value at index \(currentIndex)") | |
} | |
return _FirestoreDecoder.UnkeyedContainer( | |
codingPath: nestedCodingPath, | |
userInfo: userInfo, | |
arrayValue: nestedValue | |
) | |
} | |
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { | |
defer { | |
currentIndex += 1 | |
} | |
guard case .mapValue(let nestedValue) = try currentValue() else { | |
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Cannot create a UnkeyedDecodingContainer for a non-map value at index \(currentIndex)") | |
} | |
let container = _FirestoreDecoder.KeyedContainer<NestedKey>( | |
codingPath: nestedCodingPath, | |
userInfo: userInfo, | |
mapValue: nestedValue | |
) | |
return KeyedDecodingContainer(container) | |
} | |
func superDecoder() throws -> Decoder { | |
fatalError("Implement me!") | |
} | |
} | |
extension _FirestoreDecoder.UnkeyedContainer: FirestoreDecodingContainer { | |
var value: FirestoreDocument.Value { | |
return .arrayValue(arrayValue) | |
} | |
} | |
fileprivate extension _FirestoreDecoder.UnkeyedContainer { | |
func currentValue() throws -> FirestoreDocument.Value { | |
guard let values = arrayValue.values, currentIndex < values.count else { | |
throw DecodingError.dataCorruptedError(in: self, debugDescription: "Cannot decode value from empty array value.") | |
} | |
return values[currentIndex] | |
} | |
var nestedCodingPath: [CodingKey] { | |
return codingPath + [AnyCodingKey(intValue: currentIndex)!] | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment