Skip to content

Map to optional type array #1

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

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
28 changes: 16 additions & 12 deletions Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -113,6 +114,7 @@
4DE9B52D1DD5E811005CB994 /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = Carthage/Build/iOS/ReactiveSwift.framework; sourceTree = "<group>"; };
4DE9B52E1DD5E811005CB994 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Result.framework; path = Carthage/Build/iOS/Result.framework; sourceTree = "<group>"; };
4DE9B5361DD5E88C005CB994 /* ReactiveMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveMapper.swift; sourceTree = "<group>"; };
BC86B7151E51D8A900094ABD /* tasks_null_object.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tasks_null_object.json; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -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 = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0810"
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
86 changes: 86 additions & 0 deletions ReactiveMapper/ReactiveMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,58 @@ extension SignalProtocol where Value == Any {
}
}

/// 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 Signal emitting an array of decoded objects.
public func mapToOptionalTypeArray<T: Mappable>(_ 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
Expand Down Expand Up @@ -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<T: Mappable>(_ type: T.Type, rootKeys: [String]? = nil, innerRootKeys: [String]? = nil) -> SignalProducer<[T?], ReactiveMapperError> {
return lift { $0.mapToOptionalTypeArray(type, rootKeys: rootKeys, innerRootKeys: innerRootKeys) }
}

}


Expand Down
2 changes: 1 addition & 1 deletion ReactiveMapperTests/MockDataLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MockDataLoader {
}

func array(_ fileName: String) -> SignalProducer<Any, NSError> {
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)
}

Expand Down
25 changes: 19 additions & 6 deletions ReactiveMapperTests/ReactiveMapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ReactiveSwift
class ReactiveMapperTests: XCTestCase {

let mockData = MockDataLoader()

func testMapToObject() {
var user: User?
mockData.dictionary("user")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
}

Expand All @@ -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")
}

Expand All @@ -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")
}

Expand All @@ -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")
}

Expand Down
12 changes: 12 additions & 0 deletions ReactiveMapperTests/json/tasks_null_object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
null,
{
"name": "Task 1"
},
{
"name": "Task 2"
},
{
"name": "Task 3"
}
]