diff --git a/Example.xcodeproj/project.pbxproj b/Example.xcodeproj/project.pbxproj index cc0694d..3d43c18 100644 --- a/Example.xcodeproj/project.pbxproj +++ b/Example.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 4DE9B5331DD5E829005CB994 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DE9B52D1DD5E811005CB994 /* ReactiveSwift.framework */; }; 4DE9B5341DD5E829005CB994 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DE9B52E1DD5E811005CB994 /* Result.framework */; }; 4DE9B5371DD5E88C005CB994 /* ReactiveMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE9B5361DD5E88C005CB994 /* ReactiveMapper.swift */; }; + BC86B7161E51D8A900094ABD /* tasks_null_object.json in Resources */ = {isa = PBXBuildFile; fileRef = BC86B7151E51D8A900094ABD /* tasks_null_object.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -113,6 +114,7 @@ 4DE9B52D1DD5E811005CB994 /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = Carthage/Build/iOS/ReactiveSwift.framework; sourceTree = ""; }; 4DE9B52E1DD5E811005CB994 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = Carthage/Build/iOS/Result.framework; sourceTree = ""; }; 4DE9B5361DD5E88C005CB994 /* ReactiveMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveMapper.swift; sourceTree = ""; }; + BC86B7151E51D8A900094ABD /* tasks_null_object.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tasks_null_object.json; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -213,15 +215,16 @@ 4D565F3A1DD9E4E9006E9B57 /* json */ = { isa = PBXGroup; children = ( - 4D565F3B1DD9E4E9006E9B57 /* tasks.json */, - 4D565F3C1DD9E4E9006E9B57 /* tasks_invalid.json */, - 4D565F3D1DD9E4E9006E9B57 /* tasks_rootkey.json */, - 4D1D1A251DDB30E000CDE961 /* tasks_multiplerootkey.json */, 4D565F4F1DD9E8AF006E9B57 /* tasks_innerrootkey.json */, + 4D565F3C1DD9E4E9006E9B57 /* tasks_invalid.json */, 4D1D1A241DDB30E000CDE961 /* tasks_multipleinnerrootkey.json */, - 4D565F3E1DD9E4E9006E9B57 /* user.json */, + 4D1D1A251DDB30E000CDE961 /* tasks_multiplerootkey.json */, + BC86B7151E51D8A900094ABD /* tasks_null_object.json */, + 4D565F3D1DD9E4E9006E9B57 /* tasks_rootkey.json */, + 4D565F3B1DD9E4E9006E9B57 /* tasks.json */, 4D565F3F1DD9E4E9006E9B57 /* user_invalid.json */, 4D565F401DD9E4E9006E9B57 /* user_rootkey.json */, + 4D565F3E1DD9E4E9006E9B57 /* user.json */, ); path = json; sourceTree = ""; @@ -323,17 +326,15 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0810; - LastUpgradeCheck = 0810; + LastUpgradeCheck = 0820; ORGANIZATIONNAME = "Alexander Schuch"; TargetAttributes = { 4D2C26181DD5E1A200A107BB = { CreatedOnToolsVersion = 8.1; - DevelopmentTeam = LSTNUPY836; ProvisioningStyle = Automatic; }; 4D2C26401DD5E1B700A107BB = { CreatedOnToolsVersion = 8.1; - DevelopmentTeam = LSTNUPY836; LastSwiftMigration = 0810; ProvisioningStyle = Automatic; }; @@ -392,6 +393,7 @@ 4D565F501DD9E8AF006E9B57 /* tasks_innerrootkey.json in Resources */, 4D565F441DD9E4E9006E9B57 /* user.json in Resources */, 4D565F421DD9E4E9006E9B57 /* tasks_invalid.json in Resources */, + BC86B7161E51D8A900094ABD /* tasks_null_object.json in Resources */, 4D565F451DD9E4E9006E9B57 /* user_invalid.json in Resources */, 4D565F411DD9E4E9006E9B57 /* tasks.json in Resources */, 4D565F431DD9E4E9006E9B57 /* tasks_rootkey.json in Resources */, @@ -508,6 +510,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -557,6 +560,7 @@ CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -586,7 +590,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = LSTNUPY836; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -604,7 +608,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = LSTNUPY836; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -624,7 +628,7 @@ CODE_SIGN_IDENTITY = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = LSTNUPY836; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -653,7 +657,7 @@ CODE_SIGN_IDENTITY = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = LSTNUPY836; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; diff --git a/Example.xcodeproj/xcshareddata/xcschemes/ReactiveMapper.xcscheme b/Example.xcodeproj/xcshareddata/xcschemes/ReactiveMapper.xcscheme index fa28523..88a6cd0 100644 --- a/Example.xcodeproj/xcshareddata/xcschemes/ReactiveMapper.xcscheme +++ b/Example.xcodeproj/xcshareddata/xcschemes/ReactiveMapper.xcscheme @@ -1,6 +1,6 @@ (_ type: T.Type, rootKeys: [String]? = nil, innerRootKeys: [String]? = nil) -> Signal<[T?], ReactiveMapperError> { + return signal + .mapError { ReactiveMapperError.underlying($0) } + .attemptMap { json -> Result<[T?], ReactiveMapperError> in + if let array = extract(json, rootKeys: rootKeys) as? [NSDictionary?] { + return unwrapThrowableResult { + try array.map { jsonObject in + guard let jsonObject = jsonObject else { return nil } + if let jsonObject = extract(jsonObject, rootKeys: innerRootKeys) as? NSDictionary { + return type.from(jsonObject) + } + throw MapperError.customError(field: "", message: "Could not parse inner object with root keys: \(innerRootKeys)") + } + } + } + + let info = [NSLocalizedFailureReasonErrorKey: "The provided `Value` could not be cast to `[NSDictionary]` or there is no array of values at the given `rootKeys`: \(rootKeys)"] + let error = NSError(domain: ReactiveMapperErrorDomain, code: -1, userInfo: info) + return .failure(.underlying(error)) + } + } + } // MARK: Signal @@ -136,6 +188,40 @@ extension SignalProducerProtocol where Value == Any { return lift { $0.mapToTypeArray(type, rootKeys: rootKeys, innerRootKeys: innerRootKeys) } } + /// Maps the given JSON object array within the stream to an array of optional objects of the given `type`. + /// + /// - parameter type: The type of the array that should be returned + /// - parameter rootKeys: An array of keys that should be traversed in order to find a nested JSON object. The resulting object is subsequently used for further decoding. + /// - parameter innerRootKeys: An array of keys that should traversed in order to find a nested JSON object. The resulting object is subsequently used for further decoding. + /// In contrast to the `rootKeys`, the `innerRootKeys` are applied on each nested array element and the resulting object is used for decoding. + /// For example, use .mapToType(User.self, rootKeys: ["outer"], innerRootKeys: ["inner"]) to decode the following JSON + /// ``` + /// { + /// "outer": [ + /// { + /// "inner": { "name": "Alex" } + /// }, + /// { + /// "inner": { "name": "Tom" } + /// } + /// ] + /// } + /// ``` + /// + /// Supports `null` objects like: + /// ``` + /// [ + /// null, + /// { "name: "Alex" }, + /// { "name: "Tom" } + /// ] + /// ``` + /// + /// - returns: A new SignalProducer emitting an array of decoded objects. + public func mapToOptionalTypeArray(_ type: T.Type, rootKeys: [String]? = nil, innerRootKeys: [String]? = nil) -> SignalProducer<[T?], ReactiveMapperError> { + return lift { $0.mapToOptionalTypeArray(type, rootKeys: rootKeys, innerRootKeys: innerRootKeys) } + } + } diff --git a/ReactiveMapperTests/MockDataLoader.swift b/ReactiveMapperTests/MockDataLoader.swift index ac2f263..e2cf554 100644 --- a/ReactiveMapperTests/MockDataLoader.swift +++ b/ReactiveMapperTests/MockDataLoader.swift @@ -17,7 +17,7 @@ class MockDataLoader { } func array(_ fileName: String) -> SignalProducer { - let json = try! JSONSerialization.jsonObject(with: self.jsonData(fileName), options: []) as! [[String: Any]] + let json = try! JSONSerialization.jsonObject(with: self.jsonData(fileName), options: []) as! [[String: Any]?] return SignalProducer(value: json) } diff --git a/ReactiveMapperTests/ReactiveMapperTests.swift b/ReactiveMapperTests/ReactiveMapperTests.swift index e5b8318..655436c 100644 --- a/ReactiveMapperTests/ReactiveMapperTests.swift +++ b/ReactiveMapperTests/ReactiveMapperTests.swift @@ -13,7 +13,7 @@ import ReactiveSwift class ReactiveMapperTests: XCTestCase { let mockData = MockDataLoader() - + func testMapToObject() { var user: User? mockData.dictionary("user") @@ -29,10 +29,23 @@ class ReactiveMapperTests: XCTestCase { .mapToTypeArray(Task.self) .startWithResult { tasks = $0.value } - XCTAssertNotNil(tasks, "mapToType should not return nil tasks") + XCTAssertNotNil(tasks, "mapToTypeArray should not return nil tasks") XCTAssertTrue((tasks!).count == 3, "mapJSON returned wrong number of tasks") } + func testMapToOptionalObjectArray() { + var tasks: [Task?]? + mockData.array("tasks_null_object") + .mapToOptionalTypeArray(Task.self) + .startWithResult { + print($0) + tasks = $0.value + } + + XCTAssertNotNil(tasks, "mapToOptionalTypeArray should not return nil tasks") + XCTAssertTrue((tasks!).count == 4, "mapJSON returned wrong number of tasks") + } + func testInvalidTasks() { var invalidTasks: [Task]? = nil mockData.array("tasks_invalid") @@ -78,7 +91,7 @@ class ReactiveMapperTests: XCTestCase { .mapToTypeArray(Task.self, rootKeys: ["tasks"]) .startWithResult { tasks = $0.value } - XCTAssertNotNil(tasks, "mapToType should not return nil tasks") + XCTAssertNotNil(tasks, "mapToTypeArray should not return nil tasks") XCTAssertTrue((tasks!).count == 3, "mapJSON returned wrong number of tasks") } @@ -88,7 +101,7 @@ class ReactiveMapperTests: XCTestCase { .mapToTypeArray(Task.self, rootKeys: ["taskList", "tasks"]) .startWithResult { tasks = $0.value } - XCTAssertNotNil(tasks, "mapToType should not return nil tasks") + XCTAssertNotNil(tasks, "mapToTypeArray should not return nil tasks") XCTAssertTrue((tasks!).count == 3, "mapJSON returned wrong number of tasks") } @@ -98,7 +111,7 @@ class ReactiveMapperTests: XCTestCase { .mapToTypeArray(Task.self, rootKeys: ["tasks"], innerRootKeys: ["task"]) .startWithResult { tasks = $0.value } - XCTAssertNotNil(tasks, "mapToType should not return nil tasks") + XCTAssertNotNil(tasks, "mapToTypeArray should not return nil tasks") XCTAssertTrue((tasks!).count == 3, "mapJSON returned wrong number of tasks") } @@ -108,7 +121,7 @@ class ReactiveMapperTests: XCTestCase { .mapToTypeArray(Task.self, rootKeys: ["taskList", "tasks"], innerRootKeys: ["task", "t"]) .startWithResult { tasks = $0.value } - XCTAssertNotNil(tasks, "mapToType should not return nil tasks") + XCTAssertNotNil(tasks, "mapToTypeArray should not return nil tasks") XCTAssertTrue((tasks!).count == 3, "mapJSON returned wrong number of tasks") } diff --git a/ReactiveMapperTests/json/tasks_null_object.json b/ReactiveMapperTests/json/tasks_null_object.json new file mode 100644 index 0000000..e0cbbe4 --- /dev/null +++ b/ReactiveMapperTests/json/tasks_null_object.json @@ -0,0 +1,12 @@ +[ + null, + { + "name": "Task 1" + }, + { + "name": "Task 2" + }, + { + "name": "Task 3" + } +]