From a80b5f40454f4a2b35bd541c104121a411ee57ca Mon Sep 17 00:00:00 2001 From: Shakhzod Ikromov Date: Thu, 18 Jan 2018 17:54:34 +0300 Subject: [PATCH] Allowing explicitly putting null --- Sources/Wrap.swift | 68 +++++++++++- Tests/WrapTests/WrapTests.swift | 188 ++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 4 deletions(-) diff --git a/Sources/Wrap.swift b/Sources/Wrap.swift index df0db39..763cf7f 100644 --- a/Sources/Wrap.swift +++ b/Sources/Wrap.swift @@ -30,6 +30,14 @@ import Foundation /// Type alias defining what type of Dictionary that Wrap produces public typealias WrappedDictionary = [String : Any] +/// Type for encoding JSON null value (alternate for NSNull) +public struct WrapNull { + public static let null = WrapNull() + private init() { + + } +} + /** * Wrap any object or value, encoding it into a JSON compatible Dictionary * @@ -113,6 +121,25 @@ public enum WrapKeyStyle { case convertToSnakeCase } +// Enum describing nil values in a wrapped dictionary +public enum WrapNilStyle { + /// Nil values are just skipped (default) + case skipNilValues + /// Puts JSON null value when property value is nil + /// Example: + /// + /// // Swift + /// struct Object { + /// let name: String? = nil + /// } + /// // JSON: + /// { + /// "name": null + /// } + /// + case explicitlyPutNull +} + /** * Protocol providing the main customization point for Wrap * @@ -127,6 +154,10 @@ public protocol WrapCustomizable { * implementation of the `keyForWrapping(propertyNamed:)` method. */ var wrapKeyStyle: WrapKeyStyle { get } + /** + * The style that wrap should ignore nil values or explicitly put JSON null value + */ + var wrapNilStyle: WrapNilStyle { get } /** * Override the wrapping process for this type * @@ -238,6 +269,10 @@ public extension WrapCustomizable { var wrapKeyStyle: WrapKeyStyle { return .matchPropertyName } + + var wrapNilStyle: WrapNilStyle { + return .skipNilValues + } func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { return try? Wrapper(context: context, dateFormatter: dateFormatter).wrap(object: self) @@ -400,6 +435,13 @@ private extension Wrapper { func wrap(object: T, writingOptions: JSONSerialization.WritingOptions) throws -> Data { let dictionary = try self.wrap(object: object, enableCustomizedWrapping: true) + .mapValues { value -> Any in + if let _ = value as? WrapNull { + return NSNull() + } + + return value + } return try JSONSerialization.data(withJSONObject: dictionary, options: writingOptions) } @@ -507,12 +549,21 @@ private extension Wrapper { var wrappedDictionary = WrappedDictionary() for mirror in mirrors { - for property in mirror.children { + propValueLoop:for property in mirror.children { if (property.value as? WrapOptional)?.isNil == true { - continue + if let customizable = customizable { + switch customizable.wrapNilStyle { + case .skipNilValues: + continue propValueLoop + case .explicitlyPutNull: + break + } + } else { + continue propValueLoop + } } - + guard let propertyName = property.label else { continue } @@ -529,7 +580,16 @@ private extension Wrapper { if let wrappedProperty = try customizable?.wrap(propertyNamed: propertyName, originalValue: property.value, context: self.context, dateFormatter: self.dateFormatter) { wrappedDictionary[wrappingKey] = wrappedProperty } else { - wrappedDictionary[wrappingKey] = try self.wrap(value: property.value, propertyName: propertyName) + if let customizable = customizable { + switch customizable.wrapNilStyle { + case .skipNilValues: + wrappedDictionary[wrappingKey] = try self.wrap(value: property.value, propertyName: propertyName) + case .explicitlyPutNull: + wrappedDictionary[wrappingKey] = WrapNull.null + } + } else { + wrappedDictionary[wrappingKey] = try self.wrap(value: property.value, propertyName: propertyName) + } } } } diff --git a/Tests/WrapTests/WrapTests.swift b/Tests/WrapTests/WrapTests.swift index 1da92a3..13a8a15 100644 --- a/Tests/WrapTests/WrapTests.swift +++ b/Tests/WrapTests/WrapTests.swift @@ -65,6 +65,30 @@ class WrapTests: XCTestCase { } } + func testOptionalPropertiesWithExplicitlyNull() { + struct Model: WrapCustomizable { + let string: String? = "A string" + let int: Int? = 5 + let missing: String? = nil + let missingNestedOptional: Optional> = .some(.none) + + var wrapNilStyle: WrapNilStyle { + return .explicitlyPutNull + } + } + + do { + try verify(dictionary: wrap(Model()), againstDictionary: [ + "string" : "A string", + "int" : 5, + "missing": WrapNull.null, + "missingNestedOptional": WrapNull.null + ]) + } catch { + XCTFail(error.toString()) + } + } + func testSpecificNonOptionalProperties() { struct Model { let some: String = "value" @@ -281,6 +305,34 @@ class WrapTests: XCTestCase { } } + func testNestedEmptyStructWithExcplicitNull() { + struct Empty {} + + struct EmptyWithOptional: WrapCustomizable { + let optional: String? = nil + + var wrapNilStyle: WrapNilStyle { + return .explicitlyPutNull + } + } + + struct Model { + let empty = Empty() + let emptyWithOptional = EmptyWithOptional() + } + + do { + try verify(dictionary: wrap(Model()), againstDictionary: [ + "empty" : [:], + "emptyWithOptional" : [ + "optional": WrapNull.null + ] + ]) + } catch { + XCTFail(error.toString()) + } + } + func testArrayProperties() { struct Model { let homogeneous = ["Wrap", "Tests"] @@ -297,6 +349,26 @@ class WrapTests: XCTestCase { } } + func testArrayPropertiesExplicitlyNull() { + struct Model: WrapCustomizable { + let homogeneous = ["Wrap", "Tests"] + let mixed = ["Wrap", 15, 8.3, Optional.none] as [Any] + + var wrapNilStyle: WrapNilStyle { + return .explicitlyPutNull + } + } + + do { + try verify(dictionary: wrap(Model()), againstDictionary: [ + "homogeneous" : ["Wrap", "Tests"], + "mixed" : ["Wrap", 15, 8.3, WrapNull.null] + ]) + } catch { + XCTFail(error.toString()) + } + } + func testDictionaryProperties() { struct Model { let homogeneous = [ @@ -336,6 +408,51 @@ class WrapTests: XCTestCase { } } + func testDictionaryPropertiesExplicitlyNull() { + struct Model: WrapCustomizable { + let homogeneous = [ + "Key1" : "Value1", + "Key2" : "Value2" + ] + + let mixed: WrappedDictionary = [ + "Key1" : 15, + "Key2" : 19.2, + "Key3" : "Value", + "Key4" : ["Wrap", "Tests"], + "Key5" : [ + "NestedKey" : "NestedValue" + ], + "Key6": Optional.none + ] + + var wrapNilStyle: WrapNilStyle { + return .explicitlyPutNull + } + } + + do { + try verify(dictionary: wrap(Model()), againstDictionary: [ + "homogeneous" : [ + "Key1" : "Value1", + "Key2" : "Value2" + ], + "mixed" : [ + "Key1" : 15, + "Key2" : 19.2, + "Key3" : "Value", + "Key4" : ["Wrap", "Tests"], + "Key5" : [ + "NestedKey" : "NestedValue" + ], + "Key6" : WrapNull.null + ] + ]) + } catch { + XCTFail(error.toString()) + } + } + func testHomogeneousSetProperty() { struct Model { let set: Set = ["Wrap", "Tests"] @@ -779,6 +896,35 @@ class WrapTests: XCTestCase { } } + func testCustomWrappingForSinglePropertyExplicitlyNull() { + struct Model: WrapCustomizable { + let string = "Hello" + let int = 16 + + func wrap(propertyNamed propertyName: String, originalValue: Any, context: Any?, dateFormatter: DateFormatter?) throws -> Any? { + if propertyName == "int" { + XCTAssertEqual((originalValue as? Int) ?? 0, self.int) + return 27 + } + + return nil + } + + var wrapNilStyle: WrapNilStyle { + return .explicitlyPutNull + } + } + + do { + try verify(dictionary: wrap(Model()), againstDictionary: [ + "string" : WrapNull.null, + "int" : 27 + ]) + } catch { + XCTFail(error.toString()) + } + } + func testCustomWrappingFailureThrows() { struct Model: WrapCustomizable { func wrap(context: Any?, dateFormatter: DateFormatter?) -> Any? { @@ -850,6 +996,37 @@ class WrapTests: XCTestCase { } } + func testDataWrappingExplicitlyNull() { + struct Model: WrapCustomizable { + let string = "A string" + let int = 42 + let array = [4, 1, 9] + let optional: String? = nil + + var wrapNilStyle: WrapNilStyle { + return .explicitlyPutNull + } + } + + do { + let data: Data = try wrap(Model()) + let object = try JSONSerialization.jsonObject(with: data, options: []) + + guard let dictionary = object as? WrappedDictionary else { + return XCTFail("Invalid encoded type") + } + + try verify(dictionary: dictionary, againstDictionary: [ + "string" : "A string", + "int" : 42, + "array" : [4, 1, 9], + "optional": WrapNull.null + ]) + } catch { + XCTFail(error.toString()) + } + } + func testWrappingArray() { struct Model { let string: String @@ -1161,6 +1338,17 @@ private func verify(array: [Any], againstArray expectedArray: [Any]) throws { } private func verify(value: Any, againstValue expectedValue: Any, convertToObjectiveCObjectIfNeeded: Bool = true) throws { + // Casting Any to Optional + // https://stackoverflow.com/a/32355277/3815843 + func castToOptional(x: Any) -> T? { + return x as? T + } + + // First we check special case when nil == WrapNull + if expectedValue is WrapNull && castToOptional(x: value) == Optional.none { + return + } + guard let expectedVerifiableValue = expectedValue as? Verifiable else { throw VerificationError.cannotVerifyValue(expectedValue) }