// Copyright (c) 2017-2020 Shawn Moore and XMLCoder contributors // // This software is released under the MIT License. // https://opensource.org/licenses/MIT // // Created by Shawn Moore on 11/22/17. // import Foundation class XMLEncoderImplementation: Encoder { // MARK: Properties /// The encoder's storage. var storage: XMLEncodingStorage /// Options set on the top-level encoder. let options: XMLEncoder.Options /// The path to the current point in encoding. public var codingPath: [CodingKey] public var nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding?] /// Contextual user-provided information for use during encoding. public var userInfo: [CodingUserInfoKey: Any] { return options.userInfo } // MARK: - Initialization /// Initializes `self` with the given top-level encoder options. init( options: XMLEncoder.Options, nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding?], codingPath: [CodingKey] = [] ) { self.options = options storage = XMLEncodingStorage() self.codingPath = codingPath self.nodeEncodings = nodeEncodings } /// 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. 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 storage.count == codingPath.count } // MARK: - Encoder Methods public func container(keyedBy _: Key.Type) -> KeyedEncodingContainer { guard canEncodeNewValue else { return mergeWithExistingKeyedContainer(keyedBy: Key.self) } if Key.self is XMLChoiceCodingKey.Type { return choiceContainer(keyedBy: Key.self) } else { return keyedContainer(keyedBy: Key.self) } } public func unkeyedContainer() -> UnkeyedEncodingContainer { // If an existing unkeyed container was already requested, return that one. let topContainer: SharedBox if canEncodeNewValue { // We haven't yet pushed a container at this level; do so here. topContainer = storage.pushUnkeyedContainer() } else { guard let container = storage.lastContainer as? SharedBox else { preconditionFailure( """ Attempt to push new unkeyed encoding container when already previously encoded \ at this path. """ ) } topContainer = container } return XMLUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) } public func singleValueContainer() -> SingleValueEncodingContainer { return self } private func keyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { let container = XMLKeyedEncodingContainer( referencing: self, codingPath: codingPath, wrapping: storage.pushKeyedContainer() ) return KeyedEncodingContainer(container) } private func choiceContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { let container = XMLChoiceEncodingContainer( referencing: self, codingPath: codingPath, wrapping: storage.pushChoiceContainer() ) return KeyedEncodingContainer(container) } private func mergeWithExistingKeyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { switch storage.lastContainer { case let keyed as SharedBox: let container = XMLKeyedEncodingContainer( referencing: self, codingPath: codingPath, wrapping: keyed ) return KeyedEncodingContainer(container) case let choice as SharedBox: _ = storage.popContainer() let keyed = KeyedBox( elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]), attributes: [] ) let container = XMLKeyedEncodingContainer( referencing: self, codingPath: codingPath, wrapping: storage.pushKeyedContainer(keyed) ) return KeyedEncodingContainer(container) default: preconditionFailure( """ No existing keyed encoding container to merge with. """ ) } } } extension XMLEncoderImplementation { /// Returns the given value boxed in a container appropriate for pushing onto the container stack. func box() -> SimpleBox { return NullBox() } func box(_ value: Bool) -> SimpleBox { return BoolBox(value) } func box(_ value: Decimal) -> SimpleBox { return DecimalBox(value) } func box(_ value: T) -> SimpleBox { return IntBox(value) } func box(_ value: T) -> SimpleBox { return UIntBox(value) } func box(_ value: Float) throws -> SimpleBox { return try box(value, FloatBox.self) } func box(_ value: Double) throws -> SimpleBox { return try box(value, DoubleBox.self) } func box( _ value: T, _: B.Type ) throws -> SimpleBox where B.Unboxed == T { guard value.isInfinite || value.isNaN else { return B(value) } guard case let .convertToString( positiveInfinity: posInfString, negativeInfinity: negInfString, nan: nanString ) = options.nonConformingFloatEncodingStrategy else { throw EncodingError._invalidFloatingPointValue(value, at: codingPath) } if value == T.infinity { return StringBox(posInfString) } else if value == -T.infinity { return StringBox(negInfString) } else { return StringBox(nanString) } } func box(_ value: String) -> SimpleBox { return StringBox(value) } func box(_ value: Date) throws -> Box { switch options.dateEncodingStrategy { case .deferredToDate: try value.encode(to: self) return storage.popContainer() case .secondsSince1970: return DateBox(value, format: .secondsSince1970) case .millisecondsSince1970: return DateBox(value, format: .millisecondsSince1970) case .iso8601: return DateBox(value, format: .iso8601) case let .formatted(formatter): return DateBox(value, format: .formatter(formatter)) case let .custom(closure): let depth = storage.count try closure(value, self) guard storage.count > depth else { return KeyedBox() } return storage.popContainer() } } func box(_ value: Data) throws -> Box { switch options.dataEncodingStrategy { case .deferredToData: try value.encode(to: self) return storage.popContainer() case .base64: return DataBox(value, format: .base64) case let .custom(closure): let depth = storage.count try closure(value, self) guard storage.count > depth else { return KeyedBox() } return storage.popContainer() } } func box(_ value: URL) -> SimpleBox { return URLBox(value) } func box(_ value: T) throws -> Box { if T.self == Date.self || T.self == NSDate.self, let value = value as? Date { return try box(value) } else if T.self == Data.self || T.self == NSData.self, let value = value as? Data { return try box(value) } else if T.self == URL.self || T.self == NSURL.self, let value = value as? URL { return box(value) } else if T.self == Decimal.self || T.self == NSDecimalNumber.self, let value = value as? Decimal { return box(value) } let depth = storage.count try value.encode(to: self) // The top container should be a new container. guard storage.count > depth else { return KeyedBox() } let lastContainer = storage.popContainer() guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else { return lastContainer } return sharedBox.typeErasedUnbox() } }