diff --git a/CodableFirebase.xcodeproj/project.pbxproj b/CodableFirebase.xcodeproj/project.pbxproj index 139459e..8c37632 100644 --- a/CodableFirebase.xcodeproj/project.pbxproj +++ b/CodableFirebase.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ CE7DD3831F9D04AE000225C5 /* FirestoreEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */; }; CE7DD3841F9D04AE000225C5 /* FirestoreDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */; }; CE7DD3861F9DE4F7000225C5 /* TestCodableFirestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */; }; + CEFDBF821FF3B35B00745EBE /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF811FF3B35B00745EBE /* Encoder.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -33,6 +34,7 @@ CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirestoreEncoder.swift; sourceTree = ""; }; CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirestoreDecoder.swift; sourceTree = ""; }; CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestCodableFirestore.swift; sourceTree = ""; }; + CEFDBF811FF3B35B00745EBE /* Encoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encoder.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -78,6 +80,7 @@ CE7DD36A1F9CFA81000225C5 /* CodableFirebase.h */, CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */, CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */, + CEFDBF811FF3B35B00745EBE /* Encoder.swift */, CE7DD36B1F9CFA81000225C5 /* Info.plist */, ); path = CodableFirebase; @@ -204,6 +207,7 @@ buildActionMask = 2147483647; files = ( CE7DD3831F9D04AE000225C5 /* FirestoreEncoder.swift in Sources */, + CEFDBF821FF3B35B00745EBE /* Encoder.swift in Sources */, CE7DD3841F9D04AE000225C5 /* FirestoreDecoder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CodableFirebase/Encoder.swift b/CodableFirebase/Encoder.swift new file mode 100644 index 0000000..57b603e --- /dev/null +++ b/CodableFirebase/Encoder.swift @@ -0,0 +1,494 @@ +// +// Encoder.swift +// CodableFirebase +// +// Created by Oleksii on 27/12/2017. +// Copyright © 2017 ViolentOctopus. All rights reserved. +// + +import Foundation + +class _FirestoreEncoder : Encoder { + fileprivate var storage: _FirestoreEncodingStorage + fileprivate(set) public var codingPath: [CodingKey] + public var userInfo: [CodingUserInfoKey : Any] + + init(codingPath: [CodingKey] = []) { + self.storage = _FirestoreEncodingStorage() + self.codingPath = codingPath + userInfo = [:] + } + + /// Returns whether a new element can be encoded at this coding path. + /// + /// `true` if an element has not yet been encoded at this coding path; `false` otherwise. + fileprivate var canEncodeNewValue: Bool { + // Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container). + // At the same time, every time a container is requested, a new value gets pushed onto the storage stack. + // If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition. + // + // This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path. + // Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here). + return self.storage.count == self.codingPath.count + } + + // MARK: - Encoder Methods + public func container(keyedBy: Key.Type) -> KeyedEncodingContainer { + // If an existing keyed container was already requested, return that one. + let topContainer: NSMutableDictionary + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = storage.pushKeyedContainer() + } else { + guard let container = self.storage.containers.last as? NSMutableDictionary else { + preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") + } + + topContainer = container + } + + let container = _FirestoreKeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> UnkeyedEncodingContainer { + // If an existing unkeyed container was already requested, return that one. + let topContainer: NSMutableArray + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = self.storage.pushUnkeyedContainer() + } else { + guard let container = self.storage.containers.last as? NSMutableArray else { + preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") + } + + topContainer = container + } + + return _FirestoreUnkeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer) + } + + public func singleValueContainer() -> SingleValueEncodingContainer { + return self + } +} + +fileprivate struct _FirestoreEncodingStorage { + // MARK: Properties + /// The container stack. + /// Elements may be any one of the plist types (NSNumber, NSString, NSDate, NSArray, NSDictionary). + private(set) fileprivate var containers: [NSObject] = [] + + // MARK: - Initialization + /// Initializes `self` with no containers. + fileprivate init() {} + + // MARK: - Modifying the Stack + fileprivate var count: Int { + return containers.count + } + + fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary { + let dictionary = NSMutableDictionary() + containers.append(dictionary) + return dictionary + } + + fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray { + let array = NSMutableArray() + containers.append(array) + return array + } + + fileprivate mutating func push(container: NSObject) { + containers.append(container) + } + + fileprivate mutating func popContainer() -> NSObject { + precondition(containers.count > 0, "Empty container stack.") + return containers.popLast()! + } +} + +fileprivate struct _FirestoreKeyedEncodingContainer : KeyedEncodingContainerProtocol { + typealias Key = K + + // MARK: Properties + /// A reference to the encoder we're writing to. + private let encoder: _FirestoreEncoder + + /// A reference to the container we're writing to. + private let container: NSMutableDictionary + + /// The path of coding keys taken to get to this point in encoding. + private(set) public var codingPath: [CodingKey] + + // MARK: - Initialization + /// Initializes `self` with the given references. + fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - KeyedEncodingContainerProtocol Methods + public mutating func encodeNil(forKey key: Key) throws { container[key.stringValue] = NSNull() } + public mutating func encode(_ value: Bool, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: String, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Float, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Double, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + + public mutating func encode(_ value: T, forKey key: Key) throws { + encoder.codingPath.append(key) + defer { encoder.codingPath.removeLast() } + container[key.stringValue] = try encoder.box(value) + } + + public mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + let dictionary = NSMutableDictionary() + self.container[key.stringValue] = dictionary + + codingPath.append(key) + defer { codingPath.removeLast() } + + let container = _FirestoreKeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: dictionary) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let array = NSMutableArray() + container[key.stringValue] = array + + codingPath.append(key) + defer { codingPath.removeLast() } + return _FirestoreUnkeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: array) + } + + public mutating func superEncoder() -> Encoder { + return _FirestoreReferencingEncoder(referencing: encoder, at: _FirebaseKey.super, wrapping: container) + } + + public mutating func superEncoder(forKey key: Key) -> Encoder { + return _FirestoreReferencingEncoder(referencing: encoder, at: key, wrapping: container) + } +} + +fileprivate struct _FirestoreUnkeyedEncodingContainer : UnkeyedEncodingContainer { + // MARK: Properties + /// A reference to the encoder we're writing to. + private let encoder: _FirestoreEncoder + + /// A reference to the container we're writing to. + private let container: NSMutableArray + + /// The path of coding keys taken to get to this point in encoding. + private(set) public var codingPath: [CodingKey] + + /// The number of elements encoded into the container. + public var count: Int { + return container.count + } + + // MARK: - Initialization + /// Initializes `self` with the given references. + fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - UnkeyedEncodingContainer Methods + public mutating func encodeNil() throws { container.add(NSNull()) } + public mutating func encode(_ value: Bool) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Int) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Int8) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Int16) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Int32) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Int64) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: UInt) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: UInt8) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: UInt16) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: UInt32) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: UInt64) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Float) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: Double) throws { container.add(self.encoder.box(value)) } + public mutating func encode(_ value: String) throws { container.add(self.encoder.box(value)) } + + public mutating func encode(_ value: T) throws { + encoder.codingPath.append(_FirebaseKey(index: count)) + defer { encoder.codingPath.removeLast() } + container.add(try encoder.box(value)) + } + + public mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + self.codingPath.append(_FirebaseKey(index: self.count)) + defer { self.codingPath.removeLast() } + + let dictionary = NSMutableDictionary() + self.container.add(dictionary) + + let container = _FirestoreKeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + self.codingPath.append(_FirebaseKey(index: self.count)) + defer { self.codingPath.removeLast() } + + let array = NSMutableArray() + self.container.add(array) + return _FirestoreUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array) + } + + public mutating func superEncoder() -> Encoder { + return _FirestoreReferencingEncoder(referencing: encoder, at: container.count, wrapping: container) + } +} + +struct _FirebaseKey : CodingKey { + public var stringValue: String + public var intValue: Int? + + public init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + public init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + init(index: Int) { + self.stringValue = "Index \(index)" + self.intValue = index + } + + static let `super` = _FirebaseKey(stringValue: "super")! +} + +extension _FirestoreEncoder { + + /// Returns the given value boxed in a container appropriate for pushing onto the container stack. + fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Float) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Double) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) } + + fileprivate func box(_ value: T) throws -> NSObject { + return try self.box_(value) ?? NSDictionary() + } + + func box_(_ value: T) throws -> NSObject? { + if T.self == Date.self || T.self == NSDate.self { + return (value as! NSDate) + } else if T.self == Data.self || T.self == NSData.self { + return (value as! NSData) + } + + // The value should request a container from the _FirestoreEncoder. + let depth = storage.count + try value.encode(to: self) + + // The top container should be a new container. + guard storage.count > depth else { + return nil + } + + return storage.popContainer() + } +} + +extension _FirestoreEncoder : SingleValueEncodingContainer { + // MARK: - SingleValueEncodingContainer Methods + private func assertCanEncodeNewValue() { + precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.") + } + + public func encodeNil() throws { + assertCanEncodeNewValue() + storage.push(container: NSNull()) + } + + public func encode(_ value: Bool) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: String) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Float) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Double) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: T) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } +} + +fileprivate class _FirestoreReferencingEncoder : _FirestoreEncoder { + // MARK: Reference types. + /// The type of container we're referencing. + private enum Reference { + /// Referencing a specific index in an array container. + case array(NSMutableArray, Int) + + /// Referencing a specific key in a dictionary container. + case dictionary(NSMutableDictionary, String) + } + + // MARK: - Properties + /// The encoder we're referencing. + private let encoder: _FirestoreEncoder + + /// The container reference itself. + private let reference: Reference + + // MARK: - Initialization + /// Initializes `self` by referencing the given array container in the given encoder. + fileprivate init(referencing encoder: _FirestoreEncoder, at index: Int, wrapping array: NSMutableArray) { + self.encoder = encoder + self.reference = .array(array, index) + super.init(codingPath: encoder.codingPath) + + self.codingPath.append(_FirebaseKey(index: index)) + } + + /// Initializes `self` by referencing the given dictionary container in the given encoder. + fileprivate init(referencing encoder: _FirestoreEncoder, at key: CodingKey, wrapping dictionary: NSMutableDictionary) { + self.encoder = encoder + reference = .dictionary(dictionary, key.stringValue) + super.init(codingPath: encoder.codingPath) + codingPath.append(key) + } + + // MARK: - Coding Path Operations + fileprivate override var canEncodeNewValue: Bool { + // With a regular encoder, the storage and coding path grow together. + // A referencing encoder, however, inherits its parents coding path, as well as the key it was created for. + // We have to take this into account. + return storage.count == codingPath.count - encoder.codingPath.count - 1 + } + + // MARK: - Deinitialization + // Finalizes `self` by writing the contents of our storage to the referenced encoder's storage. + deinit { + let value: Any + switch storage.count { + case 0: value = NSDictionary() + case 1: value = self.storage.popContainer() + default: fatalError("Referencing encoder deallocated with multiple containers on stack.") + } + + switch self.reference { + case .array(let array, let index): + array.insert(value, at: index) + + case .dictionary(let dictionary, let key): + dictionary[NSString(string: key)] = value + } + } +} + +internal extension DecodingError { + internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError { + let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead." + return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description)) + } + + fileprivate static func _typeDescription(of value: Any) -> String { + if value is NSNull { + return "a null value" + } else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ { + return "a number" + } else if value is String { + return "a string/data" + } else if value is [Any] { + return "an array" + } else if value is [String : Any] { + return "a dictionary" + } else { + return "\(type(of: value))" + } + } +} diff --git a/CodableFirebase/FirestoreEncoder.swift b/CodableFirebase/FirestoreEncoder.swift index 0e8b6fc..142b55c 100644 --- a/CodableFirebase/FirestoreEncoder.swift +++ b/CodableFirebase/FirestoreEncoder.swift @@ -34,489 +34,3 @@ open class FirestoreEncoder { return topLevel } } - -fileprivate class _FirestoreEncoder : Encoder { - fileprivate var storage: _FirestoreEncodingStorage - fileprivate(set) public var codingPath: [CodingKey] - public var userInfo: [CodingUserInfoKey : Any] - - fileprivate init(codingPath: [CodingKey] = []) { - self.storage = _FirestoreEncodingStorage() - self.codingPath = codingPath - userInfo = [:] - } - - /// Returns whether a new element can be encoded at this coding path. - /// - /// `true` if an element has not yet been encoded at this coding path; `false` otherwise. - fileprivate var canEncodeNewValue: Bool { - // Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container). - // At the same time, every time a container is requested, a new value gets pushed onto the storage stack. - // If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition. - // - // This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path. - // Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here). - return self.storage.count == self.codingPath.count - } - - // MARK: - Encoder Methods - public func container(keyedBy: Key.Type) -> KeyedEncodingContainer { - // If an existing keyed container was already requested, return that one. - let topContainer: NSMutableDictionary - if canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = storage.pushKeyedContainer() - } else { - guard let container = self.storage.containers.last as? NSMutableDictionary else { - preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") - } - - topContainer = container - } - - let container = _FirestoreKeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer) - return KeyedEncodingContainer(container) - } - - public func unkeyedContainer() -> UnkeyedEncodingContainer { - // If an existing unkeyed container was already requested, return that one. - let topContainer: NSMutableArray - if canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = self.storage.pushUnkeyedContainer() - } else { - guard let container = self.storage.containers.last as? NSMutableArray else { - preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") - } - - topContainer = container - } - - return _FirestoreUnkeyedEncodingContainer(referencing: self, codingPath: self.codingPath, wrapping: topContainer) - } - - public func singleValueContainer() -> SingleValueEncodingContainer { - return self - } -} - -fileprivate struct _FirestoreEncodingStorage { - // MARK: Properties - /// The container stack. - /// Elements may be any one of the plist types (NSNumber, NSString, NSDate, NSArray, NSDictionary). - private(set) fileprivate var containers: [NSObject] = [] - - // MARK: - Initialization - /// Initializes `self` with no containers. - fileprivate init() {} - - // MARK: - Modifying the Stack - fileprivate var count: Int { - return containers.count - } - - fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary { - let dictionary = NSMutableDictionary() - containers.append(dictionary) - return dictionary - } - - fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray { - let array = NSMutableArray() - containers.append(array) - return array - } - - fileprivate mutating func push(container: NSObject) { - containers.append(container) - } - - fileprivate mutating func popContainer() -> NSObject { - precondition(containers.count > 0, "Empty container stack.") - return containers.popLast()! - } -} - -fileprivate struct _FirestoreKeyedEncodingContainer : KeyedEncodingContainerProtocol { - typealias Key = K - - // MARK: Properties - /// A reference to the encoder we're writing to. - private let encoder: _FirestoreEncoder - - /// A reference to the container we're writing to. - private let container: NSMutableDictionary - - /// The path of coding keys taken to get to this point in encoding. - private(set) public var codingPath: [CodingKey] - - // MARK: - Initialization - /// Initializes `self` with the given references. - fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) { - self.encoder = encoder - self.codingPath = codingPath - self.container = container - } - - // MARK: - KeyedEncodingContainerProtocol Methods - public mutating func encodeNil(forKey key: Key) throws { container[key.stringValue] = NSNull() } - public mutating func encode(_ value: Bool, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Int, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Int8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Int16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Int32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Int64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: UInt, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: UInt8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: UInt16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: UInt32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: UInt64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: String, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Float, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - public mutating func encode(_ value: Double, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } - - public mutating func encode(_ value: T, forKey key: Key) throws { - encoder.codingPath.append(key) - defer { encoder.codingPath.removeLast() } - container[key.stringValue] = try encoder.box(value) - } - - public mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { - let dictionary = NSMutableDictionary() - self.container[key.stringValue] = dictionary - - codingPath.append(key) - defer { codingPath.removeLast() } - - let container = _FirestoreKeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: dictionary) - return KeyedEncodingContainer(container) - } - - public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - let array = NSMutableArray() - container[key.stringValue] = array - - codingPath.append(key) - defer { codingPath.removeLast() } - return _FirestoreUnkeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: array) - } - - public mutating func superEncoder() -> Encoder { - return _FirestoreReferencingEncoder(referencing: encoder, at: _FirebaseKey.super, wrapping: container) - } - - public mutating func superEncoder(forKey key: Key) -> Encoder { - return _FirestoreReferencingEncoder(referencing: encoder, at: key, wrapping: container) - } -} - -fileprivate struct _FirestoreUnkeyedEncodingContainer : UnkeyedEncodingContainer { - // MARK: Properties - /// A reference to the encoder we're writing to. - private let encoder: _FirestoreEncoder - - /// A reference to the container we're writing to. - private let container: NSMutableArray - - /// The path of coding keys taken to get to this point in encoding. - private(set) public var codingPath: [CodingKey] - - /// The number of elements encoded into the container. - public var count: Int { - return container.count - } - - // MARK: - Initialization - /// Initializes `self` with the given references. - fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) { - self.encoder = encoder - self.codingPath = codingPath - self.container = container - } - - // MARK: - UnkeyedEncodingContainer Methods - public mutating func encodeNil() throws { container.add(NSNull()) } - public mutating func encode(_ value: Bool) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Int) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Int8) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Int16) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Int32) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Int64) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: UInt) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: UInt8) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: UInt16) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: UInt32) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: UInt64) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Float) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: Double) throws { container.add(self.encoder.box(value)) } - public mutating func encode(_ value: String) throws { container.add(self.encoder.box(value)) } - - public mutating func encode(_ value: T) throws { - encoder.codingPath.append(_FirebaseKey(index: count)) - defer { encoder.codingPath.removeLast() } - container.add(try encoder.box(value)) - } - - public mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { - self.codingPath.append(_FirebaseKey(index: self.count)) - defer { self.codingPath.removeLast() } - - let dictionary = NSMutableDictionary() - self.container.add(dictionary) - - let container = _FirestoreKeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: dictionary) - return KeyedEncodingContainer(container) - } - - public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - self.codingPath.append(_FirebaseKey(index: self.count)) - defer { self.codingPath.removeLast() } - - let array = NSMutableArray() - self.container.add(array) - return _FirestoreUnkeyedEncodingContainer(referencing: self.encoder, codingPath: self.codingPath, wrapping: array) - } - - public mutating func superEncoder() -> Encoder { - return _FirestoreReferencingEncoder(referencing: encoder, at: container.count, wrapping: container) - } -} - -struct _FirebaseKey : CodingKey { - public var stringValue: String - public var intValue: Int? - - public init?(stringValue: String) { - self.stringValue = stringValue - self.intValue = nil - } - - public init?(intValue: Int) { - self.stringValue = "\(intValue)" - self.intValue = intValue - } - - init(index: Int) { - self.stringValue = "Index \(index)" - self.intValue = index - } - - static let `super` = _FirebaseKey(stringValue: "super")! -} - -extension _FirestoreEncoder { - - /// Returns the given value boxed in a container appropriate for pushing onto the container stack. - fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Float) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: Double) -> NSObject { return NSNumber(value: value) } - fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) } - - fileprivate func box(_ value: T) throws -> NSObject { - return try self.box_(value) ?? NSDictionary() - } - - fileprivate func box_(_ value: T) throws -> NSObject? { - if T.self == Date.self || T.self == NSDate.self { - return (value as! NSDate) - } else if T.self == Data.self || T.self == NSData.self { - return (value as! NSData) - } - - // The value should request a container from the _FirestoreEncoder. - let depth = storage.count - try value.encode(to: self) - - // The top container should be a new container. - guard storage.count > depth else { - return nil - } - - return storage.popContainer() - } -} - -extension _FirestoreEncoder : SingleValueEncodingContainer { - // MARK: - SingleValueEncodingContainer Methods - private func assertCanEncodeNewValue() { - precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.") - } - - public func encodeNil() throws { - assertCanEncodeNewValue() - storage.push(container: NSNull()) - } - - public func encode(_ value: Bool) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int8) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int16) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int32) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int64) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt8) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt16) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt32) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt64) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: String) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Float) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Double) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: T) throws { - assertCanEncodeNewValue() - try storage.push(container: box(value)) - } -} - -fileprivate class _FirestoreReferencingEncoder : _FirestoreEncoder { - // MARK: Reference types. - /// The type of container we're referencing. - private enum Reference { - /// Referencing a specific index in an array container. - case array(NSMutableArray, Int) - - /// Referencing a specific key in a dictionary container. - case dictionary(NSMutableDictionary, String) - } - - // MARK: - Properties - /// The encoder we're referencing. - private let encoder: _FirestoreEncoder - - /// The container reference itself. - private let reference: Reference - - // MARK: - Initialization - /// Initializes `self` by referencing the given array container in the given encoder. - fileprivate init(referencing encoder: _FirestoreEncoder, at index: Int, wrapping array: NSMutableArray) { - self.encoder = encoder - self.reference = .array(array, index) - super.init(codingPath: encoder.codingPath) - - self.codingPath.append(_FirebaseKey(index: index)) - } - - /// Initializes `self` by referencing the given dictionary container in the given encoder. - fileprivate init(referencing encoder: _FirestoreEncoder, at key: CodingKey, wrapping dictionary: NSMutableDictionary) { - self.encoder = encoder - reference = .dictionary(dictionary, key.stringValue) - super.init(codingPath: encoder.codingPath) - codingPath.append(key) - } - - // MARK: - Coding Path Operations - fileprivate override var canEncodeNewValue: Bool { - // With a regular encoder, the storage and coding path grow together. - // A referencing encoder, however, inherits its parents coding path, as well as the key it was created for. - // We have to take this into account. - return storage.count == codingPath.count - encoder.codingPath.count - 1 - } - - // MARK: - Deinitialization - // Finalizes `self` by writing the contents of our storage to the referenced encoder's storage. - deinit { - let value: Any - switch storage.count { - case 0: value = NSDictionary() - case 1: value = self.storage.popContainer() - default: fatalError("Referencing encoder deallocated with multiple containers on stack.") - } - - switch self.reference { - case .array(let array, let index): - array.insert(value, at: index) - - case .dictionary(let dictionary, let key): - dictionary[NSString(string: key)] = value - } - } -} - -internal extension DecodingError { - internal static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError { - let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead." - return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description)) - } - - fileprivate static func _typeDescription(of value: Any) -> String { - if value is NSNull { - return "a null value" - } else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ { - return "a number" - } else if value is String { - return "a string/data" - } else if value is [Any] { - return "an array" - } else if value is [String : Any] { - return "a dictionary" - } else { - return "\(type(of: value))" - } - } -} -