diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index a4b703b62..bce7649ec 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -50,6 +50,25 @@ struct GameScore: ParseObject { } } +struct GameData: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties. + var polygon: ParsePolygon? + //: `ParseBytes` needs to be a part of the original schema + //: or else you will need your masterKey to force an upgrade. + var bytes: ParseBytes? + + init (bytes: ParseBytes?, polygon: ParsePolygon) { + self.bytes = bytes + self.polygon = polygon + } +} + //: Define initial GameScores. let score = GameScore(score: 10) let score2 = GameScore(score: 3) @@ -308,6 +327,24 @@ do { assertionFailure("Error deleting: \(error)") }*/ +//: How to add `ParseBytes` and `ParsePolygon` to objects. +let points = [ + try ParseGeoPoint(latitude: 0, longitude: 0), + try ParseGeoPoint(latitude: 0, longitude: 1), + try ParseGeoPoint(latitude: 1, longitude: 1), + try ParseGeoPoint(latitude: 1, longitude: 0), + try ParseGeoPoint(latitude: 0, longitude: 0) +] +do { + let polygon = try ParsePolygon(points) + let bytes = ParseBytes(data: "hello world".data(using: .utf8)!) + var gameData = GameData(bytes: bytes, polygon: polygon) + gameData = try gameData.save() + print("Successfully saved: \(gameData)") +} catch { + print("Error saving: \(error.localizedDescription)") +} + PlaygroundPage.current.finishExecution() //: [Next](@next) diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index e1b5b05c4..195b2da4d 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -32,7 +32,9 @@ struct GameScore: ParseObject { //: Define initial GameScore. var score = GameScore(score: 10) -score.location = ParseGeoPoint(latitude: 40.0, longitude: -30.0) +do { + try score.location = ParseGeoPoint(latitude: 40.0, longitude: -30.0) +} /*: Save asynchronously (preferred way) - performs work on background queue and returns to specified callbackQueue. @@ -61,22 +63,26 @@ score.save { result in } //: Now we will show how to query based on the `ParseGeoPoint`. -let pointToFind = ParseGeoPoint(latitude: 40.0, longitude: -30.0) +var query: Query //: Store query for later user var constraints = [QueryConstraint]() -constraints.append(near(key: "location", geoPoint: pointToFind)) -let query = GameScore.query(constraints) -query.find { results in - switch results { - case .success(let scores): +do { + let pointToFind = try ParseGeoPoint(latitude: 40.0, longitude: -30.0) + constraints.append(near(key: "location", geoPoint: pointToFind)) - assert(scores.count >= 1) - scores.forEach { (score) in - print("Someone with objectId \"\(score.objectId!)\" has a score of \"\(score.score)\" near me") - } + query = GameScore.query(constraints) + query.find { results in + switch results { + case .success(let scores): - case .failure(let error): - assertionFailure("Error querying: \(error)") + assert(scores.count >= 1) + scores.forEach { (score) in + print("Someone with objectId \"\(score.objectId!)\" has a score of \"\(score.score)\" near me") + } + + case .failure(let error): + assertionFailure("Error querying: \(error)") + } } } diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 695e6c461..9da83c4d9 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -369,6 +369,20 @@ 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */; }; 911DB13324C494390027F3C7 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13224C494390027F3C7 /* MockURLProtocol.swift */; }; 911DB13624C4FC100027F3C7 /* ParseObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */; }; + 91285B132698DBF20051B544 /* ParseBytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B122698DBF20051B544 /* ParseBytes.swift */; }; + 91285B142698DBF20051B544 /* ParseBytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B122698DBF20051B544 /* ParseBytes.swift */; }; + 91285B152698DBF20051B544 /* ParseBytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B122698DBF20051B544 /* ParseBytes.swift */; }; + 91285B162698DBF20051B544 /* ParseBytes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B122698DBF20051B544 /* ParseBytes.swift */; }; + 91285B182698E66D0051B544 /* ParseBytesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B172698E66D0051B544 /* ParseBytesTests.swift */; }; + 91285B192698E66D0051B544 /* ParseBytesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B172698E66D0051B544 /* ParseBytesTests.swift */; }; + 91285B1A2698E66D0051B544 /* ParseBytesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B172698E66D0051B544 /* ParseBytesTests.swift */; }; + 91285B1C26990D7F0051B544 /* ParsePolygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B1B26990D7F0051B544 /* ParsePolygon.swift */; }; + 91285B1D26990D7F0051B544 /* ParsePolygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B1B26990D7F0051B544 /* ParsePolygon.swift */; }; + 91285B1E26990D7F0051B544 /* ParsePolygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B1B26990D7F0051B544 /* ParsePolygon.swift */; }; + 91285B1F26990D7F0051B544 /* ParsePolygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B1B26990D7F0051B544 /* ParsePolygon.swift */; }; + 91285B2126991EE80051B544 /* ParsePolygonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B2026991EE80051B544 /* ParsePolygonTests.swift */; }; + 91285B2226991EE80051B544 /* ParsePolygonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B2026991EE80051B544 /* ParsePolygonTests.swift */; }; + 91285B2326991EE80051B544 /* ParsePolygonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91285B2026991EE80051B544 /* ParsePolygonTests.swift */; }; 912C9BCF24D3005D009947C3 /* ParseSwift_watchOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 912C9BCD24D3005D009947C3 /* ParseSwift_watchOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 912C9BDC24D3011F009947C3 /* ParseSwift_tvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 912C9BDA24D3011F009947C3 /* ParseSwift_tvOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 912C9BE024D302B0009947C3 /* Parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A82B7EE1F254B820063D731 /* Parse.swift */; }; @@ -707,6 +721,10 @@ 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICommandTests.swift; sourceTree = ""; }; 911DB13224C494390027F3C7 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectTests.swift; sourceTree = ""; }; + 91285B122698DBF20051B544 /* ParseBytes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseBytes.swift; sourceTree = ""; }; + 91285B172698E66D0051B544 /* ParseBytesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseBytesTests.swift; sourceTree = ""; }; + 91285B1B26990D7F0051B544 /* ParsePolygon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePolygon.swift; sourceTree = ""; }; + 91285B2026991EE80051B544 /* ParsePolygonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsePolygonTests.swift; sourceTree = ""; }; 912C9BCB24D3005D009947C3 /* ParseSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ParseSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 912C9BCD24D3005D009947C3 /* ParseSwift_watchOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ParseSwift_watchOS.h; sourceTree = ""; }; 912C9BCE24D3005D009947C3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -884,6 +902,7 @@ 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */, 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */, 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */, + 91285B172698E66D0051B544 /* ParseBytesTests.swift */, 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */, 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */, 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */, @@ -911,6 +930,7 @@ 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */, 70E09E1B262F0634002DD451 /* ParsePointerCombineTests.swift */, 70CE1D882545BF730018D572 /* ParsePointerTests.swift */, + 91285B2026991EE80051B544 /* ParsePolygonTests.swift */, 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */, @@ -1188,6 +1208,8 @@ F97B45C024D9C6F200F4A88B /* ParseACL.swift */, 70170A432656B02C0070C905 /* ParseAnalytics.swift */, 70170A482656E2FE0070C905 /* ParseAnalytics+combine.swift */, + 91285B122698DBF20051B544 /* ParseBytes.swift */, + 91285B1B26990D7F0051B544 /* ParsePolygon.swift */, 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */, 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */, 70D1BDB925BB17A600A42E7C /* ParseConfig.swift */, @@ -1711,6 +1733,7 @@ 70D1BDBA25BB17A600A42E7C /* ParseConfig.swift in Sources */, F97B465224D9C78C00F4A88B /* AddUnique.swift in Sources */, 91679D64268E596300F71809 /* ParseVersion.swift in Sources */, + 91285B1C26990D7F0051B544 /* ParsePolygon.swift in Sources */, F97B45D624D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70F79A272639D84600731C46 /* ParseHealth+combine.swift in Sources */, 700395A325A119430052CB31 /* Operations.swift in Sources */, @@ -1755,6 +1778,7 @@ F97B45EA24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BA25A1470F0052CB31 /* Subscription.swift in Sources */, + 91285B132698DBF20051B544 /* ParseBytes.swift in Sources */, 7016ED5625C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972A25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D125A147BE0052CB31 /* ParseSubscription.swift in Sources */, @@ -1797,11 +1821,13 @@ 89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4625D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 709B40C1268F999000ED2EAC /* IOS13Tests.swift in Sources */, + 91285B182698E66D0051B544 /* ParseBytesTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 70DFEA8A2618E77800F8EB4B /* InitializeSDKTests.swift in Sources */, + 91285B2126991EE80051B544 /* ParsePolygonTests.swift in Sources */, 70170A4E2656EBA50070C905 /* ParseAnalyticsTests.swift in Sources */, 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 89899D9F26045998002E2043 /* ParseTwitterCombineTests.swift in Sources */, @@ -1868,6 +1894,7 @@ 70D1BDBB25BB17A600A42E7C /* ParseConfig.swift in Sources */, F97B465324D9C78C00F4A88B /* AddUnique.swift in Sources */, 91679D65268E596300F71809 /* ParseVersion.swift in Sources */, + 91285B1D26990D7F0051B544 /* ParsePolygon.swift in Sources */, F97B45D724D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70F79A282639D84600731C46 /* ParseHealth+combine.swift in Sources */, 700395A425A119430052CB31 /* Operations.swift in Sources */, @@ -1912,6 +1939,7 @@ F97B45EB24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BB25A1470F0052CB31 /* Subscription.swift in Sources */, + 91285B142698DBF20051B544 /* ParseBytes.swift in Sources */, 7016ED5725C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972B25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D225A147BE0052CB31 /* ParseSubscription.swift in Sources */, @@ -1963,11 +1991,13 @@ 89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4825D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 709B40C3268F999000ED2EAC /* IOS13Tests.swift in Sources */, + 91285B1A2698E66D0051B544 /* ParseBytesTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 70DFEA8C2618E77800F8EB4B /* InitializeSDKTests.swift in Sources */, + 91285B2326991EE80051B544 /* ParsePolygonTests.swift in Sources */, 70170A502656EBA50070C905 /* ParseAnalyticsTests.swift in Sources */, 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 89899DA126045998002E2043 /* ParseTwitterCombineTests.swift in Sources */, @@ -2027,11 +2057,13 @@ 89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4725D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 709B40C2268F999000ED2EAC /* IOS13Tests.swift in Sources */, + 91285B192698E66D0051B544 /* ParseBytesTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderExtraTests.swift in Sources */, 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectIdTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 70DFEA8B2618E77800F8EB4B /* InitializeSDKTests.swift in Sources */, + 91285B2226991EE80051B544 /* ParsePolygonTests.swift in Sources */, 70170A4F2656EBA50070C905 /* ParseAnalyticsTests.swift in Sources */, 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 89899DA026045998002E2043 /* ParseTwitterCombineTests.swift in Sources */, @@ -2098,6 +2130,7 @@ 70D1BDBD25BB17A600A42E7C /* ParseConfig.swift in Sources */, F97B45E524D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 91679D67268E596300F71809 /* ParseVersion.swift in Sources */, + 91285B1F26990D7F0051B544 /* ParsePolygon.swift in Sources */, F97B465D24D9C78C00F4A88B /* Increment.swift in Sources */, 70F79A2A2639D84600731C46 /* ParseHealth+combine.swift in Sources */, 700395A625A119430052CB31 /* Operations.swift in Sources */, @@ -2142,6 +2175,7 @@ 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BD25A1470F0052CB31 /* Subscription.swift in Sources */, + 91285B162698DBF20051B544 /* ParseBytes.swift in Sources */, 7016ED5925C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972D25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D425A147BE0052CB31 /* ParseSubscription.swift in Sources */, @@ -2191,6 +2225,7 @@ 70D1BDBC25BB17A600A42E7C /* ParseConfig.swift in Sources */, F97B45E424D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 91679D66268E596300F71809 /* ParseVersion.swift in Sources */, + 91285B1E26990D7F0051B544 /* ParsePolygon.swift in Sources */, F97B465C24D9C78C00F4A88B /* Increment.swift in Sources */, 70F79A292639D84600731C46 /* ParseHealth+combine.swift in Sources */, 700395A525A119430052CB31 /* Operations.swift in Sources */, @@ -2235,6 +2270,7 @@ 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BC25A1470F0052CB31 /* Subscription.swift in Sources */, + 91285B152698DBF20051B544 /* ParseBytes.swift in Sources */, 7016ED5825C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972C25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D325A147BE0052CB31 /* ParseSubscription.swift in Sources */, diff --git a/Sources/ParseSwift/Types/ParseBytes.swift b/Sources/ParseSwift/Types/ParseBytes.swift new file mode 100644 index 000000000..e5d721142 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseBytes.swift @@ -0,0 +1,70 @@ +// +// ParseBytes.swift +// ParseSwift +// +// Created by Corey Baker on 7/9/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +/** + `ParseBytes` is used to stote bytes. + It could be used to perform queries. +*/ +public struct ParseBytes: Codable, Hashable { + private let __type: String = "Bytes" // swiftlint:disable:this identifier_name + public let base64: String + + enum CodingKeys: String, CodingKey { + case __type // swiftlint:disable:this identifier_name + case base64 + } + + /** + Create new `ParseBytes` instance with the specified base64 string. + - parameter base64: A base64 string. + */ + public init(base64: String) { + self.base64 = base64 + } + + /** + Create new `ParseBytes` instance with the specified data. + - parameter data: The data to encode to a base64 string. + */ + public init(data: Data) { + self.base64 = data.base64EncodedString() + } +} + +extension ParseBytes { + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + base64 = try values.decode(String.self, forKey: .base64) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(__type, forKey: .__type) + try container.encode(base64, forKey: .base64) + } +} + +// MARK: CustomDebugStringConvertible +extension ParseBytes: CustomDebugStringConvertible { + public var debugDescription: String { + guard let descriptionData = try? ParseCoding.jsonEncoder().encode(self), + let descriptionString = String(data: descriptionData, encoding: .utf8) else { + return "ParseBytes ()" + } + return "ParseBytes (\(descriptionString))" + } +} + +// MARK: CustomStringConvertible +extension ParseBytes: CustomStringConvertible { + public var description: String { + debugDescription + } +} diff --git a/Sources/ParseSwift/Types/ParseGeoPoint.swift b/Sources/ParseSwift/Types/ParseGeoPoint.swift index ceea102cd..a17580e6f 100644 --- a/Sources/ParseSwift/Types/ParseGeoPoint.swift +++ b/Sources/ParseSwift/Types/ParseGeoPoint.swift @@ -6,7 +6,8 @@ import CoreLocation /** `ParseGeoPoint` is used to embed a latitude / longitude point as the value for a key in a `ParseObject`. It could be used to perform queries in a geospatial manner using `ParseQuery.whereKey:nearGeoPoint:`. - Currently, instances of `ParseObject` may only have one key associated with a `ParseGeoPoint` type. + + - warning:Currently, instances of `ParseObject` may only have one key associated with a `ParseGeoPoint` type. */ public struct ParseGeoPoint: Codable, Hashable { private let __type: String = "GeoPoint" // swiftlint:disable:this identifier_name @@ -20,64 +21,59 @@ public struct ParseGeoPoint: Codable, Hashable { /** Latitude of point in degrees. Valid range is from `-90.0` to `90.0`. */ - public var latitude: Double { - get { - return _latitude - } - set { - assert(newValue > -90, "latitude should be > -90") - assert(newValue < 90, "latitude should be > 90") - _latitude = newValue - } - } + public var latitude: Double /** Longitude of point in degrees. Valid range is from `-180.0` to `180.0`. */ - public var longitude: Double { - get { - return _longitude - } - set { - assert(newValue > -180, "longitude should be > -180") - assert(newValue < 180, "longitude should be > -180") - _longitude = newValue - } - } - - private var _latitude: Double - private var _longitude: Double + public var longitude: Double /** Create a `ParseGeoPoint` instance. Latitude and longitude are set to `0.0`. */ public init() { - _latitude = 0.0 - _longitude = 0.0 + latitude = 0.0 + longitude = 0.0 } /** Create a new `ParseGeoPoint` instance with the specified latitude and longitude. - parameter latitude: Latitude of point in degrees. - parameter longitude: Longitude of point in degrees. + - throws: `ParseError`. */ - public init(latitude: Double, longitude: Double) { - assert(longitude > -180, "longitude should be > -180") - assert(longitude < 180, "longitude should be > -180") - assert(latitude > -90, "latitude should be > -90") - assert(latitude < 90, "latitude should be > 90") - self._latitude = latitude - self._longitude = longitude + public init(latitude: Double, longitude: Double) throws { + self.latitude = latitude + self.longitude = longitude + try validate() + } + + func validate() throws { + if longitude < -180 { + throw ParseError(code: .unknownError, + message: "longitude should be > -180") + } else if longitude > 180 { + throw ParseError(code: .unknownError, + message: "longitude should be < 180") + } else if latitude < -90 { + throw ParseError(code: .unknownError, + message: "latitude should be > -90") + } else if latitude > 90 { + throw ParseError(code: .unknownError, + message: "latitude should be < 90") + } } #if canImport(CoreLocation) /** Creates a new `ParseGeoPoint` instance for the given `CLLocation`, set to the location's coordinates. - parameter location: Instance of `CLLocation`, with set latitude and longitude. + - throws: `ParseError`. */ - public init(location: CLLocation) { - self._longitude = location.coordinate.longitude - self._latitude = location.coordinate.latitude + public init(location: CLLocation) throws { + self.longitude = location.coordinate.longitude + self.latitude = location.coordinate.latitude + try validate() } #endif @@ -127,15 +123,17 @@ public struct ParseGeoPoint: Codable, Hashable { extension ParseGeoPoint { public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - _longitude = try values.decode(Double.self, forKey: .longitude) - _latitude = try values.decode(Double.self, forKey: .latitude) + longitude = try values.decode(Double.self, forKey: .longitude) + latitude = try values.decode(Double.self, forKey: .latitude) + try validate() } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(__type, forKey: .__type) - try container.encode(_longitude, forKey: .longitude) - try container.encode(_latitude, forKey: .latitude) + try container.encode(longitude, forKey: .longitude) + try container.encode(latitude, forKey: .latitude) + try validate() } } diff --git a/Sources/ParseSwift/Types/ParsePolygon.swift b/Sources/ParseSwift/Types/ParsePolygon.swift new file mode 100644 index 000000000..3eaac7fe4 --- /dev/null +++ b/Sources/ParseSwift/Types/ParsePolygon.swift @@ -0,0 +1,150 @@ +// +// ParsePolygon.swift +// ParseSwift +// +// Created by Corey Baker on 7/9/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +/** + `ParsePolygon` is used to create a polygon that represents the coordinates + that may be associated with a key in a ParseObject or used as a reference point + for geo queries. This allows proximity-based queries on the key. +*/ +public struct ParsePolygon: Codable, Hashable { + private let __type: String = "Polygon" // swiftlint:disable:this identifier_name + public let coordinates: [ParseGeoPoint] + + enum CodingKeys: String, CodingKey { + case __type // swiftlint:disable:this identifier_name + case coordinates + } + + /** + Create new `ParsePolygon` instance with coordinates. + - parameter coordinates: The geopoints that make the polygon. + - throws: `ParseError`. + */ + public init(_ coordinates: [ParseGeoPoint]) throws { + self.coordinates = coordinates + try validate() + } + + /** + Create new `ParsePolygon` instance with a variadic amount of coordinates. + - parameter coordinates: variadic amount of zero or more `ParseGeoPoint`'s. + - throws: `ParseError`. + */ + public init(_ coordinates: ParseGeoPoint...) throws { + self.coordinates = coordinates + try validate() + } + + func validate() throws { + if coordinates.count < 3 { + throw ParseError(code: .unknownError, + message: "Polygon must have at least 3 ParseGeoPoint's or Points") + } + } + + /** + Determines if a `ParsePolygon` containes a point. + - parameter point: The point to check. + */ + public func containsPoint(_ point: ParseGeoPoint) -> Bool { + var minX = coordinates[0].latitude + var maxX = coordinates[0].latitude + var minY = coordinates[0].longitude + var maxY = coordinates[0].longitude + + var modifiedCoordinates = coordinates + modifiedCoordinates.removeFirst() + for coordinate in modifiedCoordinates { + minX = Swift.min(coordinate.latitude, minX) + maxX = Swift.max(coordinate.latitude, maxX) + minY = Swift.min(coordinate.longitude, minY) + maxY = Swift.max(coordinate.longitude, maxY) + } + + // Check if outside of the polygon + if point.latitude < minX || + point.latitude > maxX || + point.longitude < minY || + point.longitude > maxY { + return false + } + + modifiedCoordinates = coordinates + + // Check if intersects polygon + var otherIndex = coordinates.count - 1 + for (index, coordinate) in coordinates.enumerated() { + let startX = coordinate.latitude + let startY = coordinate.longitude + let endX = coordinates[otherIndex].latitude + let endY = coordinates[otherIndex].longitude + let startYComparison = startY > point.longitude + let endYComparison = endY > point.longitude + if startYComparison != endYComparison && + point.latitude < ((endX - startX) * (point.longitude - startY)) / (endY - startY) + startX { + return true + } + if index == 0 { + otherIndex = index + } else { + otherIndex += 1 + } + } + return false + } +} + +extension ParsePolygon { + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + var decodedCoordinates = [ParseGeoPoint]() + let points = try values.decode([[Double]].self, forKey: .coordinates) + try points.forEach { + if $0.count == 2 { + guard let latitude = $0.first, + let longitude = $0.last else { + throw ParseError(code: .unknownError, message: "Could not decode ParsePolygon: \(points)") + } + decodedCoordinates.append(try ParseGeoPoint(latitude: latitude, + longitude: longitude)) + } else { + throw ParseError(code: .unknownError, message: "Could not decode ParsePolygon: \(points)") + } + } + coordinates = decodedCoordinates + try validate() + } + + public func encode(to encoder: Encoder) throws { + try validate() + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(__type, forKey: .__type) + var nestedUnkeyedContainer = container.nestedUnkeyedContainer(forKey: .coordinates) + try coordinates.forEach { + try nestedUnkeyedContainer.encode([$0.latitude, $0.longitude]) + } + } +} + +// MARK: CustomDebugStringConvertible +extension ParsePolygon: CustomDebugStringConvertible { + public var debugDescription: String { + guard let descriptionData = try? ParseCoding.jsonEncoder().encode(self), + let descriptionString = String(data: descriptionData, encoding: .utf8) else { + return "ParsePolygon ()" + } + return "ParsePolygon (\(descriptionString))" + } +} + +// MARK: CustomStringConvertible +extension ParsePolygon: CustomStringConvertible { + public var description: String { + debugDescription + } +} diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index eff0c3876..02e07d6d3 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -434,16 +434,33 @@ public func withinGeoBox(key: String, fromSouthWest southwest: ParseGeoPoint, - returns: The same instance of `QueryConstraint` as the receiver. */ public func withinPolygon(key: String, points: [ParseGeoPoint]) -> QueryConstraint { - let dictionary = [QueryConstraint.Comparator.polygon.rawValue: points] + let polygon = points.flatMap { [[$0.latitude, $0.longitude]]} + let dictionary = [QueryConstraint.Comparator.polygon.rawValue: polygon] return .init(key: key, value: dictionary, comparator: .geoWithin) } /** Add a constraint to the query that requires a particular key's - coordinates that contains a `ParseGeoPoint`. + coordinates be contained within and on the bounds of a given polygon + Supports closed and open (last point is connected to first) paths. - parameter key: The key to be constrained. - - parameter point: The point the polygon contains `ParseGeoPoint`. + - parameter polygon: The `ParsePolygon`. + - warning: Requires Parse Server 2.5.0+. + - returns: The same instance of `QueryConstraint` as the receiver. + */ +public func withinPolygon(key: String, polygon: ParsePolygon) -> QueryConstraint { + let polygon = polygon.coordinates.flatMap { [[$0.latitude, $0.longitude]]} + let dictionary = [QueryConstraint.Comparator.polygon.rawValue: polygon] + return .init(key: key, value: dictionary, comparator: .geoWithin) +} + +/** + Add a constraint to the query that requires a particular key's + coordinates contains a `ParseGeoPoint`. + + - parameter key: The key of the `ParsePolygon`. + - parameter point: The `ParseGeoPoint` to check for containment. - warning: Requires Parse Server 2.6.0+. - returns: The same instance of `QueryConstraint` as the receiver. */ diff --git a/Tests/ParseSwiftTests/ParseBytesTests.swift b/Tests/ParseSwiftTests/ParseBytesTests.swift new file mode 100644 index 000000000..d38a47db1 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseBytesTests.swift @@ -0,0 +1,67 @@ +// +// ParseBytesTests.swift +// ParseSwift +// +// Created by Corey Baker on 7/9/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import XCTest +@testable import ParseSwift + +class ParseBytesTests: XCTestCase { + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) && !os(Android) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testDecode() throws { + let bytes = ParseBytes(base64: "ZnJveW8=") + let encoded = try ParseCoding.jsonEncoder().encode(bytes) + let decoded = try ParseCoding.jsonDecoder().decode(ParseBytes.self, from: encoded) + XCTAssertEqual(decoded, bytes) + } + + #if !os(Linux) && !os(Android) + func testDebugString() { + let bytes = ParseBytes(base64: "ZnJveW8=") + let expected = "ParseBytes ({\"__type\":\"Bytes\",\"base64\":\"ZnJveW8=\"})" + XCTAssertEqual(bytes.debugDescription, expected) + guard let data = Data(base64Encoded: "ZnJveW8=") else { + XCTFail("Should have unwrapped") + return + } + let bytes2 = ParseBytes(data: data) + XCTAssertEqual(bytes2.debugDescription, expected) + } + + func testDescription() { + let bytes = ParseBytes(base64: "ZnJveW8=") + let expected = "ParseBytes ({\"__type\":\"Bytes\",\"base64\":\"ZnJveW8=\"})" + XCTAssertEqual(bytes.description, expected) + guard let data = Data(base64Encoded: "ZnJveW8=") else { + XCTFail("Should have unwrapped") + return + } + let bytes2 = ParseBytes(data: data) + XCTAssertEqual(bytes2.description, expected) + } + #endif +} diff --git a/Tests/ParseSwiftTests/ParseGeoPointTests.swift b/Tests/ParseSwiftTests/ParseGeoPointTests.swift index 6dbd8be70..344be9349 100644 --- a/Tests/ParseSwiftTests/ParseGeoPointTests.swift +++ b/Tests/ParseSwiftTests/ParseGeoPointTests.swift @@ -40,19 +40,23 @@ class ParseGeoPointTests: XCTestCase { // Check default values XCTAssertEqual(point.latitude, 0.0, accuracy: 0.00001, "Latitude should be 0.0") XCTAssertEqual(point.longitude, 0.0, accuracy: 0.00001, "Longitude should be 0.0") + XCTAssertThrowsError(try ParseGeoPoint(latitude: 100, longitude: 0)) + XCTAssertThrowsError(try ParseGeoPoint(latitude: -100, longitude: 0)) + XCTAssertThrowsError(try ParseGeoPoint(latitude: 0, longitude: 200)) + XCTAssertThrowsError(try ParseGeoPoint(latitude: 0, longitude: -200)) } #if canImport(CoreLocation) - func testGeoPointFromLocation() { + func testGeoPointFromLocation() throws { let location = CLLocation(latitude: 10.0, longitude: 20.0) - let geoPoint = ParseGeoPoint(location: location) + let geoPoint = try ParseGeoPoint(location: location) XCTAssertEqual(geoPoint.latitude, location.coordinate.latitude) XCTAssertEqual(geoPoint.longitude, location.coordinate.longitude) } #endif - func testGeoPointEncoding() { - let point = ParseGeoPoint(latitude: 10, longitude: 20) + func testGeoPointEncoding() throws { + let point = try ParseGeoPoint(latitude: 10, longitude: 20) do { let encoded = try ParseCoding.jsonEncoder().encode(point) @@ -64,21 +68,21 @@ class ParseGeoPointTests: XCTestCase { } #if !os(Linux) && !os(Android) - func testDebugString() { - let point = ParseGeoPoint(latitude: 10, longitude: 20) + func testDebugString() throws { + let point = try ParseGeoPoint(latitude: 10, longitude: 20) let expected = "ParseGeoPoint ({\"__type\":\"GeoPoint\",\"longitude\":20,\"latitude\":10})" XCTAssertEqual(point.debugDescription, expected) } - func testDescription() { - let point = ParseGeoPoint(latitude: 10, longitude: 20) + func testDescription() throws { + let point = try ParseGeoPoint(latitude: 10, longitude: 20) let expected = "ParseGeoPoint ({\"__type\":\"GeoPoint\",\"longitude\":20,\"latitude\":10})" XCTAssertEqual(point.description, expected) } #endif // swiftlint:disable:next function_body_length - func testGeoUtilityDistance() { + func testGeoUtilityDistance() throws { let d2R = Double.pi / 180.0 var pointA = ParseGeoPoint() var pointB = ParseGeoPoint() @@ -151,28 +155,28 @@ class ParseGeoPointTests: XCTestCase { accuracy: 0.01, "Sydney to Buenos Aires Fail") // [SAC] 38.52 -121.50 Sacramento,CA - let sacramento = ParseGeoPoint(latitude: 38.52, longitude: -121.50) + let sacramento = try ParseGeoPoint(latitude: 38.52, longitude: -121.50) // [HNL] 21.35 -157.93 Honolulu Int,HI - let honolulu = ParseGeoPoint(latitude: 21.35, longitude: -157.93) + let honolulu = try ParseGeoPoint(latitude: 21.35, longitude: -157.93) // [51Q] 37.75 -122.68 San Francisco,CA - let sanfran = ParseGeoPoint(latitude: 37.75, longitude: -122.68) + let sanfran = try ParseGeoPoint(latitude: 37.75, longitude: -122.68) // Vorkuta 67.509619,64.085999 - let vorkuta = ParseGeoPoint(latitude: 67.509619, longitude: 64.085999) + let vorkuta = try ParseGeoPoint(latitude: 67.509619, longitude: 64.085999) // London - let london = ParseGeoPoint(latitude: 51.501904, longitude: -0.115356) + let london = try ParseGeoPoint(latitude: 51.501904, longitude: -0.115356) // Northampton - let northhampton = ParseGeoPoint(latitude: 52.241256, longitude: -0.895386) + let northhampton = try ParseGeoPoint(latitude: 52.241256, longitude: -0.895386) // Powell St BART station - let powell = ParseGeoPoint(latitude: 37.78507, longitude: -122.407007) + let powell = try ParseGeoPoint(latitude: 37.78507, longitude: -122.407007) // Apple store - let astore = ParseGeoPoint(latitude: 37.785809, longitude: -122.406363) + let astore = try ParseGeoPoint(latitude: 37.785809, longitude: -122.406363) // Self XCTAssertEqual(honolulu.distanceInKilometers(honolulu), 0.0, @@ -206,8 +210,8 @@ class ParseGeoPointTests: XCTestCase { accuracy: 100.0, "Sacramento to Vorkuta") } - func testDebugGeoPoint() { - let point = ParseGeoPoint(latitude: 10, longitude: 20) + func testDebugGeoPoint() throws { + let point = try ParseGeoPoint(latitude: 10, longitude: 20) XCTAssertTrue(point.debugDescription.contains("10")) XCTAssertTrue(point.debugDescription.contains("20")) } diff --git a/Tests/ParseSwiftTests/ParsePolygonTests.swift b/Tests/ParseSwiftTests/ParsePolygonTests.swift new file mode 100644 index 000000000..151156d62 --- /dev/null +++ b/Tests/ParseSwiftTests/ParsePolygonTests.swift @@ -0,0 +1,131 @@ +// +// ParsePolygonTests.swift +// ParseSwift +// +// Created by Corey Baker on 7/9/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import XCTest +@testable import ParseSwift + +class ParsePolygonTests: XCTestCase { + + struct FakeParsePolygon: Encodable, Hashable { + private let __type: String = "Polygon" // swiftlint:disable:this identifier_name + public let coordinates: [[Double]] + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + points = [ + try ParseGeoPoint(latitude: 0, longitude: 0), + try ParseGeoPoint(latitude: 0, longitude: 1), + try ParseGeoPoint(latitude: 1, longitude: 1), + try ParseGeoPoint(latitude: 1, longitude: 0), + try ParseGeoPoint(latitude: 0, longitude: 0) + ] + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) && !os(Android) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + var points = [ParseGeoPoint]() + + func testContainsPoint() throws { + let polygon = try ParsePolygon(points) + let inside = try ParseGeoPoint(latitude: 0.5, longitude: 0.5) + let outside = try ParseGeoPoint(latitude: 10, longitude: 10) + XCTAssertTrue(polygon.containsPoint(inside)) + XCTAssertFalse(polygon.containsPoint(outside)) + } + + func testCheckInitializerRequiresMinPoints() throws { + let point = try ParseGeoPoint(latitude: 0, longitude: 0) + XCTAssertNoThrow(try ParsePolygon([point, point, point])) + XCTAssertThrowsError(try ParsePolygon([point, point])) + XCTAssertNoThrow(try ParsePolygon(point, point, point)) + XCTAssertThrowsError(try ParsePolygon(point, point)) + } + + func testDecode() throws { + let polygon = try ParsePolygon(points) + let encoded = try ParseCoding.jsonEncoder().encode(polygon) + let decoded = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded) + XCTAssertEqual(decoded, polygon) + } + + func testDecodeFailNotEnoughPoints() throws { + let fakePolygon = FakeParsePolygon(coordinates: [[0.0, 0.0], [0.0, 1.0]]) + let encoded = try ParseCoding.jsonEncoder().encode(fakePolygon) + do { + _ = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded) + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have unwrapped") + return + } + XCTAssertTrue(parseError.message.contains("3 ParseGeoPoint")) + } + } + + func testDecodeFailWrongData() throws { + let fakePolygon = FakeParsePolygon(coordinates: [[0.0], [1.0]]) + let encoded = try ParseCoding.jsonEncoder().encode(fakePolygon) + do { + _ = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded) + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have unwrapped") + return + } + XCTAssertTrue(parseError.message.contains("decode ParsePolygon")) + } + } + + func testDecodeFailTooMuchCoordinates() throws { + let fakePolygon = FakeParsePolygon(coordinates: [[0.0, 0.0, 0.0], [0.0, 1.0, 1.0]]) + let encoded = try ParseCoding.jsonEncoder().encode(fakePolygon) + do { + _ = try ParseCoding.jsonDecoder().decode(ParsePolygon.self, from: encoded) + XCTFail("Should have failed") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have unwrapped") + return + } + XCTAssertTrue(parseError.message.contains("decode ParsePolygon")) + } + } + + #if !os(Linux) && !os(Android) + func testDebugString() throws { + let polygon = try ParsePolygon(points) + let expected = "ParsePolygon ({\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]})" + XCTAssertEqual(polygon.debugDescription, expected) + } + + func testDescription() throws { + let polygon = try ParsePolygon(points) + let expected = "ParsePolygon ({\"__type\":\"Polygon\",\"coordinates\":[[0,0],[0,1],[1,1],[1,0],[0,0]]})" + XCTAssertEqual(polygon.description, expected) + } + #endif +} diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index eb4d308d1..8ce70ea15 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -1928,11 +1928,11 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } // MARK: GeoPoint - func testWhereKeyNearGeoPoint() { + func testWhereKeyNearGeoPoint() throws { let expected: [String: AnyCodable] = [ "yolo": ["$nearSphere": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"]] ] - let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint = try ParseGeoPoint(latitude: 10, longitude: 20) let constraint = near(key: "yolo", geoPoint: geoPoint) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -1968,13 +1968,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } #if !os(Linux) && !os(Android) - func testWhereKeyNearGeoPointWithinMiles() { + func testWhereKeyNearGeoPointWithinMiles() throws { let expected: [String: AnyCodable] = [ "yolo": ["$nearSphere": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], "$maxDistance": 1 ] ] - let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint = try ParseGeoPoint(latitude: 10, longitude: 20) let constraint = withinMiles(key: "yolo", geoPoint: geoPoint, distance: 3958.8) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -2016,13 +2016,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length #endif #if !os(Linux) && !os(Android) - func testWhereKeyNearGeoPointWithinKilometers() { + func testWhereKeyNearGeoPointWithinKilometers() throws { let expected: [String: AnyCodable] = [ "yolo": ["$nearSphere": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], "$maxDistance": 1 ] ] - let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint = try ParseGeoPoint(latitude: 10, longitude: 20) let constraint = withinKilometers(key: "yolo", geoPoint: geoPoint, distance: 6371.0) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -2062,13 +2062,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func testWhereKeyNearGeoPointWithinRadians() { + func testWhereKeyNearGeoPointWithinRadians() throws { let expected: [String: AnyCodable] = [ "yolo": ["$nearSphere": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], "$maxDistance": 10 ] ] - let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint = try ParseGeoPoint(latitude: 10, longitude: 20) let constraint = withinRadians(key: "yolo", geoPoint: geoPoint, distance: 10.0) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -2108,13 +2108,13 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func testWhereKeyNearGeoPointWithinRadiansNotSorted() { + func testWhereKeyNearGeoPointWithinRadiansNotSorted() throws { let expected: [String: AnyCodable] = [ "yolo": ["$centerSphere": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], "$geoWithin": 10 ] ] - let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint = try ParseGeoPoint(latitude: 10, longitude: 20) let constraint = withinRadians(key: "yolo", geoPoint: geoPoint, distance: 10.0, sorted: false) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -2156,7 +2156,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length #endif // swiftlint:disable:next function_body_length - func testWhereKeyNearGeoBox() { + func testWhereKeyNearGeoBox() throws { let expected: [String: AnyCodable] = [ "yolo": ["$within": ["$box": [ ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], @@ -2164,8 +2164,8 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length ] ] ] - let geoPoint1 = ParseGeoPoint(latitude: 10, longitude: 20) - let geoPoint2 = ParseGeoPoint(latitude: 20, longitude: 30) + let geoPoint1 = try ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint2 = try ParseGeoPoint(latitude: 20, longitude: 30) let constraint = withinGeoBox(key: "yolo", fromSouthWest: geoPoint1, toNortheast: geoPoint2) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -2212,19 +2212,20 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } // swiftlint:disable:next function_body_length - func testWhereKeyWithinPolygon() { + func testWhereKeyWithinPolygonPoints() throws { let expected: [String: AnyCodable] = [ "yolo": ["$geoWithin": ["$polygon": [ - ["latitude": 10, "longitude": 20, "__type": "GeoPoint"], - ["latitude": 20, "longitude": 30, "__type": "GeoPoint"], - ["latitude": 30, "longitude": 40, "__type": "GeoPoint"]] + [10.1, 20.1], + [20.1, 30.1], + [30.1, 40.1]] ] ] ] - let geoPoint1 = ParseGeoPoint(latitude: 10, longitude: 20) - let geoPoint2 = ParseGeoPoint(latitude: 20, longitude: 30) - let geoPoint3 = ParseGeoPoint(latitude: 30, longitude: 40) - let constraint = withinPolygon(key: "yolo", points: [geoPoint1, geoPoint2, geoPoint3]) + let geoPoint1 = try ParseGeoPoint(latitude: 10.1, longitude: 20.1) + let geoPoint2 = try ParseGeoPoint(latitude: 20.1, longitude: 30.1) + let geoPoint3 = try ParseGeoPoint(latitude: 30.1, longitude: 40.1) + let polygon = [geoPoint1, geoPoint2, geoPoint3] + let constraint = withinPolygon(key: "yolo", points: polygon) let query = GameScore.query(constraint) let queryWhere = query.`where` @@ -2233,44 +2234,60 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) - guard let expectedValues = expected.values.first?.value as? [String: [String: [[String: Any]]]], - let expectedBox = expectedValues["$geoWithin"]?["$polygon"], - let expectedLongitude = expectedBox.first?["longitude"] as? Int, - let expectedLatitude = expectedBox.first?["latitude"] as? Int, - let expectedType = expectedBox.first?["__type"] as? String, - let expectedLongitude2 = expectedBox[1]["longitude"] as? Int, - let expectedLatitude2 = expectedBox[1]["latitude"] as? Int, - let expectedType2 = expectedBox[1]["__type"] as? String, - let expectedLongitude3 = expectedBox.last?["longitude"] as? Int, - let expectedLatitude3 = expectedBox.last?["latitude"] as? Int, - let expectedType3 = expectedBox.last?["__type"] as? String else { + guard let expectedValues = expected.values.first?.value as? [String: [String: [[Double]]]], + let expectedBox = expectedValues["$geoWithin"]?["$polygon"] else { XCTFail("Should have casted") return } - guard let decodedValues = decodedDictionary.values.first?.value as? [String: [String: [[String: Any]]]], - let decodedBox = decodedValues["$geoWithin"]?["$polygon"], - let decodedLongitude = decodedBox.first?["longitude"] as? Int, - let decodedLatitude = decodedBox.first?["latitude"] as? Int, - let decodedType = decodedBox.first?["__type"] as? String, - let decodedLongitude2 = decodedBox[1]["longitude"] as? Int, - let decodedLatitude2 = decodedBox[1]["latitude"] as? Int, - let decodedType2 = decodedBox[1]["__type"] as? String, - let decodedLongitude3 = decodedBox.last?["longitude"] as? Int, - let decodedLatitude3 = decodedBox.last?["latitude"] as? Int, - let decodedType3 = decodedBox.last?["__type"] as? String else { + guard let decodedValues = decodedDictionary.values.first?.value as? [String: [String: [[Double]]]], + let decodedBox = decodedValues["$geoWithin"]?["$polygon"] else { XCTFail("Should have casted") return } - XCTAssertEqual(expectedLongitude, decodedLongitude) - XCTAssertEqual(expectedLatitude, decodedLatitude) - XCTAssertEqual(expectedType, decodedType) - XCTAssertEqual(expectedLongitude2, decodedLongitude2) - XCTAssertEqual(expectedLatitude2, decodedLatitude2) - XCTAssertEqual(expectedType2, decodedType2) - XCTAssertEqual(expectedLongitude3, decodedLongitude3) - XCTAssertEqual(expectedLatitude3, decodedLatitude3) - XCTAssertEqual(expectedType3, decodedType3) + XCTAssertEqual(expectedBox, decodedBox) + + } catch { + XCTFail(error.localizedDescription) + return + } + } + + // swiftlint:disable:next function_body_length + func testWhereKeyWithinPolygon() throws { + let expected: [String: AnyCodable] = [ + "yolo": ["$geoWithin": ["$polygon": [ + [10.1, 20.1], + [20.1, 30.1], + [30.1, 40.1]] + ] + ] + ] + let geoPoint1 = try ParseGeoPoint(latitude: 10.1, longitude: 20.1) + let geoPoint2 = try ParseGeoPoint(latitude: 20.1, longitude: 30.1) + let geoPoint3 = try ParseGeoPoint(latitude: 30.1, longitude: 40.1) + let polygon = try ParsePolygon(geoPoint1, geoPoint2, geoPoint3) + let constraint = withinPolygon(key: "yolo", polygon: polygon) + let query = GameScore.query(constraint) + let queryWhere = query.`where` + + do { + let encoded = try ParseCoding.jsonEncoder().encode(queryWhere) + let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) + XCTAssertEqual(expected.keys, decodedDictionary.keys) + + guard let expectedValues = expected.values.first?.value as? [String: [String: [[Double]]]], + let expectedBox = expectedValues["$geoWithin"]?["$polygon"] else { + XCTFail("Should have casted") + return + } + + guard let decodedValues = decodedDictionary.values.first?.value as? [String: [String: [[Double]]]], + let decodedBox = decodedValues["$geoWithin"]?["$polygon"] else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(expectedBox, decodedBox) } catch { XCTFail(error.localizedDescription) @@ -2278,14 +2295,14 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func testWhereKeyPolygonContains() { + func testWhereKeyPolygonContains() throws { let expected: [String: AnyCodable] = [ "yolo": ["$geoIntersects": ["$point": ["latitude": 10, "longitude": 20, "__type": "GeoPoint"] ] ] ] - let geoPoint = ParseGeoPoint(latitude: 10, longitude: 20) + let geoPoint = try ParseGeoPoint(latitude: 10, longitude: 20) let constraint = polygonContains(key: "yolo", point: geoPoint) let query = GameScore.query(constraint) let queryWhere = query.`where`