// // Map.swift // ObjectMapper // // Created by Tristan Himmelman on 2015-10-09. // // The MIT License (MIT) // // Copyright (c) 2014-2016 Hearst // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// MapContext is available for developers who wish to pass information around during the mapping process. public protocol MapContext { } /// A class used for holding mapping data public final class Map { public let mappingType: MappingType public internal(set) var JSON: [String: Any] = [:] public internal(set) var isKeyPresent = false public internal(set) var currentValue: Any? public internal(set) var currentKey: String? var keyIsNested = false public internal(set) var nestedKeyDelimiter: String = "." public var context: MapContext? public var shouldIncludeNilValues = false /// If this is set to true, toJSON output will include null values for any variables that are not set. public let toObject: Bool // indicates whether the mapping is being applied to an existing object public init(mappingType: MappingType, JSON: [String: Any], toObject: Bool = false, context: MapContext? = nil, shouldIncludeNilValues: Bool = false) { self.mappingType = mappingType self.JSON = JSON self.toObject = toObject self.context = context self.shouldIncludeNilValues = shouldIncludeNilValues } /// Sets the current mapper value and key. /// The Key paramater can be a period separated string (ex. "distance.value") to access sub objects. public subscript(key: String) -> Map { // save key and value associated to it return self.subscript(key: key) } public subscript(key: String, delimiter delimiter: String) -> Map { return self.subscript(key: key, delimiter: delimiter) } public subscript(key: String, nested nested: Bool) -> Map { return self.subscript(key: key, nested: nested) } public subscript(key: String, nested nested: Bool, delimiter delimiter: String) -> Map { return self.subscript(key: key, nested: nested, delimiter: delimiter) } public subscript(key: String, ignoreNil ignoreNil: Bool) -> Map { return self.subscript(key: key, ignoreNil: ignoreNil) } public subscript(key: String, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map { return self.subscript(key: key, delimiter: delimiter, ignoreNil: ignoreNil) } public subscript(key: String, nested nested: Bool, ignoreNil ignoreNil: Bool) -> Map { return self.subscript(key: key, nested: nested, ignoreNil: ignoreNil) } public subscript(key: String, nested nested: Bool?, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map { return self.subscript(key: key, nested: nested, delimiter: delimiter, ignoreNil: ignoreNil) } private func `subscript`(key: String, nested: Bool? = nil, delimiter: String = ".", ignoreNil: Bool = false) -> Map { // save key and value associated to it currentKey = key keyIsNested = nested ?? key.contains(delimiter) nestedKeyDelimiter = delimiter if mappingType == .fromJSON { // check if a value exists for the current key // do this pre-check for performance reasons if keyIsNested { // break down the components of the key that are separated by delimiter (isKeyPresent, currentValue) = valueFor(ArraySlice(key.components(separatedBy: delimiter)), dictionary: JSON) } else { let object = JSON[key] let isNSNull = object is NSNull isKeyPresent = isNSNull ? true : object != nil currentValue = isNSNull ? nil : object } // update isKeyPresent if ignoreNil is true if ignoreNil && currentValue == nil { isKeyPresent = false } } return self } public func value() -> T? { let value = currentValue as? T // Swift 4.1 breaks Float casting from `NSNumber`. So Added extra checks for `Float` `[Float]` and `[String:Float]` if value == nil && T.self == Float.self { if let v = currentValue as? NSNumber { return v.floatValue as? T } } else if value == nil && T.self == [Float].self { if let v = currentValue as? [Double] { #if swift(>=4.1) return v.compactMap{ Float($0) } as? T #else return v.flatMap{ Float($0) } as? T #endif } } else if value == nil && T.self == [String:Float].self { if let v = currentValue as? [String:Double] { return v.mapValues{ Float($0) } as? T } } return value } } /// Fetch value from JSON dictionary, loop through keyPathComponents until we reach the desired object private func valueFor(_ keyPathComponents: ArraySlice, dictionary: [String: Any]) -> (Bool, Any?) { // Implement it as a tail recursive function. if keyPathComponents.isEmpty { return (false, nil) } if let keyPath = keyPathComponents.first { let isTail = keyPathComponents.count == 1 let object = dictionary[keyPath] if object is NSNull { return (isTail, nil) } else if keyPathComponents.count > 1, let dict = object as? [String: Any] { let tail = keyPathComponents.dropFirst() return valueFor(tail, dictionary: dict) } else if keyPathComponents.count > 1, let array = object as? [Any] { let tail = keyPathComponents.dropFirst() return valueFor(tail, array: array) } else { return (isTail && object != nil, object) } } return (false, nil) } /// Fetch value from JSON Array, loop through keyPathComponents them until we reach the desired object private func valueFor(_ keyPathComponents: ArraySlice, array: [Any]) -> (Bool, Any?) { // Implement it as a tail recursive function. if keyPathComponents.isEmpty { return (false, nil) } //Try to convert keypath to Int as index if let keyPath = keyPathComponents.first, let index = Int(keyPath) , index >= 0 && index < array.count { let isTail = keyPathComponents.count == 1 let object = array[index] if object is NSNull { return (isTail, nil) } else if keyPathComponents.count > 1, let array = object as? [Any] { let tail = keyPathComponents.dropFirst() return valueFor(tail, array: array) } else if keyPathComponents.count > 1, let dict = object as? [String: Any] { let tail = keyPathComponents.dropFirst() return valueFor(tail, dictionary: dict) } else { return (isTail, object) } } return (false, nil) }