Skip to content

Commit

Permalink
fix(iOS): preserve null values in bridged types (#4072)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikeith authored Jan 21, 2021
1 parent 194ae86 commit 6dc691e
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 11 deletions.
22 changes: 12 additions & 10 deletions ios/Capacitor/Capacitor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
62E0736325535E8700BAAADB /* flat.json in CopyFiles */ = {isa = PBXBuildFile; fileRef = 62E0735425535E6500BAAADB /* flat.json */; };
62E0736425535E8700BAAADB /* hierarchy.json in CopyFiles */ = {isa = PBXBuildFile; fileRef = 62E0735525535E6500BAAADB /* hierarchy.json */; };
62E207AE2588234500A78983 /* WebViewDelegationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E207AD2588234500A78983 /* WebViewDelegationHandler.swift */; };
62FABD1A25AE5C01007B3814 /* Array+Capacitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */; };
62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */; };
62FABD2B25AE6182007B3814 /* BridgedTypesHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -151,7 +154,6 @@
625AF1EC258963C700869675 /* WebViewAssetHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebViewAssetHandler.swift; sourceTree = "<group>"; };
62959AE22524DA7700A3D7F1 /* CAPPluginCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPPluginCall.h; sourceTree = "<group>"; };
62959AE32524DA7700A3D7F1 /* JSExport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSExport.swift; sourceTree = "<group>"; };
62959AE42524DA7700A3D7F1 /* CAPBridgedJSTypes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CAPBridgedJSTypes.m; sourceTree = "<group>"; };
62959AE52524DA7700A3D7F1 /* CAPBridgedPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAPBridgedPlugin.h; sourceTree = "<group>"; };
62959AE62524DA7700A3D7F1 /* CAPPluginCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPPluginCall.swift; sourceTree = "<group>"; };
62959AE72524DA7700A3D7F1 /* CAPFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAPFile.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -197,6 +199,9 @@
62E0735425535E6500BAAADB /* flat.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = flat.json; sourceTree = "<group>"; };
62E0735525535E6500BAAADB /* hierarchy.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = hierarchy.json; sourceTree = "<group>"; };
62E207AD2588234500A78983 /* WebViewDelegationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDelegationHandler.swift; sourceTree = "<group>"; };
62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Capacitor.swift"; sourceTree = "<group>"; };
62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BridgedTypesTests.m; sourceTree = "<group>"; };
62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BridgedTypesHelper.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -245,7 +250,6 @@
6296A77C253A2E49005A202A /* TestsHostApp */,
50503EE01FC08594003606DC /* Products */,
501CBAA51FC0A723009B0D4D /* Frameworks */,
621ECCD12542059800D3D615 /* Recovered References */,
);
sourceTree = "<group>";
};
Expand All @@ -264,7 +268,9 @@
children = (
50503EED1FC08595003606DC /* CapacitorTests.swift */,
621ECCC2254204B700D3D615 /* BridgedTypesTests.swift */,
62FABD2225AE60BA007B3814 /* BridgedTypesTests.m */,
62A91C3325535F5700861508 /* ConfigurationTests.swift */,
62FABD2A25AE6182007B3814 /* BridgedTypesHelper.swift */,
621ECCC7254204BE00D3D615 /* JSONSerializationWrapper.h */,
621ECCC6254204BE00D3D615 /* JSONSerializationWrapper.m */,
621ECCCD254204C400D3D615 /* CapacitorTests-Bridging-Header.h */,
Expand All @@ -273,14 +279,6 @@
path = CapacitorTests;
sourceTree = "<group>";
};
621ECCD12542059800D3D615 /* Recovered References */ = {
isa = PBXGroup;
children = (
62959AE42524DA7700A3D7F1 /* CAPBridgedJSTypes.m */,
);
name = "Recovered References";
sourceTree = "<group>";
};
62959AE12524DA7700A3D7F1 /* Capacitor */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -321,6 +319,7 @@
62959B152524DA7700A3D7F1 /* CAPNotifications.swift */,
62959B072524DA7700A3D7F1 /* CapacitorExtension.swift */,
62959B112524DA7700A3D7F1 /* Data+Capacitor.swift */,
62FABD1925AE5C01007B3814 /* Array+Capacitor.swift */,
62D43AEF2581817500673C24 /* WKWebView+Capacitor.swift */,
62D43B642582A13D00673C24 /* WKWebView+Capacitor.m */,
62959AE92524DA7700A3D7F1 /* UIColor.swift */,
Expand Down Expand Up @@ -573,6 +572,7 @@
62959B302524DA7800A3D7F1 /* UIStatusBarManager+CAPHandleTapAction.m in Sources */,
62959B392524DA7800A3D7F1 /* CapacitorExtension.swift in Sources */,
62959B422524DA7800A3D7F1 /* DocLinks.swift in Sources */,
62FABD1A25AE5C01007B3814 /* Array+Capacitor.swift in Sources */,
62959B172524DA7800A3D7F1 /* JSExport.swift in Sources */,
373A69C1255C9360000A6F44 /* NotificationHandlerProtocol.swift in Sources */,
625AF1ED258963C700869675 /* WebViewAssetHandler.swift in Sources */,
Expand All @@ -594,8 +594,10 @@
buildActionMask = 2147483647;
files = (
50503EEE1FC08595003606DC /* CapacitorTests.swift in Sources */,
62FABD2B25AE6182007B3814 /* BridgedTypesHelper.swift in Sources */,
621ECCC8254204BE00D3D615 /* JSONSerializationWrapper.m in Sources */,
62A91C3425535F5700861508 /* ConfigurationTests.swift in Sources */,
62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */,
621ECCC3254204B700D3D615 /* BridgedTypesTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
31 changes: 31 additions & 0 deletions ios/Capacitor/Capacitor/Array+Capacitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// convenience wrappers to transform Arrays between NSNull and Optional values, for interoperability with Obj-C
extension Array: CapacitorExtension {}
extension CapacitorExtensionTypeWrapper where T == Array<JSValue> {
public func replacingNullValues() -> Array<JSValue?> {
return baseType.map({ (value) -> JSValue? in
if value is NSNull {
return nil
}
return value
})
}

public func replacingOptionalValues() -> Array<JSValue> {
return baseType
}
}

extension CapacitorExtensionTypeWrapper where T == Array<JSValue?> {
public func replacingNullValues() -> Array<JSValue?> {
return baseType
}

public func replacingOptionalValues() -> Array<JSValue> {
return baseType.map({ (value) -> JSValue in
if let value = value {
return value
}
return NSNull()
})
}
}
6 changes: 5 additions & 1 deletion ios/Capacitor/Capacitor/CAPPluginCall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ extension CAPPluginCall: JSValueContainer {
}

@objc public extension CAPPluginCall {
@available(*, deprecated, message: "Presence of a key should not be considered significant. Use typed accessors to check the value instead.")
func hasOption(_ key: String) -> Bool {
return self.options.index(forKey: key) != nil
guard let value = options[key] else {
return false
}
return !(value is NSNull)
}

@available(*, deprecated, renamed: "resolve()")
Expand Down
7 changes: 7 additions & 0 deletions ios/Capacitor/Capacitor/JSTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extension Int: JSValue {}
extension Float: JSValue {}
extension Double: JSValue {}
extension NSNumber: JSValue {}
extension NSNull: JSValue {}
extension Array: JSValue {}
extension Date: JSValue {}
extension Dictionary: JSValue where Key == String, Value == JSValue {}
Expand Down Expand Up @@ -183,6 +184,10 @@ extension JSTypes {
public static func coerceDictionaryToJSObject(_ dictionary: [AnyHashable: Any]?) -> JSObject? {
return coerceToJSValue(dictionary) as? JSObject
}

public static func coerceArrayToJSArray(_ array: [Any]?) -> JSArray? {
return array?.compactMap { coerceToJSValue($0) }
}
}

private func coerceToJSValue(_ value: Any?) -> JSValue? {
Expand All @@ -204,6 +209,8 @@ private func coerceToJSValue(_ value: Any?) -> JSValue? {
return doubleValue
case let dateValue as Date:
return dateValue
case let nullValue as NSNull:
return nullValue
case let arrayValue as NSArray:
return arrayValue.compactMap { coerceToJSValue($0) }
case let dictionaryValue as NSDictionary:
Expand Down
31 changes: 31 additions & 0 deletions ios/Capacitor/CapacitorTests/BridgedTypesHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
@testable import Capacitor

enum BridgeTypeError: Error {
case badCast
}

@objc class BridgedTypesHelper: NSObject {
@objc static let shared = BridgedTypesHelper()

var untypedArray: [Any] {
return []
}

@objc func validTransformationOf(array: [Any]) -> [Any] {
let result = JSTypes.coerceArrayToJSArray(array)!.capacitor.replacingNullValues()
return result.capacitor.replacingOptionalValues() as [Any]
}

@objc func invalidTransformationOf(array: [Any]) -> [Any] {
let result = JSTypes.coerceArrayToJSArray(array)!.capacitor.replacingNullValues()
return result as [Any]
}

@objc func testCast(of array: [Any], atIndex index: Int) throws -> Any {
if let castArray = array as? [JSValue] {
return castArray[index] as Any
}
throw BridgeTypeError.badCast
}
}
54 changes: 54 additions & 0 deletions ios/Capacitor/CapacitorTests/BridgedTypesTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#import <XCTest/XCTest.h>
#import <Capacitor/Capacitor.h>
// forward declaration of internal capacitor classes that are exposed in the swift header via the @testable import.
@interface CAPWebViewAssetHandler: NSObject
@end
@interface CapacitorBridge: NSObject
@end
@interface CAPWebViewDelegationHandler: NSObject
@end
// import that will fail without the declarations
#import "CapacitorTests-Swift.h"

// interface for this class
@interface BridgedTypesTestsObjc : XCTestCase
@end

@implementation BridgedTypesTestsObjc

- (void)setUp {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

- (void)testNullHandling {
NSArray* source = @[@"test", [NSNull null], @3];
NSArray* result = [[BridgedTypesHelper shared] validTransformationOfArray:source];
NSError *error = nil;
// test that the replaced null value exists
id value = [result objectAtIndex:1];
XCTAssertNotNil(value);
XCTAssertTrue([value isKindOfClass:[NSNull class]]);
// test that the null value casts to non-optional
value = [[BridgedTypesHelper shared] testCastOf:result atIndex:1 error:&error];
XCTAssertNotNil(value);
XCTAssertNil(error);
}

- (void)testOptionalHandling {
NSArray* source = @[@"test", [NSNull null], @3];
NSArray* result = [[BridgedTypesHelper shared] invalidTransformationOfArray:source];
NSError *error = nil;
// test that the removed null value, now optional, is automatically transformed back into a NSNull
id value = [result objectAtIndex:1];
XCTAssertNotNil(value);
XCTAssertTrue([value isKindOfClass:[NSNull class]]);
// test that the optional value fails to cast to non-optional
value = [[BridgedTypesHelper shared] testCastOf:result atIndex:1 error:&error];
XCTAssertNil(value);
XCTAssertNotNil(error);
}
@end
39 changes: 39 additions & 0 deletions ios/Capacitor/CapacitorTests/BridgedTypesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,43 @@ class BridgedTypesTests: XCTestCase {
let coercedFloat = coercedResult["testFloat"]!
XCTAssertTrue(type(of: coercedFloat) == underlyingType.self)
}

func testNullWrapping() throws {
let dictionary: [AnyHashable: Any] = ["testInt": 1 as Int, "testNull": NSNull()]
let coercedDictionary = JSTypes.coerceDictionaryToJSObject(dictionary)!
XCTAssertNotNil(coercedDictionary)
XCTAssertEqual(coercedDictionary.count, 2)
XCTAssertTrue(coercedDictionary["testNull"]! is NSNull)
}

func testNullTransformation() throws {
let array: [Any] = [1, NSNull(), "test string"]
let coercedArray = JSTypes.coerceArrayToJSArray(array)!
XCTAssertNotNil(coercedArray)
XCTAssertEqual(coercedArray.count, 3)
XCTAssertTrue(type(of: coercedArray[1]) == NSNull.self)
let filteredArray = coercedArray.capacitor.replacingNullValues()
XCTAssertEqual(filteredArray.count, 3)
XCTAssertNil(filteredArray[1])
let restoredArray = filteredArray.capacitor.replacingOptionalValues()
XCTAssertEqual(restoredArray.count, 3)
XCTAssertNotNil(restoredArray[1])
XCTAssertTrue(restoredArray[0] is NSNumber)
XCTAssertTrue(restoredArray[1] is NSNull)
XCTAssertTrue(restoredArray[2] is String)
}

func testSparseArrayCastSuccess() throws {
let array: [Any] = ["test string 1", "test string 2", NSNull()]
let sparseArray = JSTypes.coerceArrayToJSArray(array)?.capacitor.replacingNullValues() as? [String?]
XCTAssertNotNil(sparseArray)
XCTAssertEqual(sparseArray!.count, 3)
XCTAssertNil(sparseArray![2])
}

func testSparseArrayCastFailure() throws {
let array: [Any] = ["test string 1", 1, NSNull()]
let sparseArray = JSTypes.coerceArrayToJSArray(array)?.capacitor.replacingNullValues() as? [String?]
XCTAssertNil(sparseArray)
}
}

0 comments on commit 6dc691e

Please sign in to comment.