Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allowing explicitly putting null #52

Open
wants to merge 1 commit into
base: master
from
Open
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -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 }

This comment has been minimized.

Copy link
@GRiMe2D

GRiMe2D Jan 18, 2018

Author

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
*
@@ -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<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)
}

@@ -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)
}
}
}
}
@@ -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"
@@ -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<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 = [
@@ -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"]
@@ -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<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)
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.