diff --git a/CHANGELOG.md b/CHANGELOG.md index 9107979..ba14e3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ - Add `@noescape` to transformation closures [Keith Smiley](https://github.com/keith) [#60](https://github.com/lyft/mapper/pull/60) +- Add `from` for arrays of `RawRepresentable`s + [Keith Smiley](https://github.com/keith) + [#61](https://github.com/lyft/mapper/pull/61) ## Bug Fixes diff --git a/Makefile b/Makefile index 2421c58..5671f00 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ install-tvOS: true install-lint: + brew remove swiftlint --force || true brew install https://raw.githubusercontent.com/Homebrew/homebrew/fffa4b271ba57c7633e8e24cae543a197a9e3e01/Library/Formula/swiftlint.rb install-carthage: diff --git a/Sources/Mapper.swift b/Sources/Mapper.swift index 160f4f4..4cc8d3b 100644 --- a/Sources/Mapper.swift +++ b/Sources/Mapper.swift @@ -82,6 +82,33 @@ public struct Mapper { return nil } + /** + Get an array of RawRepresentable values from a field in the the source data. + + - note: If T.init(rawValue:) fails given the T.RawValue from the array of source data, that value will be + replaced by the passed defaultValue, which defaults to nil. The resulting array is flatMapped and + all nils are removed. This means that any unrecognized values will be removed or replaced with a + default. This ensures backwards compatibility if your source data has keys that your mapping + layer doesn't know about yet. + + - parameter field: The field to use from the source data + - parameter defaultValue: The value to use if the rawValue initializer fails + + - returns: An array of the RawRepresentable value, with all nils removed + */ + @warn_unused_result + public func from(field: String, defaultValue: T? = nil) throws -> [T] + { + let value = try self.JSONFromField(field) + guard let array = value as? [AnyObject] else { + throw MapperError.TypeMismatchError(field: field, value: value, type: [AnyObject].self) + } + + let rawValues = try array.map { try T.RawValue.fromMap($0) } + return rawValues.flatMap { T(rawValue: $0) ?? defaultValue } + } + // MARK: - T: Mappable /** @@ -112,7 +139,7 @@ public struct Mapper { This allows you to transparently have nested arrays of Mappable values - Note: If any value in the array of NSDictionaries is invalid, this method throws + - note: If any value in the array of NSDictionaries is invalid, this method throws - parameter field: The field to retrieve from the source data, can be an empty string to return the entire data set @@ -153,7 +180,7 @@ public struct Mapper { This allows you to transparently have nested arrays of Mappable values - Note: If any value in the provided array of NSDictionaries is invalid, this method returns nil + - note: If any value in the provided array of NSDictionaries is invalid, this method returns nil - parameter field: The field to retrieve from the source data, can be an empty string to return the entire data set diff --git a/Tests/Mapper/RawRepresentibleValueTests.swift b/Tests/Mapper/RawRepresentibleValueTests.swift index 61469a3..07c6ea8 100644 --- a/Tests/Mapper/RawRepresentibleValueTests.swift +++ b/Tests/Mapper/RawRepresentibleValueTests.swift @@ -129,4 +129,115 @@ final class RawRepresentibleValueTests: XCTestCase { let test = try! Test(map: Mapper(JSON: [:])) XCTAssertNil(test.value) } + + func testArrayOfValuesWithMissingKey() { + struct Test: Mappable { + let value: [Value] + init(map: Mapper) throws { + self.value = try map.from("a") + } + } + + enum Value: String { + case First = "hi" + } + + do { + _ = try Test(map: Mapper(JSON: [:])) + XCTFail("Expected initialization to fail") + } catch MapperError.MissingFieldError(let field) { + XCTAssertEqual(field, "a") + } catch let error { + XCTFail("Expected only missing field error, got \(error)") + } + } + + func testArrayOfValuesInvalidArray() { + struct Test: Mappable { + let values: [Value] + init(map: Mapper) throws { + self.values = try map.from("a") + } + } + + enum Value: String { + case First = "hi" + } + + do { + _ = try Test(map: Mapper(JSON: ["a": 1])) + XCTFail("Expected initialization to fail") + } catch MapperError.TypeMismatchError(let field, let value, let type) { + XCTAssertEqual(field, "a") + XCTAssertEqual(value as? Int, 1) + XCTAssert(type == [AnyObject].self) + } catch let error { + XCTFail("Expected only missing field error, got \(error)") + } + } + + func testArrayOfValuesFailedConvertible() { + struct Test: Mappable { + let values: [Value] + init(map: Mapper) throws { + self.values = try map.from("a") + } + } + + enum Value: String { + case First = "hi" + } + + do { + _ = try Test(map: Mapper(JSON: ["a": [1]])) + XCTFail("Expected initialization to fail") + } catch MapperError.ConvertibleError(let value, let type) { + XCTAssertEqual(value as? Int, 1) + XCTAssert(type == String.self) + } catch let error { + XCTFail("Expected only missing field error, got \(error)") + } + } + + func testArrayOfValuesFiltersNilsWithoutDefault() { + struct Test: Mappable { + let values: [Value] + init(map: Mapper) throws { + self.values = try map.from("a") + } + } + + enum Value: String { + case First = "hi" + } + + do { + let test = try Test(map: Mapper(JSON: ["a": ["hi", "invalid"]])) + XCTAssertEqual(test.values.count, 1) + XCTAssert(test.values.contains(.First)) + } catch let error { + XCTFail("Expected no errors, got \(error)") + } + } + + func testArrayOfValuesInsertsDefault() { + struct Test: Mappable { + let values: [Value] + init(map: Mapper) throws { + self.values = try map.from("a", defaultValue: .First) + } + } + + enum Value: String { + case First = "hi" + } + + do { + let test = try Test(map: Mapper(JSON: ["a": ["invalid"]])) + XCTAssertEqual(test.values.count, 1) + XCTAssert(test.values.contains(.First)) + } catch let error { + XCTFail("Expected no errors, got \(error)") + } + } }