diff --git a/.gitignore b/.gitignore index 9724b3c..5b75ffa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore diff --git a/Marshal.xcodeproj/project.pbxproj b/Marshal.xcodeproj/project.pbxproj index b0c7481..9f40e7b 100644 --- a/Marshal.xcodeproj/project.pbxproj +++ b/Marshal.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 459FC6BD1DC28F0000BD9DAC /* TestMissingData.json in Resources */ = {isa = PBXBuildFile; fileRef = 459FC6BC1DC28F0000BD9DAC /* TestMissingData.json */; }; 7112BA9B1C7ECD5600B657F9 /* People.json in Resources */ = {isa = PBXBuildFile; fileRef = 7112BA971C7ECD5600B657F9 /* People.json */; }; 7112BA9C1C7ECD5600B657F9 /* TestDictionary.json in Resources */ = {isa = PBXBuildFile; fileRef = 7112BA981C7ECD5600B657F9 /* TestDictionary.json */; }; 7112BA9D1C7ECD5600B657F9 /* TestObjectArray.json in Resources */ = {isa = PBXBuildFile; fileRef = 7112BA991C7ECD5600B657F9 /* TestObjectArray.json */; }; @@ -44,6 +45,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 459FC6BC1DC28F0000BD9DAC /* TestMissingData.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestMissingData.json; sourceTree = ""; }; 7112BA971C7ECD5600B657F9 /* People.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = People.json; sourceTree = ""; }; 7112BA981C7ECD5600B657F9 /* TestDictionary.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestDictionary.json; sourceTree = ""; }; 7112BA991C7ECD5600B657F9 /* TestObjectArray.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestObjectArray.json; sourceTree = ""; }; @@ -140,6 +142,7 @@ 7112BA991C7ECD5600B657F9 /* TestObjectArray.json */, 7112BA9A1C7ECD5600B657F9 /* TestSimpleArray.json */, AE5720E61C7FE97E00D45DE3 /* TestSimpleSet.json */, + 459FC6BC1DC28F0000BD9DAC /* TestMissingData.json */, AFBED25E1C7E1AAB00622331 /* MarshalTests.swift */, AFBED2601C7E1AAB00622331 /* Info.plist */, 71EFC3A71CCB4EAF00394E57 /* PerformanceTestObjects.swift */, @@ -253,6 +256,7 @@ AE5720E71C7FE97E00D45DE3 /* TestSimpleSet.json in Resources */, 71EFC3AA1CCB505C00394E57 /* Large.json in Resources */, 7112BA9E1C7ECD5600B657F9 /* TestSimpleArray.json in Resources */, + 459FC6BD1DC28F0000BD9DAC /* TestMissingData.json in Resources */, 7112BA9C1C7ECD5600B657F9 /* TestDictionary.json in Resources */, 7112BA9D1C7ECD5600B657F9 /* TestObjectArray.json in Resources */, ); diff --git a/MarshalTests/MarshalTests.swift b/MarshalTests/MarshalTests.swift index d1c07d2..53b27f8 100644 --- a/MarshalTests/MarshalTests.swift +++ b/MarshalTests/MarshalTests.swift @@ -383,6 +383,49 @@ class MarshalTests: XCTestCase { } } + func testArraysWithOptionalObjects() { + guard let path = Bundle(for: type(of: self)).path(forResource: "TestMissingData", ofType: "json"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let json = try? JSONParser.JSONObjectWithData(data) else { + XCTFail("Error parsing TestMissingData.json") + return + } + + // Using normal parsing, if any of the objects fail initialization they all fail + let failedArrayOfCars: [Car]? = try? json.value(for: "cars") + XCTAssertNil(failedArrayOfCars, "failedArrayOfCars should be nil") + + let optionalArrayOfOptionalCars: [Car?]? = try? json.value(for: "cars") + XCTAssertNotNil(optionalArrayOfOptionalCars, "optionalArrayOfOptionalCars should not be nil") + XCTAssert(optionalArrayOfOptionalCars?.count == 8, "optionalArrayOfOptionalCars should have 8 objects. Actual count = \(optionalArrayOfOptionalCars?.count)") + XCTAssert(optionalArrayOfOptionalCars?.contains(where: { $0?.make == "Lexus" }) == false, "optionalArrayOfOptionalCars should not contain a Lexus because the Lexus was malformed") + XCTAssert(optionalArrayOfOptionalCars?[1] == nil, "optionalArrayOfOptionalCars[1] should be nil") + + do { + let arrayOfOptionalCars: [Car?] = try json.value(for: "cars") + + XCTAssertNotNil(arrayOfOptionalCars, "arrayOfOptionalCars should not be nil") + XCTAssert(arrayOfOptionalCars.count == 8, "arrayOfOptionalCars should have 8 objects. Actual count = \(optionalArrayOfOptionalCars?.count)") + XCTAssert(arrayOfOptionalCars.contains(where: { $0?.make == "Lexus" }) == false, "arrayOfOptionalCars should not contain a Lexus because the Lexus was malformed") + XCTAssert(arrayOfOptionalCars[1] == nil, "arrayOfOptionalCars[1] should be nil") + } + catch { + XCTFail("error marshaling arrayOfOptionalCars: \(error)") + } + } + + func testDiscardingErrors() { + guard let path = Bundle(for: type(of: self)).path(forResource: "TestMissingData", ofType: "json"), + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let json = try? JSONParser.JSONObjectWithData(data) else { + XCTFail("Error parsing TestMissingData.json") + return + } + + let arrayOfCarsWithoutInvalidObjects: [Car]? = try? json.value(for: "cars", discardingErrors: true) + XCTAssert(arrayOfCarsWithoutInvalidObjects?.count == 5, "arrayOfCarsWithoutInvalidObjects should have 5 objects. Actual count = \(arrayOfCarsWithoutInvalidObjects?.count)") + XCTAssert(arrayOfCarsWithoutInvalidObjects?.contains(where: { $0.make == "Lexus" }) == false, "arrayOfCarsWithoutInvalidObjects should not contain a Lexus because the Lexus was malformed") + } } private struct Address: Unmarshaling { @@ -399,6 +442,7 @@ private struct Person: Unmarshaling { let lastName:String let score:Int let address:Address? + init(object json: MarshaledObject) throws { firstName = try json.value(for: "first") lastName = try json.value(for: "last") @@ -413,3 +457,13 @@ private struct AgedPerson: Unmarshaling { age = try object.value(for: "age") } } + +private struct Car: Unmarshaling { + let make: String + let model: String + + init(object: MarshaledObject) throws { + make = try object.value(for: "make") + model = try object.value(for: "model") + } +} diff --git a/MarshalTests/TestMissingData.json b/MarshalTests/TestMissingData.json new file mode 100644 index 0000000..9e2cb12 --- /dev/null +++ b/MarshalTests/TestMissingData.json @@ -0,0 +1,12 @@ +{ + "cars": [ + {"make": "Audi", "model": "A4"}, + {"make": null, "model": "None"}, + {"make": "Audi", "model": "A7"}, + {"make": "BMW", "model": 240}, + {"make": "Jeep", "model": "Wrangler"}, + {"make": "Lexus"}, + {"make": "VW", "model": "CC"}, + {"make": "VW", "model": "Tiguan"} + ] +} diff --git a/Sources/MarshaledObject.swift b/Sources/MarshaledObject.swift index ea23cb7..59a5ad8 100644 --- a/Sources/MarshaledObject.swift +++ b/Sources/MarshaledObject.swift @@ -64,7 +64,17 @@ public extension MarshaledObject { } } - public func value(for key: KeyType) throws -> [A] { + public func value(for key: KeyType, discardingErrors: Bool = false) throws -> [A] { + let any = try self.any(for: key) + do { + return try Array.value(from: any, discardingErrors: discardingErrors) + } + catch let MarshalError.typeMismatch(expected: expected, actual: actual) { + throw MarshalError.typeMismatchWithKey(key: key.stringValue, expected: expected, actual: actual) + } + } + + public func value(for key: KeyType) throws -> [A?] { let any = try self.any(for: key) do { return try Array.value(from: any) @@ -73,8 +83,20 @@ public extension MarshaledObject { throw MarshalError.typeMismatchWithKey(key: key.stringValue, expected: expected, actual: actual) } } + + public func value(for key: KeyType, discardingErrors: Bool = false) throws -> [A]? { + do { + return try self.value(for: key, discardingErrors: discardingErrors) as [A] + } + catch MarshalError.keyNotFound { + return nil + } + catch MarshalError.nullValue { + return nil + } + } - public func value(for key: KeyType) throws -> [A]? { + public func value(for key: KeyType) throws -> [A?]? { do { return try self.value(for: key) as [A] } diff --git a/Sources/ValueType.swift b/Sources/ValueType.swift index e147c05..d13007e 100644 --- a/Sources/ValueType.swift +++ b/Sources/ValueType.swift @@ -49,14 +49,39 @@ extension Int64: ValueType { } extension Array where Element: ValueType { - public static func value(from object: Any) throws -> [Element] { + public static func value(from object: Any, discardingErrors: Bool = false) throws -> [Element] { guard let anyArray = object as? [AnyObject] else { throw MarshalError.typeMismatch(expected: self, actual: type(of: object)) } - return try anyArray.map { - let value = try Element.value(from: $0) + + if discardingErrors { + return anyArray.flatMap { + let value = try? Element.value(from: $0) + guard let element = value as? Element else { + return nil + } + return element + } + } + else { + return try anyArray.map { + let value = try Element.value(from: $0) + guard let element = value as? Element else { + throw MarshalError.typeMismatch(expected: Element.self, actual: type(of: value)) + } + return element + } + } + } + + public static func value(from object: Any) throws -> [Element?] { + guard let anyArray = object as? [AnyObject] else { + throw MarshalError.typeMismatch(expected: self, actual: type(of: object)) + } + return anyArray.map { + let value = try? Element.value(from: $0) guard let element = value as? Element else { - throw MarshalError.typeMismatch(expected: Element.self, actual: type(of: value)) + return nil } return element } @@ -74,7 +99,7 @@ extension Dictionary: ValueType { extension Set where Element: ValueType { public static func value(from object: Any) throws -> Set { - let elementArray = try [Element].value(from: object) + let elementArray: [Element] = try [Element].value(from: object) return Set(elementArray) } }