Skip to content

Instantly share code, notes, and snippets.

@guidomb
Created October 4, 2018 13:12
Show Gist options
  • Save guidomb/4f4a759c9810e5b999b579c28c84e15d to your computer and use it in GitHub Desktop.
Save guidomb/4f4a759c9810e5b999b579c28c84e15d to your computer and use it in GitHub Desktop.
A Firebase's Firestore document Swift decoder implementation
// 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