Skip to content
This repository has been archived by the owner on May 20, 2021. It is now read-only.

Allowing explicitly putting null #52

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions Sources/Wrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*
Expand All @@ -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 }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, using bool value provides easier integration but using enum makes possible to add more options in future

/**
* Override the wrapping process for this type
*
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -400,6 +435,13 @@ private extension Wrapper {

func wrap<T>(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)
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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)
}
}
}
}
Expand Down
188 changes: 188 additions & 0 deletions Tests/WrapTests/WrapTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Optional<String>> = .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"
Expand Down Expand Up @@ -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"]
Expand All @@ -297,6 +349,26 @@ class WrapTests: XCTestCase {
}
}

func testArrayPropertiesExplicitlyNull() {
struct Model: WrapCustomizable {
let homogeneous = ["Wrap", "Tests"]
let mixed = ["Wrap", 15, 8.3, Optional<String>.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 = [
Expand Down Expand Up @@ -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<String>.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<String> = ["Wrap", "Tests"]
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<T>(x: Any) -> T? {
return x as? T
}

// First we check special case when nil == WrapNull
if expectedValue is WrapNull && castToOptional(x: value) == Optional<String>.none {
return
}

guard let expectedVerifiableValue = expectedValue as? Verifiable else {
throw VerificationError.cannotVerifyValue(expectedValue)
}
Expand Down